From ae7b0c4821c7c28c53792068ed19860265dda4f4 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Fri, 4 Apr 2025 16:14:32 +0200 Subject: [PATCH 1/7] rebase --- .../runtime/diagnostics/client-commands.ts | 425 ++++++++++++++++++ .../browser/runtime/diagnostics/common.ts | 103 +++++ .../browser/runtime/diagnostics/diag-js.ts | 148 ++++++ .../browser/runtime/diagnostics/diag-ws.ts | 61 +++ .../runtime/diagnostics/dotnet-counters.ts | 32 ++ .../diagnostics/dotnet-cpu-profiler.ts | 35 ++ .../runtime/diagnostics/dotnet-gcdump.ts | 41 ++ src/mono/browser/runtime/diagnostics/index.ts | 78 +++- src/mono/browser/runtime/dotnet.d.ts | 40 +- src/mono/browser/runtime/driver.c | 1 + src/mono/browser/runtime/export-api.ts | 4 + src/mono/browser/runtime/startup.ts | 1 + src/mono/browser/runtime/types/index.ts | 45 +- .../Wasm.Advanced.Sample.csproj | 2 +- .../sample/wasm/browser-eventpipe/Program.cs | 168 +++++++ .../Wasm.Browser.EventPipe.Sample.csproj | 18 + .../sample/wasm/browser-eventpipe/index.html | 25 ++ .../sample/wasm/browser-eventpipe/main.js | 66 +++ src/mono/wasm/features.md | 21 + 19 files changed, 1300 insertions(+), 14 deletions(-) create mode 100644 src/mono/browser/runtime/diagnostics/client-commands.ts create mode 100644 src/mono/browser/runtime/diagnostics/common.ts create mode 100644 src/mono/browser/runtime/diagnostics/diag-js.ts create mode 100644 src/mono/browser/runtime/diagnostics/diag-ws.ts create mode 100644 src/mono/browser/runtime/diagnostics/dotnet-counters.ts create mode 100644 src/mono/browser/runtime/diagnostics/dotnet-cpu-profiler.ts create mode 100644 src/mono/browser/runtime/diagnostics/dotnet-gcdump.ts create mode 100644 src/mono/sample/wasm/browser-eventpipe/Program.cs create mode 100644 src/mono/sample/wasm/browser-eventpipe/Wasm.Browser.EventPipe.Sample.csproj create mode 100644 src/mono/sample/wasm/browser-eventpipe/index.html create mode 100644 src/mono/sample/wasm/browser-eventpipe/main.js diff --git a/src/mono/browser/runtime/diagnostics/client-commands.ts b/src/mono/browser/runtime/diagnostics/client-commands.ts new file mode 100644 index 00000000000000..f72cd7926ce419 --- /dev/null +++ b/src/mono/browser/runtime/diagnostics/client-commands.ts @@ -0,0 +1,425 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import type { DiagnosticCommandProviderV2 } from "../types"; + +import { SessionId } from "./common"; + +export const advert1 = [65, 68, 86, 82, 95]; +// TODO make GUID and process id dynamic +export const advert1Full = [65, 68, 86, 82, 95, 86, 49, 0, 66, 108, 106, 181, 91, 0, 142, 79, 182, 145, 225, 120, 77, 12, 131, 229, 42, 0, 0, 0, 0, 0, 0, 0, 0, 0]; +export const dotnet_IPC_V1 = [68, 79, 84, 78, 69, 84, 95, 73, 80, 67, 95, 86, 49, 0]; + + +// this file contains the IPC commands that are sent by client (like dotnet-trace) to the diagnostic server (like Mono VM in the browser) +// just formatting bytes, no sessions management here + +export function commandStopTracing (sessionID:SessionId) { + return Uint8Array.from([ + ...serializeHeader(CommandSetId.EventPipe, EventPipeCommandId.StopTracing, computeMessageByteLength(8)), + ...serializeUint64(sessionID), + ]); +} + +export function commandResumeRuntime () { + return Uint8Array.from([ + ...serializeHeader(CommandSetId.Process, ProcessCommandId.ResumeRuntime, computeMessageByteLength(0)), + ]); +} + +export function commandProcessInfo3 () { + return Uint8Array.from([ + ...serializeHeader(CommandSetId.Process, ProcessCommandId.ProcessInfo3, computeMessageByteLength(0)), + ]); +} + + +export function commandGcHeapDump (extraProviders:DiagnosticCommandProviderV2[]) { + return commandCollectTracing2({ + circularBufferMB: 256, + format: 1, + requestRundown: true, + providers: [ + { + keywords: [ + 0x0000_0000, + Keywords.GCHeapSnapshot, // 0x1980001 + // GC_HEAP_DUMP_VTABLE_CLASS_REF_KEYWORD 0x8000000 + // GC_FINALIZATION_KEYWORD 0x1000000 + // GC_HEAP_COLLECT_KEYWORD 0x0800000 + // GC_KEYWORD 0x0000001 + ], + logLevel: 5, + provider_name: "Microsoft-Windows-DotNETRuntime", + arguments: null + }, + ...extraProviders, + ] + }); +} + +export function commandCounters (intervalSec:number, extraProviders:DiagnosticCommandProviderV2[]) { + return commandCollectTracing2({ + circularBufferMB: 256, + format: 1, + requestRundown: false, + providers: [ + { + keywords: [0, Keywords.GCHandle], + logLevel: 4, + provider_name: "System.Diagnostics.Metrics", + arguments: `SessionId=SHARED;Metrics=System.Runtime;RefreshInterval=${intervalSec};MaxTimeSeries=1000;MaxHistograms=10;ClientId=c98f989b-369c-41af-bc8e-7ab261fba16c` + }, + ...extraProviders, + ] + }); +} + +export function commandSampleProfiler (extraProviders:DiagnosticCommandProviderV2[]) { + return commandCollectTracing2({ + circularBufferMB: 256, + format: 1, + requestRundown: true, + providers: [ + { + keywords: [ + 0x0000_0000, + 0x0000_0000, + ], + logLevel: 4, + provider_name: "Microsoft-DotNETCore-SampleProfiler", + arguments: null + }, + ...extraProviders, + ] + }); +} + +function commandCollectTracing2 (payload2:PayloadV2) { + const payloadLength = computeCollectTracing2PayloadByteLength(payload2); + const messageLength = computeMessageByteLength(payloadLength); + const message = [ + ...serializeHeader(CommandSetId.EventPipe, EventPipeCommandId.CollectTracing2, messageLength), + ...serializeUint32(payload2.circularBufferMB), + ...serializeUint32(payload2.format), + ...serializeUint8(payload2.requestRundown ? 1 : 0), + ...serializeUint32(payload2.providers.length), + ]; + for (const provider of payload2.providers) { + message.push(...serializeUint64(provider.keywords)); + message.push(...serializeUint32(provider.logLevel)); + message.push(...serializeString(provider.provider_name)); + message.push(...serializeString(provider.arguments)); + } + return Uint8Array.from(message); +} + +const enum Keywords { + None = 0, + All = 0xFFFF_FFFF, + // + // Summary: + // Logging when garbage collections and finalization happen. + GC = 1, + // + // Summary: + // Events when GC handles are set or destroyed. + GCHandle = 2, + Binder = 4, + // + // Summary: + // Logging when modules actually get loaded and unloaded. + Loader = 8, + // + // Summary: + // Logging when Just in time (JIT) compilation occurs. + Jit = 0x10, + // + // Summary: + // Logging when precompiled native (NGEN) images are loaded. + NGen = 0x20, + // + // Summary: + // Indicates that on attach or module load , a rundown of all existing methods should + // be done + StartEnumeration = 0x40, + // + // Summary: + // Indicates that on detach or process shutdown, a rundown of all existing methods + // should be done + StopEnumeration = 0x80, + // + // Summary: + // Events associated with validating security restrictions. + Security = 0x400, + // + // Summary: + // Events for logging resource consumption on an app-domain level granularity + AppDomainResourceManagement = 0x800, + // + // Summary: + // Logging of the internal workings of the Just In Time compiler. This is fairly + // verbose. It details decisions about interesting optimization (like inlining and + // tail call) + JitTracing = 0x1000, + // + // Summary: + // Log information about code thunks that transition between managed and unmanaged + // code. + Interop = 0x2000, + // + // Summary: + // Log when lock contention occurs. (Monitor.Enters actually blocks) + Contention = 0x4000, + // + // Summary: + // Log exception processing. + Exception = 0x8000, + // + // Summary: + // Log events associated with the threadpoo, and other threading events. + Threading = 0x10000, + // + // Summary: + // Dump the native to IL mapping of any method that is JIT compiled. (V4.5 runtimes + // and above). + JittedMethodILToNativeMap = 0x20000, + // + // Summary: + // If enabled will suppress the rundown of NGEN events on V4.0 runtime (has no effect + // on Pre-V4.0 runtimes). + OverrideAndSuppressNGenEvents = 0x40000, + // + // Summary: + // Enables the 'BulkType' event + Type = 0x80000, + // + // Summary: + // Enables the events associated with dumping the GC heap + GCHeapDump = 0x100000, + // + // Summary: + // Enables allocation sampling with the 'fast'. Sample to limit to 100 allocations + // per second per type. This is good for most detailed performance investigations. + // Note that this DOES update the allocation path to be slower and only works if + // the process start with this on. + GCSampledObjectAllocationHigh = 0x200000, + // + // Summary: + // Enables events associate with object movement or survival with each GC. + GCHeapSurvivalAndMovement = 0x400000, + // + // Summary: + // Triggers a GC. Can pass a 64 bit value that will be logged with the GC Start + // event so you know which GC you actually triggered. + GCHeapCollect = 0x800000, + // + // Summary: + // Indicates that you want type names looked up and put into the events (not just + // meta-data tokens). + GCHeapAndTypeNames = 0x1000000, + // + // Summary: + // Enables allocation sampling with the 'slow' rate, Sample to limit to 5 allocations + // per second per type. This is reasonable for monitoring. Note that this DOES update + // the allocation path to be slower and only works if the process start with this + // on. + GCSampledObjectAllocationLow = 0x2000000, + // + // Summary: + // Turns on capturing the stack and type of object allocation made by the .NET Runtime. + // This is only supported after V4.5.3 (Late 2014) This can be very verbose and + // you should seriously using GCSampledObjectAllocationHigh instead (and GCSampledObjectAllocationLow + // for production scenarios). + GCAllObjectAllocation = 0x2200000, + // + // Summary: + // This suppresses NGEN events on V4.0 (where you have NGEN PDBs), but not on V2.0 + // (which does not know about this bit and also does not have NGEN PDBS). + SupressNGen = 0x40000, + // + // Summary: + // TODO document + PerfTrack = 0x20000000, + // + // Summary: + // Also log the stack trace of events for which this is valuable. + Stack = 0x40000000, + // + // Summary: + // This allows tracing work item transfer events (thread pool enqueue/dequeue/ioenqueue/iodequeue/a.o.) + ThreadTransfer = 0x80000000, + // + // Summary: + // .NET Debugger events + Debugger = 0x100000000, + // + // Summary: + // Events intended for monitoring on an ongoing basis. + Monitoring = 0x200000000, + // + // Summary: + // Events that will dump PDBs of dynamically generated assemblies to the ETW stream. + Codesymbols = 0x400000000, + // + // Summary: + // Events that provide information about compilation. + Compilation = 0x1000000000, + // + // Summary: + // Diagnostic events for diagnosing compilation and pre-compilation features. + CompilationDiagnostic = 0x2000000000, + // + // Summary: + // Diagnostic events for capturing token information for events that express MethodID + MethodDiagnostic = 0x4000000000, + // + // Summary: + // Diagnostic events for diagnosing issues involving the type loader. + TypeDiagnostic = 0x8000000000, + // + // Summary: + // Events for wait handle waits. + WaitHandle = 0x40000000000, + // + // Summary: + // Recommend default flags (good compromise on verbosity). + Default = 0x14C14FCCBD, + // + // Summary: + // What is needed to get symbols for JIT compiled code. + JITSymbols = 0x60098, + // + // Summary: + // This provides the flags commonly needed to take a heap .NET Heap snapshot with + // ETW. + GCHeapSnapshot = 0x1980001 +} + +export const enum CommandSetId { + Reserved = 0, + Dump = 1, + EventPipe = 2, + Profiler = 3, + Process = 4, + + // replies + Server = 0xFF, +} + +const enum EventPipeCommandId { + StopTracing = 1, + CollectTracing = 2, + CollectTracing2 = 3, + CollectTracing3 = 4, + CollectTracing4 = 5, +} + +const enum ProcessCommandId { + ProcessInfo = 0, + ResumeRuntime = 1, + ProcessEnvironment = 2, + SetEnvVar = 3, + ProcessInfo2 = 4, + EnablePerfmap = 5, + DisablePerfmap = 6, + ApplyStartupHook = 7, + ProcessInfo3 = 8, +} + +export const enum ServerCommandId { + OK = 0, + Error = 0xFF, +} + +function serializeMagic () { + return Uint8Array.from(dotnet_IPC_V1); +} + +function serializeUint8 (value:number) { + return Uint8Array.from([value]); +} + +function serializeUint16 (value:number) { + return new Uint8Array(Uint16Array.from([value]).buffer); +} + +function serializeUint32 (value:number) { + return new Uint8Array(Uint32Array.from([value]).buffer); +} + +function serializeUint64 (value:[number, number]) { + // value == [hi, lo] + return new Uint8Array(Uint32Array.from([value[1], value[0]]).buffer); +} + +function serializeString (value:string|null) { + const message = []; + if (value === null || value === undefined || value === "") { + message.push(...serializeUint32(1)); + message.push(...serializeUint16(0)); + } else { + const len = value.length; + const hasNul = value[len - 1] === "\0"; + message.push(...serializeUint32(len + (hasNul ? 0 : 1))); + for (let i = 0; i < len; i++) { + message.push(...serializeUint16(value.charCodeAt(i))); + } + if (!hasNul) { + message.push(...serializeUint16(0)); + } + } + return message; +} + +function computeStringByteLength (s:string|null) { + if (s === undefined || s === null || s === "") + return 4 + 2; // just length of empty zero terminated string + return 4 + 2 * s.length + 2; // length + UTF16 + null +} + +function computeMessageByteLength (payloadLength:number) { + const fullHeaderSize = 14 + 2 // magic, len + + 1 + 1 // commandSet, command + + 2; // reserved ; + return fullHeaderSize + payloadLength; +} + +function serializeHeader (commandSet:CommandSetId, command:ServerCommandId|EventPipeCommandId|ProcessCommandId, len:number) { + return Uint8Array.from([ + ...serializeMagic(), + ...serializeUint16(len), + ...serializeUint8(commandSet), + ...serializeUint8(command), + ...serializeUint16(0), // reserved*/ + ]); +} + +function computeCollectTracing2PayloadByteLength (payload2:PayloadV2) { + let len = 0; + len += 4; // circularBufferMB + len += 4; // format + len += 1; // requestRundown + len += 4; // providers length + for (const provider of payload2.providers) { + len += 8; // keywords + len += 4; // level + len += computeStringByteLength(provider.provider_name); + len += computeStringByteLength(provider.arguments); + } + return len; +} + +type ProviderV2 ={ + keywords: [ number, Keywords ], + logLevel: number, + provider_name: string, + arguments: string|null +} + +type PayloadV2 = { + circularBufferMB: number, + format: number, + requestRundown: boolean, + providers: ProviderV2[] +} diff --git a/src/mono/browser/runtime/diagnostics/common.ts b/src/mono/browser/runtime/diagnostics/common.ts new file mode 100644 index 00000000000000..1e14d3774b339a --- /dev/null +++ b/src/mono/browser/runtime/diagnostics/common.ts @@ -0,0 +1,103 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import type { VoidPtr } from "../types/emscripten"; +import type { PromiseController } from "../types/internal"; + +import { runtimeHelpers } from "./globals"; +import { loaderHelpers, Module } from "./globals"; +import { mono_log_info } from "./logging"; + +let lastScheduledTimeoutId: any = undefined; + +// run another cycle of the event loop, which is EP threads on MT runtime +export function diagnostic_server_loop () { + lastScheduledTimeoutId = undefined; + if (loaderHelpers.is_runtime_running()) { + try { + runtimeHelpers.mono_background_exec();// give GC chance to run + runtimeHelpers.mono_wasm_ds_exec(); + schedule_diagnostic_server_loop(100); + } catch (ex) { + loaderHelpers.mono_exit(1, ex); + } + } +} + +export function schedule_diagnostic_server_loop (delay = 0):void { + if (!lastScheduledTimeoutId || delay === 0) { + lastScheduledTimeoutId = Module.safeSetTimeout(diagnostic_server_loop, delay); + } +} + +export class DiagConnectionBase { + protected messagesToSend: Uint8Array[] = []; + protected messagesReceived: Uint8Array[] = []; + constructor (public client_socket:number) { + } + + store (message:Uint8Array):number { + this.messagesToSend.push(message); + return message.byteLength; + } + + poll ():number { + return this.messagesReceived.length; + } + + recv (buffer:VoidPtr, bytes_to_read:number):number { + if (this.messagesReceived.length === 0) { + return 0; + } + const message = this.messagesReceived[0]!; + const bytes_read = Math.min(message.length, bytes_to_read); + Module.HEAPU8.set(message.subarray(0, bytes_read), buffer as any); + if (bytes_read === message.length) { + this.messagesReceived.shift(); + } else { + this.messagesReceived[0] = message.subarray(bytes_read); + } + return bytes_read; + } +} + +export interface IDiagConnection { + send (message: Uint8Array):number ; + poll ():number ; + recv (buffer:VoidPtr, bytes_to_read:number):number ; + close ():number ; +} + +// [hi,lo] +export type SessionId=[number, number]; + +export interface IDiagSession { + session_id:SessionId; + store(message: Uint8Array): number; + sendCommand(message:Uint8Array):void; +} + +export interface IDiagClient { + skipDownload?:boolean; + onClosePromise:PromiseController; + commandOnAdvertise():Uint8Array; + onSessionStart?(session:IDiagSession):void; + onData?(session:IDiagSession, message:Uint8Array):void; + onClose?(messages:Uint8Array[]):void; + onError?(session:IDiagSession, message:Uint8Array):void; +} + +export type fnClientProvider = (scenarioName:string) => IDiagClient; + +export function downloadBlob (messages:Uint8Array[]) { + const blob = new Blob(messages, { type: "application/octet-stream" }); + const blobUrl = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.download = "trace." + (new Date()).valueOf() + ".nettrace"; + mono_log_info(`Downloading trace ${link.download} - ${blob.size} bytes`); + link.href = blobUrl; + document.body.appendChild(link); + link.dispatchEvent(new MouseEvent("click", { + bubbles: true, cancelable: true, view: window + })); +} diff --git a/src/mono/browser/runtime/diagnostics/diag-js.ts b/src/mono/browser/runtime/diagnostics/diag-js.ts new file mode 100644 index 00000000000000..0987191a3b9aa2 --- /dev/null +++ b/src/mono/browser/runtime/diagnostics/diag-js.ts @@ -0,0 +1,148 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import { advert1, CommandSetId, dotnet_IPC_V1, ServerCommandId } from "./client-commands"; +import { DiagConnectionBase, downloadBlob, fnClientProvider, IDiagClient, IDiagConnection, IDiagSession, schedule_diagnostic_server_loop, SessionId } from "./common"; +import { PromiseAndController } from "../types/internal"; +import { loaderHelpers } from "./globals"; +import { mono_log_warn } from "./logging"; +import { collectCpuSamples } from "./dotnet-cpu-profiler"; +import { collectPerfCounters } from "./dotnet-counters"; +import { collectGcDump } from "./dotnet-gcdump"; + +//let diagClient:IDiagClient|undefined = undefined as any; +//let server:DiagServer = undefined as any; + +// configure your application +// .withEnvironmentVariable("DOTNET_DiagnosticPorts", "download:gcdump") +// or implement function globalThis.dotnetDiagnosticClient with IDiagClient interface + +let nextJsClient:PromiseAndController; +let fromScenarioNameOnce = false; + +// Only the last which sent advert is receiving commands for all sessions +export let serverSession:DiagSession|undefined = undefined; + +// singleton wrapping the protocol with the diagnostic server in the Mono VM +// there could be multiple connection at the same time. +// DS:advert ->1 +// 1<- DC1: command to start tracing session +// DS:OK, session ID ->1 +// DS:advert ->2 +// DS:events ->1 +// DS:events ->1 +// DS:events ->1 +// DS:events ->1 +// 2<- DC1: command to stop tracing session +// DS:close ->1 + +class DiagSession extends DiagConnectionBase implements IDiagConnection, IDiagSession { + public session_id: SessionId = undefined as any; + public diagClient?: IDiagClient; + public stopDelayedAfterLastMessage:number|undefined = undefined; + public resumedRuntime = false; + + constructor (public client_socket:number) { + super(client_socket); + } + + sendCommand (message: Uint8Array): void { + if (!serverSession) { + mono_log_warn("no server yet"); + return; + } + serverSession.respond(message); + } + + async connect_new_client () { + this.diagClient = await nextJsClient.promise; + cleanup_client(); + const firstCommand = this.diagClient.commandOnAdvertise(); + this.respond(firstCommand); + } + + // this is message from the diagnostic server, which is Mono VM in this browser + send (message:Uint8Array):number { + schedule_diagnostic_server_loop(); + if (advert1.every((v, i) => v === message[i])) { + // eslint-disable-next-line @typescript-eslint/no-this-alias + serverSession = this; + this.connect_new_client(); + } else if (dotnet_IPC_V1.every((v, i) => v === message[i]) && message[16] == CommandSetId.Server) { + if (message[17] == ServerCommandId.OK) { + if (message.byteLength === 28) { + const view = message.subarray(20, 28); + const sessionIDLo = view[0] | (view[1] << 8) | (view[2] << 16) | (view[3] << 24); + const sessionIDHi = view[4] | (view[5] << 8) | (view[6] << 16) | (view[7] << 24); + const sessionId = [sessionIDHi, sessionIDLo] as SessionId; + this.session_id = sessionId; + if (this.diagClient?.onSessionStart) { + this.diagClient.onSessionStart(this); + } + } + } else { + if (this.diagClient?.onError) { + this.diagClient.onError(this, message); + } else { + mono_log_warn("Diagnostic session " + this.session_id + " error : " + message.toString()); + } + } + } else { + if (this.diagClient?.onData) + this.diagClient.onData(this, message); + else { + this.store(message); + } + } + + return message.length; + } + + // this is message to the diagnostic server, which is Mono VM in this browser + respond (message:Uint8Array) : void { + this.messagesReceived.push(message); + schedule_diagnostic_server_loop(); + } + + close (): number { + if (this.diagClient?.onClose) { + this.diagClient.onClose(this.messagesToSend); + } + if (this.messagesToSend.length === 0) { + return 0; + } + if (this.diagClient && !this.diagClient.skipDownload) { + downloadBlob(this.messagesToSend); + } + this.messagesToSend = []; + return 0; + } +} + +export function cleanup_client () { + nextJsClient = loaderHelpers.createPromiseController(); +} + +export function setup_js_client (client:IDiagClient) { + nextJsClient.promise_control.resolve(client); +} + +export function createDiagConnectionJs (socket_handle:number, scenarioName:string):DiagSession { + if (!fromScenarioNameOnce) { + fromScenarioNameOnce = true; + if (scenarioName.startsWith("js://gcdump")) { + collectGcDump({}); + } + if (scenarioName.startsWith("js://counters")) { + collectPerfCounters({}); + } + if (scenarioName.startsWith("js://cpu-samples")) { + collectCpuSamples({}); + } + const dotnetDiagnosticClient:fnClientProvider = (globalThis as any).dotnetDiagnosticClient; + if (typeof dotnetDiagnosticClient === "function" ) { + nextJsClient.promise_control.resolve(dotnetDiagnosticClient(scenarioName)); + } + } + return new DiagSession(socket_handle); +} diff --git a/src/mono/browser/runtime/diagnostics/diag-ws.ts b/src/mono/browser/runtime/diagnostics/diag-ws.ts new file mode 100644 index 00000000000000..6191803259219d --- /dev/null +++ b/src/mono/browser/runtime/diagnostics/diag-ws.ts @@ -0,0 +1,61 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import { IDiagConnection, DiagConnectionBase, diagnostic_server_loop, schedule_diagnostic_server_loop } from "./common"; + +export function createDiagConnectionWs (socket_handle:number, url:string):IDiagConnection { + return new DiagConnectionWS(socket_handle, url); +} + +// this is used together with `dotnet-dsrouter` which will create IPC pipe on your local machine +// 1. run `dotnet-dsrouter server-websocket` this will print process ID and websocket URL +// 2. configure your wasm dotnet application `.withEnvironmentVariable("DOTNET_DiagnosticPorts", "ws://127.0.0.1:8088/diagnostics")` +// 3. run your wasm application +// 4. run `dotnet-gcdump -p ` or `dotnet-trace collect -p ` +class DiagConnectionWS extends DiagConnectionBase implements IDiagConnection { + private ws: WebSocket; + + constructor (client_socket:number, url:string) { + super(client_socket); + const ws = this.ws = new WebSocket(url); + const onMessage = async (evt:MessageEvent) => { + const buffer = await evt.data.arrayBuffer(); + const message = new Uint8Array(buffer); + this.messagesReceived.push(message); + diagnostic_server_loop(); + }; + ws.addEventListener("open", () => { + for (const data of this.messagesToSend) { + ws.send(data); + } + this.messagesToSend = []; + diagnostic_server_loop(); + }, { once: true }); + ws.addEventListener("message", onMessage); + ws.addEventListener("error", () => { + ws.removeEventListener("message", onMessage); + }, { once: true }); + } + + send (message:Uint8Array):number { + schedule_diagnostic_server_loop(); + // copy the message + if (this.ws!.readyState == WebSocket.CLOSED) { + return -1; + } + if (this.ws!.readyState == WebSocket.CONNECTING) { + return super.store(message); + } + + this.ws!.send(message); + + return message.length; + } + + close ():number { + schedule_diagnostic_server_loop(); + this.ws.close(); + return 0; + } +} + diff --git a/src/mono/browser/runtime/diagnostics/dotnet-counters.ts b/src/mono/browser/runtime/diagnostics/dotnet-counters.ts new file mode 100644 index 00000000000000..cebdb115d4e151 --- /dev/null +++ b/src/mono/browser/runtime/diagnostics/dotnet-counters.ts @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import type { DiagnosticCommandOptions } from "../types"; + +import { commandStopTracing, commandCounters } from "./client-commands"; +import { IDiagSession } from "./common"; +import { Module } from "./globals"; +import { serverSession, setup_js_client } from "./diag-js"; +import { loaderHelpers } from "./globals"; + +export function collectPerfCounters (options?:DiagnosticCommandOptions):Promise { + if (!options) options = {}; + if (!serverSession) { + throw new Error("No active JS diagnostic session"); + } + + const onClosePromise = loaderHelpers.createPromiseController(); + function onSessionStart (session: IDiagSession): void { + // stop tracing after period of monitoring + Module.safeSetTimeout(() => { + session.sendCommand(commandStopTracing(session.session_id)); + }, 1000 * (options?.durationSeconds ?? 60)); + } + setup_js_client({ + onClosePromise:onClosePromise.promise_control, + skipDownload:options.skipDownload, + commandOnAdvertise:() => commandCounters(options.intervalSeconds || 1, options.extraProviders || []), + onSessionStart, + }); + return onClosePromise.promise; +} diff --git a/src/mono/browser/runtime/diagnostics/dotnet-cpu-profiler.ts b/src/mono/browser/runtime/diagnostics/dotnet-cpu-profiler.ts new file mode 100644 index 00000000000000..58e67c86dbaeb8 --- /dev/null +++ b/src/mono/browser/runtime/diagnostics/dotnet-cpu-profiler.ts @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import type { DiagnosticCommandOptions } from "../types"; + +import { commandStopTracing, commandSampleProfiler } from "./client-commands"; +import { loaderHelpers, Module, runtimeHelpers } from "./globals"; +import { serverSession, setup_js_client } from "./diag-js"; +import { IDiagSession } from "./common"; + +export function collectCpuSamples (options?:DiagnosticCommandOptions):Promise { + if (!options) options = {}; + if (!serverSession) { + throw new Error("No active JS diagnostic session"); + } + if (!runtimeHelpers.config.environmentVariables!["DOTNET_WasmPerfInstrumentation"]) { + throw new Error("method instrumentation is not enabled, please enable it with WasmPerfInstrumentation MSBuild property"); + } + + const onClosePromise = loaderHelpers.createPromiseController(); + function onSessionStart (session: IDiagSession): void { + // stop tracing after period of monitoring + Module.safeSetTimeout(() => { + session.sendCommand(commandStopTracing(session.session_id)); + }, 1000 * (options?.durationSeconds ?? 60)); + } + + setup_js_client({ + onClosePromise:onClosePromise.promise_control, + skipDownload:options.skipDownload, + commandOnAdvertise: () => commandSampleProfiler(options.extraProviders || []), + onSessionStart, + }); + return onClosePromise.promise; +} diff --git a/src/mono/browser/runtime/diagnostics/dotnet-gcdump.ts b/src/mono/browser/runtime/diagnostics/dotnet-gcdump.ts new file mode 100644 index 00000000000000..0486025cd1ba1f --- /dev/null +++ b/src/mono/browser/runtime/diagnostics/dotnet-gcdump.ts @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import type { DiagnosticCommandOptions } from "../types"; + +import { commandStopTracing, commandGcHeapDump, } from "./client-commands"; +import { IDiagSession } from "./common"; +import { loaderHelpers, Module } from "./globals"; +import { serverSession, setup_js_client } from "./diag-js"; + +export function collectGcDump (options?:DiagnosticCommandOptions):Promise { + if (!options) options = {}; + if (!serverSession) { + throw new Error("No active JS diagnostic session"); + } + + const onClosePromise = loaderHelpers.createPromiseController(); + let stopDelayedAfterLastMessage = 0; + let stopSent = false; + function onData (session: IDiagSession, message: Uint8Array): void { + session.store(message); + if (!stopSent) { + // stop 500ms after last GC message on this session, there will be more messages after that + if (stopDelayedAfterLastMessage) { + clearTimeout(stopDelayedAfterLastMessage); + } + stopDelayedAfterLastMessage = Module.safeSetTimeout(() => { + stopSent = true; + session.sendCommand(commandStopTracing(session.session_id)); + }, 1000 * (options?.durationSeconds ?? 1)); + } + } + + setup_js_client({ + onClosePromise: onClosePromise.promise_control, + skipDownload: options.skipDownload, + commandOnAdvertise: () => commandGcHeapDump(options.extraProviders || []), + onData, + }); + return onClosePromise.promise; +} diff --git a/src/mono/browser/runtime/diagnostics/index.ts b/src/mono/browser/runtime/diagnostics/index.ts index fc554e31287b5d..c47f92570a7798 100644 --- a/src/mono/browser/runtime/diagnostics/index.ts +++ b/src/mono/browser/runtime/diagnostics/index.ts @@ -4,34 +4,90 @@ import type { GlobalObjects } from "../types/internal"; import type { CharPtr, VoidPtr } from "../types/emscripten"; +import { Module, runtimeHelpers } from "./globals"; +import { cleanup_client as cleanup_js_client, createDiagConnectionJs, serverSession } from "./diag-js"; +import { IDiagConnection } from "./common"; +import { createDiagConnectionWs } from "./diag-ws"; import { diagnosticHelpers, setRuntimeGlobalsImpl } from "./globals"; +import { collectCpuSamples } from "./dotnet-cpu-profiler"; +import { collectPerfCounters } from "./dotnet-counters"; +import { collectGcDump } from "./dotnet-gcdump"; +import { advert1Full } from "./client-commands"; + +let socket_handles:Map = undefined as any; +let next_socket_handle = 1; +let url_override:string | undefined = undefined; -/* eslint-disable @typescript-eslint/no-unused-vars */ export function setRuntimeGlobals (globalObjects: GlobalObjects): void { setRuntimeGlobalsImpl(globalObjects); diagnosticHelpers.ds_rt_websocket_create = (urlPtr :CharPtr):number => { - // Not implemented yet - return 1; + if (!socket_handles) { + socket_handles = new Map(); + } + const url = url_override ?? runtimeHelpers.utf8ToString(urlPtr); + const socket_handle = next_socket_handle++; + const isWebSocket = url.startsWith("ws://") || url.startsWith("wss://"); + const wrapper = isWebSocket + ? createDiagConnectionWs(socket_handle, url) + : createDiagConnectionJs(socket_handle, url); + socket_handles.set(socket_handle, wrapper); + return socket_handle; }; diagnosticHelpers.ds_rt_websocket_send = (client_socket :number, buffer:VoidPtr, bytes_to_write:number):number => { - // Not implemented yet - return bytes_to_write; + const wrapper = socket_handles.get(client_socket); + if (!wrapper) { + return -1; + } + const message = (new Uint8Array(Module.HEAPU8.buffer, buffer as any, bytes_to_write)).slice(); + return wrapper.send(message); }; diagnosticHelpers.ds_rt_websocket_poll = (client_socket :number):number => { - // Not implemented yet - return 0; + const wrapper = socket_handles.get(client_socket); + if (!wrapper) { + return 0; + } + return wrapper.poll(); }; diagnosticHelpers.ds_rt_websocket_recv = (client_socket :number, buffer:VoidPtr, bytes_to_read:number):number => { - // Not implemented yet - return 0; + const wrapper = socket_handles.get(client_socket); + if (!wrapper) { + return -1; + } + return wrapper.recv(buffer, bytes_to_read); }; diagnosticHelpers.ds_rt_websocket_close = (client_socket :number):number => { - // Not implemented yet - return 0; + const wrapper = socket_handles.get(client_socket); + if (!wrapper) { + return -1; + } + socket_handles.delete(client_socket); + return wrapper.close(); }; + + globalObjects.api.collectCpuSamples = collectCpuSamples; + globalObjects.api.collectPerfCounters = collectPerfCounters; + globalObjects.api.collectGcDump = collectGcDump; + globalObjects.api.connectDSRouter = connectDSRouter; + + cleanup_js_client(); +} + +// this will take over the existing connection to JS and send new advert message to WS client +// use dotnet-dsrouter server-websocket -v trace +function connectDSRouter (url: string): void { + if (!serverSession) { + throw new Error("No active session to reconnect"); + } + + // make sure new sessions hit the new URL + url_override = url; + + const wrapper = createDiagConnectionWs(serverSession.client_socket, url); + socket_handles.set(serverSession.client_socket, wrapper); + wrapper.send(new Uint8Array(advert1Full)); } diff --git a/src/mono/browser/runtime/dotnet.d.ts b/src/mono/browser/runtime/dotnet.d.ts index 0cd2872d719113..4da9600e037e19 100644 --- a/src/mono/browser/runtime/dotnet.d.ts +++ b/src/mono/browser/runtime/dotnet.d.ts @@ -439,7 +439,7 @@ type DotnetModuleConfig = { imports?: any; exports?: string[]; } & Partial; -type APIType = { +type RunAPIType = { /** * Runs the Main() method of the application. * Note: this will keep the .NET runtime alive and the APIs will be available for further calls. @@ -489,6 +489,8 @@ type APIType = { * You can register the scripts using MonoConfig.resources.modulesAfterConfigLoaded and MonoConfig.resources.modulesAfterRuntimeReady. */ invokeLibraryInitializers: (functionName: string, args: any[]) => Promise; +}; +type MemoryAPIType = { /** * Writes to the WASM linear memory */ @@ -630,6 +632,42 @@ type APIType = { */ localHeapViewF64: () => Float64Array; }; +type DiagnosticsAPIType = { + /** + * creates diagnostic trace file. Default is 60 seconds. + * It could be opened in PerfView or Visual Studio as is. + */ + collectCpuSamples: (options?: DiagnosticCommandOptions) => Promise; + /** + * creates diagnostic trace file. Default is 60 seconds. + * It could be opened in PerfView or Visual Studio as is. + * It could be summarized by `dotnet-trace report xxx.nettrace topN -n 10` + */ + collectPerfCounters: (options?: DiagnosticCommandOptions) => Promise; + /** + * creates diagnostic trace file. + * It could be opened in PerfView as is. + * It could be converted for Visual Studio using `dotnet-gcdump convert`. + */ + collectGcDump: (options?: DiagnosticCommandOptions) => Promise; + /** + * changes DOTNET_DiagnosticPorts and makes a new connection to WebSocket on that URL. + */ + connectDSRouter(url: string): void; +}; +type DiagnosticCommandProviderV2 = { + keywords: [number, number]; + logLevel: number; + provider_name: string; + arguments: string | null; +}; +type DiagnosticCommandOptions = { + durationSeconds?: number; + intervalSeconds?: number; + skipDownload?: boolean; + extraProviders?: DiagnosticCommandProviderV2[]; +}; +type APIType = RunAPIType & MemoryAPIType & DiagnosticsAPIType; type RuntimeAPI = { INTERNAL: any; Module: EmscriptenModule; diff --git a/src/mono/browser/runtime/driver.c b/src/mono/browser/runtime/driver.c index 6d62fd2a66d396..5a49b7753aa583 100644 --- a/src/mono/browser/runtime/driver.c +++ b/src/mono/browser/runtime/driver.c @@ -42,6 +42,7 @@ void bindings_initialize_internals (); char *monoeg_g_getenv(const char *variable); int monoeg_g_setenv(const char *variable, const char *value, int overwrite); char *mono_method_get_full_name (MonoMethod *method); +char *mono_method_full_name (MonoMethod *method, int32_t signature); #ifndef INVARIANT_TIMEZONE extern void mono_register_timezones_bundle (void); diff --git a/src/mono/browser/runtime/export-api.ts b/src/mono/browser/runtime/export-api.ts index 3c8e6312c59062..83ddc05bca8d0c 100644 --- a/src/mono/browser/runtime/export-api.ts +++ b/src/mono/browser/runtime/export-api.ts @@ -57,6 +57,10 @@ export function export_api (): any { localHeapViewI64Big: localHeapViewI64Big, localHeapViewF32: localHeapViewF32, localHeapViewF64: localHeapViewF64, + collectCpuSamples:null as any, + collectPerfCounters:null as any, + collectGcDump:null as any, + connectDSRouter:null as any, }; return api; } diff --git a/src/mono/browser/runtime/startup.ts b/src/mono/browser/runtime/startup.ts index 3af2484b8893d0..be92590cefa96e 100644 --- a/src/mono/browser/runtime/startup.ts +++ b/src/mono/browser/runtime/startup.ts @@ -540,6 +540,7 @@ export async function start_runtime () { if (runtimeHelpers.emscriptenBuildOptions.enablePerfTracing) { const diagnosticPorts = "DOTNET_DiagnosticPorts"; + // connect JS client by default const jsReady = "js://ready"; if (!environmentVariables[diagnosticPorts]) { environmentVariables[diagnosticPorts] = jsReady; diff --git a/src/mono/browser/runtime/types/index.ts b/src/mono/browser/runtime/types/index.ts index e2653cc0d54580..0ba9c0e242ca7c 100644 --- a/src/mono/browser/runtime/types/index.ts +++ b/src/mono/browser/runtime/types/index.ts @@ -403,7 +403,7 @@ export type DotnetModuleConfig = { exports?: string[]; } & Partial -export type APIType = { +export type RunAPIType = { /** * Runs the Main() method of the application. * Note: this will keep the .NET runtime alive and the APIs will be available for further calls. @@ -453,6 +453,9 @@ export type APIType = { * You can register the scripts using MonoConfig.resources.modulesAfterConfigLoaded and MonoConfig.resources.modulesAfterRuntimeReady. */ invokeLibraryInitializers: (functionName: string, args: any[]) => Promise; +} + +export type MemoryAPIType = { /** * Writes to the WASM linear memory */ @@ -595,6 +598,46 @@ export type APIType = { localHeapViewF64: () => Float64Array; } +export type DiagnosticsAPIType = { + /** + * creates diagnostic trace file. Default is 60 seconds. + * It could be opened in PerfView or Visual Studio as is. + */ + collectCpuSamples: (options?:DiagnosticCommandOptions) => Promise; + /** + * creates diagnostic trace file. Default is 60 seconds. + * It could be opened in PerfView or Visual Studio as is. + * It could be summarized by `dotnet-trace report xxx.nettrace topN -n 10` + */ + collectPerfCounters: (options?:DiagnosticCommandOptions) => Promise; + /** + * creates diagnostic trace file. + * It could be opened in PerfView as is. + * It could be converted for Visual Studio using `dotnet-gcdump convert`. + */ + collectGcDump: (options?:DiagnosticCommandOptions) => Promise; + /** + * changes DOTNET_DiagnosticPorts and makes a new connection to WebSocket on that URL. + */ + connectDSRouter (url: string): void; +} + +export type DiagnosticCommandProviderV2 = { + keywords: [ number, number ], + logLevel: number, + provider_name: string, + arguments: string|null +} + +export type DiagnosticCommandOptions = { + durationSeconds?:number, + intervalSeconds?:number, + skipDownload?:boolean, + extraProviders?:DiagnosticCommandProviderV2[], +} + +export type APIType = RunAPIType & MemoryAPIType & DiagnosticsAPIType; + export type RuntimeAPI = { INTERNAL: any, Module: EmscriptenModule, diff --git a/src/mono/sample/wasm/browser-advanced/Wasm.Advanced.Sample.csproj b/src/mono/sample/wasm/browser-advanced/Wasm.Advanced.Sample.csproj index 26ec6c0711770c..c16d05f71676a5 100644 --- a/src/mono/sample/wasm/browser-advanced/Wasm.Advanced.Sample.csproj +++ b/src/mono/sample/wasm/browser-advanced/Wasm.Advanced.Sample.csproj @@ -19,10 +19,10 @@ ./ - diff --git a/src/mono/sample/wasm/browser-eventpipe/Program.cs b/src/mono/sample/wasm/browser-eventpipe/Program.cs new file mode 100644 index 00000000000000..4c26d735b6796f --- /dev/null +++ b/src/mono/sample/wasm/browser-eventpipe/Program.cs @@ -0,0 +1,168 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Reflection; +using System.Threading.Tasks; +using System.Runtime.InteropServices.JavaScript; +using System.Runtime.InteropServices; +using System.Diagnostics.Tracing; +using System.Runtime.CompilerServices; + +namespace Sample +{ + class ConsoleWriterEventListener : EventListener + { + public static ConsoleWriterEventListener Instance; + + protected override void OnEventSourceCreated(EventSource eventSource) + { + if(eventSource.Name == "WasmHello") + { + EnableEvents(eventSource, EventLevel.Informational); + } + if(eventSource.Name == "System.Runtime") + { + EnableEvents(eventSource, EventLevel.Informational); + } + } + + protected override void OnEventWritten(EventWrittenEventArgs eventData) + { + Console.WriteLine(eventData.TimeStamp + " " + eventData.EventName); + } + } + + public partial class Test + { + public static int Main(string[] args) + { + // ActivityTracker.Instance.Enable(); via reflection + var type = typeof(EventSource).Assembly.GetType("System.Diagnostics.Tracing.ActivityTracker"); + var prop = type.GetProperty("Instance", BindingFlags.Static | BindingFlags.Public); + var m = type.GetMethod("Enable"); + var instance = prop.GetValue(null); + m.Invoke(instance, null); + + DisplayMeaning(42); + + WasmHelloEventSource.Instance.NewCallsCounter(); + ConsoleWriterEventListener.Instance = new ConsoleWriterEventListener(); + + // SayHi(); + return 0; + } + + [JSImport("Sample.Test.displayMeaning", "main.js")] + internal static partial void DisplayMeaning(int meaning); + + public static int counter; + + [JSExport] + [MethodImpl(MethodImplOptions.NoInlining)] + internal static void SayHi() + { + Console.WriteLine("Hi from C#! " + EventSource.CurrentThreadActivityId); + WasmHelloEventSource.Instance.HelloStart(counter); + Console.WriteLine("Wave from C#! " + EventSource.CurrentThreadActivityId); + for(int i = 0; i < 100000; i++) + { + WasmHelloEventSource.Instance.CountCall(); + } + counter++; + SayHiCatch(); + WasmHelloEventSource.Instance.HelloStop(counter, "counter"+counter); + Console.WriteLine("Bye from C#! " + EventSource.CurrentThreadActivityId); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + internal static void SayHiThrow() + { + throw new Exception("Hello from C#!"); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + internal static void SayHiCatch() + { + try + { + SayHiThrow(); + } + catch (Exception e) + { + Console.WriteLine("Caught exception: " + e.Message); + } + } + + [JSExport] + internal static Task SayHiAsync() + { + WasmHelloEventSource.Instance.HelloStart(counter); + Console.WriteLine("Hi from C#!"); + for(int i = 0; i < 100000; i++) + { + WasmHelloEventSource.Instance.CountCall(); + } + counter++; + Console.WriteLine("Hello from C#!"); + WasmHelloEventSource.Instance.HelloStop(counter, "counter"+counter); + + return Task.CompletedTask; + } + } + + + + [EventSource(Name = "WasmHello")] + public class WasmHelloEventSource : EventSource + { + public static readonly WasmHelloEventSource Instance = new (); + + private IncrementingEventCounter _calls; + + private WasmHelloEventSource () + { + } + + [NonEvent] + public void NewCallsCounter() + { + _calls?.Dispose(); + _calls = new ("hello-calls", this) + { + DisplayName = "Hello calls", + }; + } + + [NonEvent] + public void CountCall() { + _calls?.Increment(1.0); + } + + protected override void Dispose (bool disposing) + { + _calls?.Dispose(); + _calls = null; + + base.Dispose(disposing); + } + + [Event(1, Message="Started Hello({0})", Level = EventLevel.Informational)] + public void HelloStart(int n) + { + if (!IsEnabled()) + return; + + WriteEvent(1, n); + } + + [Event(2, Message="Stopped Hello({0}) = {1}", Level = EventLevel.Informational)] + public void HelloStop(int n, string result) + { + if (!IsEnabled()) + return; + + WriteEvent(2, n, result); + } + } +} diff --git a/src/mono/sample/wasm/browser-eventpipe/Wasm.Browser.EventPipe.Sample.csproj b/src/mono/sample/wasm/browser-eventpipe/Wasm.Browser.EventPipe.Sample.csproj new file mode 100644 index 00000000000000..96e3a8058f6b8b --- /dev/null +++ b/src/mono/sample/wasm/browser-eventpipe/Wasm.Browser.EventPipe.Sample.csproj @@ -0,0 +1,18 @@ + + + + true + N:Sample + true + true + + + + + + + diff --git a/src/mono/sample/wasm/browser-eventpipe/index.html b/src/mono/sample/wasm/browser-eventpipe/index.html new file mode 100644 index 00000000000000..0c99eac49653aa --- /dev/null +++ b/src/mono/sample/wasm/browser-eventpipe/index.html @@ -0,0 +1,25 @@ + + + + + + + Sample EventPipe profile session + + + + + + + +
+
+
+ +
+
+
+ Answer to the Ultimate Question is : + + + diff --git a/src/mono/sample/wasm/browser-eventpipe/main.js b/src/mono/sample/wasm/browser-eventpipe/main.js new file mode 100644 index 00000000000000..e6737c285f1c70 --- /dev/null +++ b/src/mono/sample/wasm/browser-eventpipe/main.js @@ -0,0 +1,66 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import { dotnet, exit } from './_framework/dotnet.js' + +function displayMeaning(meaning) { + document.getElementById("out").innerHTML = `${meaning}`; +} + +try { + const { setModuleImports, runMain, getAssemblyExports, getConfig } = await dotnet + //.withEnvironmentVariable("MONO_DIAGNOSTICS", "--diagnostic-mono-profiler=enable")// --diagnostic-ports=mock:../mock.js,suspend + //.withEnvironmentVariable("DOTNET_DiagnosticPorts", "ws://127.0.0.1:8088/diagnostics,suspend") + // .withEnvironmentVariable("DOTNET_DiagnosticPorts", "ws://127.0.0.1:8088/diagnostics") + // dotnet-trace collect --providers Microsoft-Windows-DotNETRuntime:0x1980001:5 -p 41732 + // dotnet-gcdump collect -p 41732 + // dotnet-counters + //.withEnvironmentVariable("DOTNET_DiagnosticPorts", "download:gcdump") + //.withEnvironmentVariable("DOTNET_DiagnosticPorts", "download:counters") + // .withEnvironmentVariable("DOTNET_DiagnosticPorts", "download:samples") + //.withEnvironmentVariable("DOTNET_DiagnosticPorts", "js://cpu-samples") + //.withEnvironmentVariable("MONO_LOG_LEVEL", "debug") + //.withEnvironmentVariable("MONO_LOG_MASK", "all") + /*.withEnvironmentVariable("MONO_VERBOSE_METHOD", "System.Threading.Monitor:Exit") + .withRuntimeOptions([ + "--no-jiterpreter-traces-enabled", + "--no-jiterpreter-interp-entry-enabled", + "--no-jiterpreter-jit-call-enabled", + ])*/ + .withElementOnExit() + .withExitOnUnhandledError() + .create(); + + setModuleImports("main.js", { + Sample: { + Test: { + displayMeaning + } + } + }); + const config = getConfig(); + const exports = await getAssemblyExports(config.mainAssemblyName); + const sayHi = exports.Sample.Test.SayHi; + const sayHiAsync = exports.Sample.Test.SayHiAsync; + + document.querySelector("#hello-button").addEventListener("click", () => { + try { + sayHi(); + } catch (exc) { + alert(exc); + } + }); + + await runMain(); + + sayHi(); + //sayHiAsync(); + + /*setInterval(async () => { + sayHi(); + await sayHiAsync(); + }, 1000);*/ +} +catch (err) { + exit(2, err); +} diff --git a/src/mono/wasm/features.md b/src/mono/wasm/features.md index a628d5f4b2b981..8fcc7e7a9b19f2 100644 --- a/src/mono/wasm/features.md +++ b/src/mono/wasm/features.md @@ -403,6 +403,27 @@ See also log mask [categories](https://github.com/dotnet/runtime/blob/88633ae045 `Timing-Allow-Origin` HTTP header allows for more precise time measurements. +Then you can trigger collection of a trace from browser dev tools + +```js +globalThis.getDotnetRuntime(0).collectGcDump() +``` + +The .nettrace file could be coverted for VS via `dotnet-gcdump convert` or opened in `PerfView.exe` as is. + +```js +globalThis.getDotnetRuntime(0).collectPerfCounters({durationSeconds: 60}) +``` + +The counters could be opened in VS, `PerfView.exe` tools or via `dotnet-trace report xxx.nettrace topN -n 10` + +```js +globalThis.getDotnetRuntime(0).collectCpuSamples({durationSeconds: 60}) +``` + +The counters could be opened in VS or in `PerfView.exe` + + ### Profiling in the browser dev tools You can enable integration with the profiler in browser dev tools via following elements in your .csproj From 29c2d3138fbb4d5617973bf34c7fc28f114645e5 Mon Sep 17 00:00:00 2001 From: Pavel Savara Date: Fri, 4 Apr 2025 16:17:17 +0200 Subject: [PATCH 2/7] Update src/mono/wasm/features.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/mono/wasm/features.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/mono/wasm/features.md b/src/mono/wasm/features.md index 8fcc7e7a9b19f2..42de422d79fe26 100644 --- a/src/mono/wasm/features.md +++ b/src/mono/wasm/features.md @@ -409,8 +409,7 @@ Then you can trigger collection of a trace from browser dev tools globalThis.getDotnetRuntime(0).collectGcDump() ``` -The .nettrace file could be coverted for VS via `dotnet-gcdump convert` or opened in `PerfView.exe` as is. - +The .nettrace file could be converted for VS via `dotnet-gcdump convert` or opened in `PerfView.exe` as is. ```js globalThis.getDotnetRuntime(0).collectPerfCounters({durationSeconds: 60}) ``` From b98538f7c9bc78437b674f9da36993a2106db6ea Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Fri, 4 Apr 2025 16:18:25 +0200 Subject: [PATCH 3/7] revert advanced sample --- .../sample/wasm/browser-advanced/Wasm.Advanced.Sample.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mono/sample/wasm/browser-advanced/Wasm.Advanced.Sample.csproj b/src/mono/sample/wasm/browser-advanced/Wasm.Advanced.Sample.csproj index c16d05f71676a5..26ec6c0711770c 100644 --- a/src/mono/sample/wasm/browser-advanced/Wasm.Advanced.Sample.csproj +++ b/src/mono/sample/wasm/browser-advanced/Wasm.Advanced.Sample.csproj @@ -19,10 +19,10 @@ ./ + From e48a8d05d1d1079dec7325e2b19f704de70106c4 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Mon, 7 Apr 2025 09:50:01 +0200 Subject: [PATCH 4/7] more --- src/mono/browser/runtime/diagnostics/diag-ws.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/mono/browser/runtime/diagnostics/diag-ws.ts b/src/mono/browser/runtime/diagnostics/diag-ws.ts index 6191803259219d..e4d830d9d86925 100644 --- a/src/mono/browser/runtime/diagnostics/diag-ws.ts +++ b/src/mono/browser/runtime/diagnostics/diag-ws.ts @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. import { IDiagConnection, DiagConnectionBase, diagnostic_server_loop, schedule_diagnostic_server_loop } from "./common"; +import { mono_log_warn } from "./logging"; export function createDiagConnectionWs (socket_handle:number, url:string):IDiagConnection { return new DiagConnectionWS(socket_handle, url); @@ -33,6 +34,7 @@ class DiagConnectionWS extends DiagConnectionBase implements IDiagConnection { }, { once: true }); ws.addEventListener("message", onMessage); ws.addEventListener("error", () => { + mono_log_warn("Diagnostic server WebSocket connection was closed unexpectedly."); ws.removeEventListener("message", onMessage); }, { once: true }); } From da8478e11098194c251640a0cfacfa1fecfa3afb Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Mon, 7 Apr 2025 14:39:24 +0200 Subject: [PATCH 5/7] feedback --- .../browser/runtime/diagnostics/common.ts | 24 +++++++-------- .../{diag-js.ts => diagnostics-js.ts} | 30 +++++++++---------- .../{diag-ws.ts => diagnostics-ws.ts} | 16 +++++----- .../runtime/diagnostics/dotnet-counters.ts | 8 ++--- .../diagnostics/dotnet-cpu-profiler.ts | 8 ++--- .../runtime/diagnostics/dotnet-gcdump.ts | 8 ++--- src/mono/browser/runtime/diagnostics/index.ts | 10 +++---- .../sample/wasm/browser-eventpipe/Program.cs | 7 ----- 8 files changed, 52 insertions(+), 59 deletions(-) rename src/mono/browser/runtime/diagnostics/{diag-js.ts => diagnostics-js.ts} (84%) rename src/mono/browser/runtime/diagnostics/{diag-ws.ts => diagnostics-ws.ts} (79%) diff --git a/src/mono/browser/runtime/diagnostics/common.ts b/src/mono/browser/runtime/diagnostics/common.ts index 1e14d3774b339a..dab4823b6b78ac 100644 --- a/src/mono/browser/runtime/diagnostics/common.ts +++ b/src/mono/browser/runtime/diagnostics/common.ts @@ -11,26 +11,26 @@ import { mono_log_info } from "./logging"; let lastScheduledTimeoutId: any = undefined; // run another cycle of the event loop, which is EP threads on MT runtime -export function diagnostic_server_loop () { +export function diagnosticServerEventLoop () { lastScheduledTimeoutId = undefined; if (loaderHelpers.is_runtime_running()) { try { runtimeHelpers.mono_background_exec();// give GC chance to run runtimeHelpers.mono_wasm_ds_exec(); - schedule_diagnostic_server_loop(100); + scheduleDiagnosticServerEventLoop(100); } catch (ex) { loaderHelpers.mono_exit(1, ex); } } } -export function schedule_diagnostic_server_loop (delay = 0):void { +export function scheduleDiagnosticServerEventLoop (delay = 0):void { if (!lastScheduledTimeoutId || delay === 0) { - lastScheduledTimeoutId = Module.safeSetTimeout(diagnostic_server_loop, delay); + lastScheduledTimeoutId = Module.safeSetTimeout(diagnosticServerEventLoop, delay); } } -export class DiagConnectionBase { +export class DiagnosticConnectionBase { protected messagesToSend: Uint8Array[] = []; protected messagesReceived: Uint8Array[] = []; constructor (public client_socket:number) { @@ -61,7 +61,7 @@ export class DiagConnectionBase { } } -export interface IDiagConnection { +export interface IDiagnosticConnection { send (message: Uint8Array):number ; poll ():number ; recv (buffer:VoidPtr, bytes_to_read:number):number ; @@ -71,23 +71,23 @@ export interface IDiagConnection { // [hi,lo] export type SessionId=[number, number]; -export interface IDiagSession { +export interface IDiagnosticSession { session_id:SessionId; store(message: Uint8Array): number; sendCommand(message:Uint8Array):void; } -export interface IDiagClient { +export interface IDiagnosticClient { skipDownload?:boolean; onClosePromise:PromiseController; commandOnAdvertise():Uint8Array; - onSessionStart?(session:IDiagSession):void; - onData?(session:IDiagSession, message:Uint8Array):void; + onSessionStart?(session:IDiagnosticSession):void; + onData?(session:IDiagnosticSession, message:Uint8Array):void; onClose?(messages:Uint8Array[]):void; - onError?(session:IDiagSession, message:Uint8Array):void; + onError?(session:IDiagnosticSession, message:Uint8Array):void; } -export type fnClientProvider = (scenarioName:string) => IDiagClient; +export type fnClientProvider = (scenarioName:string) => IDiagnosticClient; export function downloadBlob (messages:Uint8Array[]) { const blob = new Blob(messages, { type: "application/octet-stream" }); diff --git a/src/mono/browser/runtime/diagnostics/diag-js.ts b/src/mono/browser/runtime/diagnostics/diagnostics-js.ts similarity index 84% rename from src/mono/browser/runtime/diagnostics/diag-js.ts rename to src/mono/browser/runtime/diagnostics/diagnostics-js.ts index 0987191a3b9aa2..24f986b2ad2173 100644 --- a/src/mono/browser/runtime/diagnostics/diag-js.ts +++ b/src/mono/browser/runtime/diagnostics/diagnostics-js.ts @@ -2,7 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. import { advert1, CommandSetId, dotnet_IPC_V1, ServerCommandId } from "./client-commands"; -import { DiagConnectionBase, downloadBlob, fnClientProvider, IDiagClient, IDiagConnection, IDiagSession, schedule_diagnostic_server_loop, SessionId } from "./common"; +import { DiagnosticConnectionBase, downloadBlob, fnClientProvider, IDiagnosticClient, IDiagnosticConnection, IDiagnosticSession, scheduleDiagnosticServerEventLoop, SessionId } from "./common"; import { PromiseAndController } from "../types/internal"; import { loaderHelpers } from "./globals"; import { mono_log_warn } from "./logging"; @@ -17,11 +17,11 @@ import { collectGcDump } from "./dotnet-gcdump"; // .withEnvironmentVariable("DOTNET_DiagnosticPorts", "download:gcdump") // or implement function globalThis.dotnetDiagnosticClient with IDiagClient interface -let nextJsClient:PromiseAndController; +let nextJsClient:PromiseAndController; let fromScenarioNameOnce = false; // Only the last which sent advert is receiving commands for all sessions -export let serverSession:DiagSession|undefined = undefined; +export let serverSession:DiagnosticSession|undefined = undefined; // singleton wrapping the protocol with the diagnostic server in the Mono VM // there could be multiple connection at the same time. @@ -36,9 +36,9 @@ export let serverSession:DiagSession|undefined = undefined; // 2<- DC1: command to stop tracing session // DS:close ->1 -class DiagSession extends DiagConnectionBase implements IDiagConnection, IDiagSession { +class DiagnosticSession extends DiagnosticConnectionBase implements IDiagnosticConnection, IDiagnosticSession { public session_id: SessionId = undefined as any; - public diagClient?: IDiagClient; + public diagClient?: IDiagnosticClient; public stopDelayedAfterLastMessage:number|undefined = undefined; public resumedRuntime = false; @@ -54,20 +54,20 @@ class DiagSession extends DiagConnectionBase implements IDiagConnection, IDiagSe serverSession.respond(message); } - async connect_new_client () { + async connectNewClient () { this.diagClient = await nextJsClient.promise; - cleanup_client(); + cleanupClient(); const firstCommand = this.diagClient.commandOnAdvertise(); this.respond(firstCommand); } // this is message from the diagnostic server, which is Mono VM in this browser send (message:Uint8Array):number { - schedule_diagnostic_server_loop(); + scheduleDiagnosticServerEventLoop(); if (advert1.every((v, i) => v === message[i])) { // eslint-disable-next-line @typescript-eslint/no-this-alias serverSession = this; - this.connect_new_client(); + this.connectNewClient(); } else if (dotnet_IPC_V1.every((v, i) => v === message[i]) && message[16] == CommandSetId.Server) { if (message[17] == ServerCommandId.OK) { if (message.byteLength === 28) { @@ -101,7 +101,7 @@ class DiagSession extends DiagConnectionBase implements IDiagConnection, IDiagSe // this is message to the diagnostic server, which is Mono VM in this browser respond (message:Uint8Array) : void { this.messagesReceived.push(message); - schedule_diagnostic_server_loop(); + scheduleDiagnosticServerEventLoop(); } close (): number { @@ -119,15 +119,15 @@ class DiagSession extends DiagConnectionBase implements IDiagConnection, IDiagSe } } -export function cleanup_client () { - nextJsClient = loaderHelpers.createPromiseController(); +export function cleanupClient () { + nextJsClient = loaderHelpers.createPromiseController(); } -export function setup_js_client (client:IDiagClient) { +export function setupJsClient (client:IDiagnosticClient) { nextJsClient.promise_control.resolve(client); } -export function createDiagConnectionJs (socket_handle:number, scenarioName:string):DiagSession { +export function createDiagConnectionJs (socket_handle:number, scenarioName:string):DiagnosticSession { if (!fromScenarioNameOnce) { fromScenarioNameOnce = true; if (scenarioName.startsWith("js://gcdump")) { @@ -144,5 +144,5 @@ export function createDiagConnectionJs (socket_handle:number, scenarioName:strin nextJsClient.promise_control.resolve(dotnetDiagnosticClient(scenarioName)); } } - return new DiagSession(socket_handle); + return new DiagnosticSession(socket_handle); } diff --git a/src/mono/browser/runtime/diagnostics/diag-ws.ts b/src/mono/browser/runtime/diagnostics/diagnostics-ws.ts similarity index 79% rename from src/mono/browser/runtime/diagnostics/diag-ws.ts rename to src/mono/browser/runtime/diagnostics/diagnostics-ws.ts index e4d830d9d86925..a5a18143c18c7b 100644 --- a/src/mono/browser/runtime/diagnostics/diag-ws.ts +++ b/src/mono/browser/runtime/diagnostics/diagnostics-ws.ts @@ -1,11 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -import { IDiagConnection, DiagConnectionBase, diagnostic_server_loop, schedule_diagnostic_server_loop } from "./common"; +import { IDiagnosticConnection, DiagnosticConnectionBase, diagnosticServerEventLoop, scheduleDiagnosticServerEventLoop } from "./common"; import { mono_log_warn } from "./logging"; -export function createDiagConnectionWs (socket_handle:number, url:string):IDiagConnection { - return new DiagConnectionWS(socket_handle, url); +export function createDiagConnectionWs (socket_handle:number, url:string):IDiagnosticConnection { + return new DiagnosticConnectionWS(socket_handle, url); } // this is used together with `dotnet-dsrouter` which will create IPC pipe on your local machine @@ -13,7 +13,7 @@ export function createDiagConnectionWs (socket_handle:number, url:string):IDiagC // 2. configure your wasm dotnet application `.withEnvironmentVariable("DOTNET_DiagnosticPorts", "ws://127.0.0.1:8088/diagnostics")` // 3. run your wasm application // 4. run `dotnet-gcdump -p ` or `dotnet-trace collect -p ` -class DiagConnectionWS extends DiagConnectionBase implements IDiagConnection { +class DiagnosticConnectionWS extends DiagnosticConnectionBase implements IDiagnosticConnection { private ws: WebSocket; constructor (client_socket:number, url:string) { @@ -23,14 +23,14 @@ class DiagConnectionWS extends DiagConnectionBase implements IDiagConnection { const buffer = await evt.data.arrayBuffer(); const message = new Uint8Array(buffer); this.messagesReceived.push(message); - diagnostic_server_loop(); + diagnosticServerEventLoop(); }; ws.addEventListener("open", () => { for (const data of this.messagesToSend) { ws.send(data); } this.messagesToSend = []; - diagnostic_server_loop(); + diagnosticServerEventLoop(); }, { once: true }); ws.addEventListener("message", onMessage); ws.addEventListener("error", () => { @@ -40,7 +40,7 @@ class DiagConnectionWS extends DiagConnectionBase implements IDiagConnection { } send (message:Uint8Array):number { - schedule_diagnostic_server_loop(); + scheduleDiagnosticServerEventLoop(); // copy the message if (this.ws!.readyState == WebSocket.CLOSED) { return -1; @@ -55,7 +55,7 @@ class DiagConnectionWS extends DiagConnectionBase implements IDiagConnection { } close ():number { - schedule_diagnostic_server_loop(); + scheduleDiagnosticServerEventLoop(); this.ws.close(); return 0; } diff --git a/src/mono/browser/runtime/diagnostics/dotnet-counters.ts b/src/mono/browser/runtime/diagnostics/dotnet-counters.ts index cebdb115d4e151..eb9ed471bd18af 100644 --- a/src/mono/browser/runtime/diagnostics/dotnet-counters.ts +++ b/src/mono/browser/runtime/diagnostics/dotnet-counters.ts @@ -4,9 +4,9 @@ import type { DiagnosticCommandOptions } from "../types"; import { commandStopTracing, commandCounters } from "./client-commands"; -import { IDiagSession } from "./common"; +import { IDiagnosticSession } from "./common"; import { Module } from "./globals"; -import { serverSession, setup_js_client } from "./diag-js"; +import { serverSession, setupJsClient } from "./diagnostics-js"; import { loaderHelpers } from "./globals"; export function collectPerfCounters (options?:DiagnosticCommandOptions):Promise { @@ -16,13 +16,13 @@ export function collectPerfCounters (options?:DiagnosticCommandOptions):Promise< } const onClosePromise = loaderHelpers.createPromiseController(); - function onSessionStart (session: IDiagSession): void { + function onSessionStart (session: IDiagnosticSession): void { // stop tracing after period of monitoring Module.safeSetTimeout(() => { session.sendCommand(commandStopTracing(session.session_id)); }, 1000 * (options?.durationSeconds ?? 60)); } - setup_js_client({ + setupJsClient({ onClosePromise:onClosePromise.promise_control, skipDownload:options.skipDownload, commandOnAdvertise:() => commandCounters(options.intervalSeconds || 1, options.extraProviders || []), diff --git a/src/mono/browser/runtime/diagnostics/dotnet-cpu-profiler.ts b/src/mono/browser/runtime/diagnostics/dotnet-cpu-profiler.ts index 58e67c86dbaeb8..ab9b1887434861 100644 --- a/src/mono/browser/runtime/diagnostics/dotnet-cpu-profiler.ts +++ b/src/mono/browser/runtime/diagnostics/dotnet-cpu-profiler.ts @@ -5,8 +5,8 @@ import type { DiagnosticCommandOptions } from "../types"; import { commandStopTracing, commandSampleProfiler } from "./client-commands"; import { loaderHelpers, Module, runtimeHelpers } from "./globals"; -import { serverSession, setup_js_client } from "./diag-js"; -import { IDiagSession } from "./common"; +import { serverSession, setupJsClient } from "./diagnostics-js"; +import { IDiagnosticSession } from "./common"; export function collectCpuSamples (options?:DiagnosticCommandOptions):Promise { if (!options) options = {}; @@ -18,14 +18,14 @@ export function collectCpuSamples (options?:DiagnosticCommandOptions):Promise(); - function onSessionStart (session: IDiagSession): void { + function onSessionStart (session: IDiagnosticSession): void { // stop tracing after period of monitoring Module.safeSetTimeout(() => { session.sendCommand(commandStopTracing(session.session_id)); }, 1000 * (options?.durationSeconds ?? 60)); } - setup_js_client({ + setupJsClient({ onClosePromise:onClosePromise.promise_control, skipDownload:options.skipDownload, commandOnAdvertise: () => commandSampleProfiler(options.extraProviders || []), diff --git a/src/mono/browser/runtime/diagnostics/dotnet-gcdump.ts b/src/mono/browser/runtime/diagnostics/dotnet-gcdump.ts index 0486025cd1ba1f..2690231976eabd 100644 --- a/src/mono/browser/runtime/diagnostics/dotnet-gcdump.ts +++ b/src/mono/browser/runtime/diagnostics/dotnet-gcdump.ts @@ -4,9 +4,9 @@ import type { DiagnosticCommandOptions } from "../types"; import { commandStopTracing, commandGcHeapDump, } from "./client-commands"; -import { IDiagSession } from "./common"; +import { IDiagnosticSession } from "./common"; import { loaderHelpers, Module } from "./globals"; -import { serverSession, setup_js_client } from "./diag-js"; +import { serverSession, setupJsClient } from "./diagnostics-js"; export function collectGcDump (options?:DiagnosticCommandOptions):Promise { if (!options) options = {}; @@ -17,7 +17,7 @@ export function collectGcDump (options?:DiagnosticCommandOptions):Promise(); let stopDelayedAfterLastMessage = 0; let stopSent = false; - function onData (session: IDiagSession, message: Uint8Array): void { + function onData (session: IDiagnosticSession, message: Uint8Array): void { session.store(message); if (!stopSent) { // stop 500ms after last GC message on this session, there will be more messages after that @@ -31,7 +31,7 @@ export function collectGcDump (options?:DiagnosticCommandOptions):Promise commandGcHeapDump(options.extraProviders || []), diff --git a/src/mono/browser/runtime/diagnostics/index.ts b/src/mono/browser/runtime/diagnostics/index.ts index c47f92570a7798..a5c3e234bc46fe 100644 --- a/src/mono/browser/runtime/diagnostics/index.ts +++ b/src/mono/browser/runtime/diagnostics/index.ts @@ -5,16 +5,16 @@ import type { GlobalObjects } from "../types/internal"; import type { CharPtr, VoidPtr } from "../types/emscripten"; import { Module, runtimeHelpers } from "./globals"; -import { cleanup_client as cleanup_js_client, createDiagConnectionJs, serverSession } from "./diag-js"; -import { IDiagConnection } from "./common"; -import { createDiagConnectionWs } from "./diag-ws"; +import { cleanupClient as cleanup_js_client, createDiagConnectionJs, serverSession } from "./diagnostics-js"; +import { IDiagnosticConnection } from "./common"; +import { createDiagConnectionWs } from "./diagnostics-ws"; import { diagnosticHelpers, setRuntimeGlobalsImpl } from "./globals"; import { collectCpuSamples } from "./dotnet-cpu-profiler"; import { collectPerfCounters } from "./dotnet-counters"; import { collectGcDump } from "./dotnet-gcdump"; import { advert1Full } from "./client-commands"; -let socket_handles:Map = undefined as any; +let socket_handles:Map = undefined as any; let next_socket_handle = 1; let url_override:string | undefined = undefined; @@ -23,7 +23,7 @@ export function setRuntimeGlobals (globalObjects: GlobalObjects): void { diagnosticHelpers.ds_rt_websocket_create = (urlPtr :CharPtr):number => { if (!socket_handles) { - socket_handles = new Map(); + socket_handles = new Map(); } const url = url_override ?? runtimeHelpers.utf8ToString(urlPtr); const socket_handle = next_socket_handle++; diff --git a/src/mono/sample/wasm/browser-eventpipe/Program.cs b/src/mono/sample/wasm/browser-eventpipe/Program.cs index 4c26d735b6796f..9e6fed1bb7a1f1 100644 --- a/src/mono/sample/wasm/browser-eventpipe/Program.cs +++ b/src/mono/sample/wasm/browser-eventpipe/Program.cs @@ -37,13 +37,6 @@ public partial class Test { public static int Main(string[] args) { - // ActivityTracker.Instance.Enable(); via reflection - var type = typeof(EventSource).Assembly.GetType("System.Diagnostics.Tracing.ActivityTracker"); - var prop = type.GetProperty("Instance", BindingFlags.Static | BindingFlags.Public); - var m = type.GetMethod("Enable"); - var instance = prop.GetValue(null); - m.Invoke(instance, null); - DisplayMeaning(42); WasmHelloEventSource.Instance.NewCallsCounter(); From 1330bd884d896c53be559b724827577dca7f3bdf Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Mon, 7 Apr 2025 17:08:24 +0200 Subject: [PATCH 6/7] - feedback - implement EP cookie - mono_wasm_process_current_pid --- .../runtime/diagnostics/client-commands.ts | 25 +++++++++++-- .../runtime/diagnostics/diagnostics-js.ts | 37 +++++++++++++------ src/mono/browser/runtime/diagnostics/index.ts | 4 +- src/mono/browser/runtime/exports-binding.ts | 5 ++- src/mono/browser/runtime/exports.ts | 3 +- src/mono/browser/runtime/startup.ts | 6 +++ src/mono/browser/runtime/types/internal.ts | 1 + src/mono/mono/utils/mono-proclib.c | 6 +++ 8 files changed, 67 insertions(+), 20 deletions(-) diff --git a/src/mono/browser/runtime/diagnostics/client-commands.ts b/src/mono/browser/runtime/diagnostics/client-commands.ts index f72cd7926ce419..dd8340365efc3c 100644 --- a/src/mono/browser/runtime/diagnostics/client-commands.ts +++ b/src/mono/browser/runtime/diagnostics/client-commands.ts @@ -4,16 +4,33 @@ import type { DiagnosticCommandProviderV2 } from "../types"; import { SessionId } from "./common"; +import { runtimeHelpers } from "./globals"; -export const advert1 = [65, 68, 86, 82, 95]; -// TODO make GUID and process id dynamic -export const advert1Full = [65, 68, 86, 82, 95, 86, 49, 0, 66, 108, 106, 181, 91, 0, 142, 79, 182, 145, 225, 120, 77, 12, 131, 229, 42, 0, 0, 0, 0, 0, 0, 0, 0, 0]; +// ADVR_V1\0 +export const advert1 = [65, 68, 86, 82, 95, 86, 49, 0,]; +// DOTNET_IPC_V1\0 export const dotnet_IPC_V1 = [68, 79, 84, 78, 69, 84, 95, 73, 80, 67, 95, 86, 49, 0]; - // this file contains the IPC commands that are sent by client (like dotnet-trace) to the diagnostic server (like Mono VM in the browser) // just formatting bytes, no sessions management here + +export function advertise () { + // xxxxxxxx-xxxx-4xxx-xxxx-xxxxxxxxxxxx + const uuid = new Uint8Array(16); + crypto.getRandomValues(uuid); + uuid[7] = (uuid[7] & 0xf) | 0x40;// version 4 + + const pid = runtimeHelpers.mono_wasm_process_current_pid(); + + return Uint8Array.from([ + ...advert1, + ...uuid, + ...serializeUint64([0, pid]), + 0, 0// future + ]); +} + export function commandStopTracing (sessionID:SessionId) { return Uint8Array.from([ ...serializeHeader(CommandSetId.EventPipe, EventPipeCommandId.StopTracing, computeMessageByteLength(8)), diff --git a/src/mono/browser/runtime/diagnostics/diagnostics-js.ts b/src/mono/browser/runtime/diagnostics/diagnostics-js.ts index 24f986b2ad2173..8fd885ba674b8c 100644 --- a/src/mono/browser/runtime/diagnostics/diagnostics-js.ts +++ b/src/mono/browser/runtime/diagnostics/diagnostics-js.ts @@ -61,24 +61,37 @@ class DiagnosticSession extends DiagnosticConnectionBase implements IDiagnosticC this.respond(firstCommand); } + is_advert_message (message:Uint8Array):boolean { + return advert1.every((v, i) => v === message[i]); + } + + is_response_message (message:Uint8Array):boolean { + return dotnet_IPC_V1.every((v, i) => v === message[i]) && message[16] == CommandSetId.Server; + } + + is_response_ok_with_session (message:Uint8Array):boolean { + return message.byteLength === 28 && message[17] == ServerCommandId.OK; + } + + parse_session_id (message:Uint8Array):SessionId { + const view = message.subarray(20, 28); + const sessionIDLo = view[0] | (view[1] << 8) | (view[2] << 16) | (view[3] << 24); + const sessionIDHi = view[4] | (view[5] << 8) | (view[6] << 16) | (view[7] << 24); + return [sessionIDHi, sessionIDLo] as SessionId; + } + // this is message from the diagnostic server, which is Mono VM in this browser send (message:Uint8Array):number { scheduleDiagnosticServerEventLoop(); - if (advert1.every((v, i) => v === message[i])) { + if (this.is_advert_message(message)) { // eslint-disable-next-line @typescript-eslint/no-this-alias serverSession = this; this.connectNewClient(); - } else if (dotnet_IPC_V1.every((v, i) => v === message[i]) && message[16] == CommandSetId.Server) { - if (message[17] == ServerCommandId.OK) { - if (message.byteLength === 28) { - const view = message.subarray(20, 28); - const sessionIDLo = view[0] | (view[1] << 8) | (view[2] << 16) | (view[3] << 24); - const sessionIDHi = view[4] | (view[5] << 8) | (view[6] << 16) | (view[7] << 24); - const sessionId = [sessionIDHi, sessionIDLo] as SessionId; - this.session_id = sessionId; - if (this.diagClient?.onSessionStart) { - this.diagClient.onSessionStart(this); - } + } else if (this.is_response_message(message)) { + if (this.is_response_ok_with_session(message)) { + this.session_id = this.parse_session_id(message); + if (this.diagClient?.onSessionStart) { + this.diagClient.onSessionStart(this); } } else { if (this.diagClient?.onError) { diff --git a/src/mono/browser/runtime/diagnostics/index.ts b/src/mono/browser/runtime/diagnostics/index.ts index a5c3e234bc46fe..f7ddb450681387 100644 --- a/src/mono/browser/runtime/diagnostics/index.ts +++ b/src/mono/browser/runtime/diagnostics/index.ts @@ -12,7 +12,7 @@ import { diagnosticHelpers, setRuntimeGlobalsImpl } from "./globals"; import { collectCpuSamples } from "./dotnet-cpu-profiler"; import { collectPerfCounters } from "./dotnet-counters"; import { collectGcDump } from "./dotnet-gcdump"; -import { advert1Full } from "./client-commands"; +import { advertise } from "./client-commands"; let socket_handles:Map = undefined as any; let next_socket_handle = 1; @@ -89,5 +89,5 @@ function connectDSRouter (url: string): void { const wrapper = createDiagConnectionWs(serverSession.client_socket, url); socket_handles.set(serverSession.client_socket, wrapper); - wrapper.send(new Uint8Array(advert1Full)); + wrapper.send(advertise()); } diff --git a/src/mono/browser/runtime/exports-binding.ts b/src/mono/browser/runtime/exports-binding.ts index e0e7faa06d57ea..fbed0f8a2d3fc2 100644 --- a/src/mono/browser/runtime/exports-binding.ts +++ b/src/mono/browser/runtime/exports-binding.ts @@ -11,7 +11,7 @@ import { mono_interp_jit_wasm_entry_trampoline, mono_interp_record_interp_entry import { mono_interp_jit_wasm_jit_call_trampoline, mono_interp_invoke_wasm_jit_call_trampoline, mono_interp_flush_jitcall_queue } from "./jiterpreter-jit-call"; import { mono_wasm_resolve_or_reject_promise } from "./marshal-to-js"; import { mono_wasm_schedule_timer, schedule_background_exec } from "./scheduling"; -import { mono_wasm_asm_loaded } from "./startup"; +import { mono_wasm_asm_loaded, mono_wasm_process_current_pid } from "./startup"; import { mono_log_warn, mono_wasm_console_clear, mono_wasm_trace_logger } from "./logging"; import { mono_wasm_browser_entropy } from "./crypto"; import { mono_wasm_cancel_promise } from "./cancelable-promise"; @@ -83,6 +83,9 @@ export const mono_wasm_imports = [ // src/native/minipal/random.c mono_wasm_browser_entropy, + // mono-proclib.c + mono_wasm_process_current_pid, + // corebindings.c mono_wasm_console_clear, mono_wasm_release_cs_owned_object, diff --git a/src/mono/browser/runtime/exports.ts b/src/mono/browser/runtime/exports.ts index 3dc0117767161b..af519bc4b908aa 100644 --- a/src/mono/browser/runtime/exports.ts +++ b/src/mono/browser/runtime/exports.ts @@ -11,7 +11,7 @@ import { type RuntimeAPI } from "./types"; import { Module, exportedRuntimeAPI, loaderHelpers, passEmscriptenInternals, runtimeHelpers, setRuntimeGlobals, } from "./globals"; import { GlobalObjects, RuntimeHelpers } from "./types/internal"; -import { configureEmscriptenStartup, configureRuntimeStartup, configureWorkerStartup } from "./startup"; +import { configureEmscriptenStartup, configureRuntimeStartup, configureWorkerStartup, mono_wasm_process_current_pid } from "./startup"; import { create_weak_ref } from "./weak-ref"; import { export_internal } from "./exports-internal"; @@ -42,6 +42,7 @@ function initializeExports (globalObjects: GlobalObjects): RuntimeAPI { jiterpreter_dump_stats, forceDisposeProxies, utf8ToString, + mono_wasm_process_current_pid, mono_background_exec: () => tcwraps.mono_background_exec(), mono_wasm_ds_exec: () => tcwraps.mono_wasm_ds_exec(), }; diff --git a/src/mono/browser/runtime/startup.ts b/src/mono/browser/runtime/startup.ts index be92590cefa96e..60ab49bc0f6611 100644 --- a/src/mono/browser/runtime/startup.ts +++ b/src/mono/browser/runtime/startup.ts @@ -34,6 +34,12 @@ import { runtimeList } from "./exports"; import { nativeAbort, nativeExit } from "./run"; import { replaceEmscriptenPThreadInit } from "./pthreads/worker-thread"; +const pid = (globalThis.performance?.timeOrigin ?? Date.now()) | 0; + +export function mono_wasm_process_current_pid ():number { + return pid; +} + export async function configureRuntimeStartup (module: DotnetModuleInternal): Promise { if (!module.out) { // eslint-disable-next-line no-console diff --git a/src/mono/browser/runtime/types/internal.ts b/src/mono/browser/runtime/types/internal.ts index 9b7512a11625e0..5e132209563ee8 100644 --- a/src/mono/browser/runtime/types/internal.ts +++ b/src/mono/browser/runtime/types/internal.ts @@ -244,6 +244,7 @@ export type RuntimeHelpers = { utf8ToString: (ptr: CharPtr) => string, mono_background_exec: () => void, mono_wasm_ds_exec: () => void, + mono_wasm_process_current_pid: () => number, } export type DiagnosticHelpers = { diff --git a/src/mono/mono/utils/mono-proclib.c b/src/mono/mono/utils/mono-proclib.c index 2627c5aa059ff1..0daec1915691c0 100644 --- a/src/mono/mono/utils/mono-proclib.c +++ b/src/mono/mono/utils/mono-proclib.c @@ -29,11 +29,17 @@ #endif #endif +#if defined(HOST_BROWSER) +int mono_wasm_process_current_pid (); +#endif + int mono_process_current_pid (void) { #ifdef HOST_WIN32 return (int) GetCurrentProcessId (); +#elif defined(HOST_BROWSER) + return mono_wasm_process_current_pid (); #elif defined(HAVE_GETPID) return (int) getpid (); #elif defined(HOST_WASI) From 3d175464085450a081a59dcbff8240e51dce66e0 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Tue, 8 Apr 2025 11:39:03 +0200 Subject: [PATCH 7/7] feedback --- .../runtime/diagnostics/client-commands.ts | 31 +++++++++++-------- .../runtime/diagnostics/dotnet-counters.ts | 2 +- .../diagnostics/dotnet-cpu-profiler.ts | 2 +- .../runtime/diagnostics/dotnet-gcdump.ts | 2 +- src/mono/browser/runtime/dotnet.d.ts | 1 + src/mono/browser/runtime/types/index.ts | 1 + 6 files changed, 23 insertions(+), 16 deletions(-) diff --git a/src/mono/browser/runtime/diagnostics/client-commands.ts b/src/mono/browser/runtime/diagnostics/client-commands.ts index dd8340365efc3c..91f9db34a83a7f 100644 --- a/src/mono/browser/runtime/diagnostics/client-commands.ts +++ b/src/mono/browser/runtime/diagnostics/client-commands.ts @@ -1,7 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -import type { DiagnosticCommandProviderV2 } from "../types"; +import type { DiagnosticCommandOptions } from "../types"; import { SessionId } from "./common"; import { runtimeHelpers } from "./globals"; @@ -18,7 +18,7 @@ export const dotnet_IPC_V1 = [68, 79, 84, 78, 69, 84, 95, 73, 80, 67, 95, 86, 49 export function advertise () { // xxxxxxxx-xxxx-4xxx-xxxx-xxxxxxxxxxxx const uuid = new Uint8Array(16); - crypto.getRandomValues(uuid); + globalThis.crypto.getRandomValues(uuid); uuid[7] = (uuid[7] & 0xf) | 0x40;// version 4 const pid = runtimeHelpers.mono_wasm_process_current_pid(); @@ -50,10 +50,9 @@ export function commandProcessInfo3 () { ]); } - -export function commandGcHeapDump (extraProviders:DiagnosticCommandProviderV2[]) { +export function commandGcHeapDump (options:DiagnosticCommandOptions) { return commandCollectTracing2({ - circularBufferMB: 256, + circularBufferMB:options.circularBufferMB || 256, format: 1, requestRundown: true, providers: [ @@ -70,14 +69,20 @@ export function commandGcHeapDump (extraProviders:DiagnosticCommandProviderV2[]) provider_name: "Microsoft-Windows-DotNETRuntime", arguments: null }, - ...extraProviders, + ...options.extraProviders || [], ] }); } -export function commandCounters (intervalSec:number, extraProviders:DiagnosticCommandProviderV2[]) { +function uuidv4 () { + return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, c => + (+c ^ globalThis.crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> +c / 4).toString(16) + ); +} + +export function commandCounters (options:DiagnosticCommandOptions) { return commandCollectTracing2({ - circularBufferMB: 256, + circularBufferMB:options.circularBufferMB || 256, format: 1, requestRundown: false, providers: [ @@ -85,16 +90,16 @@ export function commandCounters (intervalSec:number, extraProviders:DiagnosticCo keywords: [0, Keywords.GCHandle], logLevel: 4, provider_name: "System.Diagnostics.Metrics", - arguments: `SessionId=SHARED;Metrics=System.Runtime;RefreshInterval=${intervalSec};MaxTimeSeries=1000;MaxHistograms=10;ClientId=c98f989b-369c-41af-bc8e-7ab261fba16c` + arguments: `SessionId=SHARED;Metrics=System.Runtime;RefreshInterval=${options.intervalSeconds || 1};MaxTimeSeries=1000;MaxHistograms=10;ClientId=${uuidv4()};`, }, - ...extraProviders, + ...options.extraProviders || [], ] }); } -export function commandSampleProfiler (extraProviders:DiagnosticCommandProviderV2[]) { +export function commandSampleProfiler (options:DiagnosticCommandOptions) { return commandCollectTracing2({ - circularBufferMB: 256, + circularBufferMB:options.circularBufferMB || 256, format: 1, requestRundown: true, providers: [ @@ -107,7 +112,7 @@ export function commandSampleProfiler (extraProviders:DiagnosticCommandProviderV provider_name: "Microsoft-DotNETCore-SampleProfiler", arguments: null }, - ...extraProviders, + ...options.extraProviders || [], ] }); } diff --git a/src/mono/browser/runtime/diagnostics/dotnet-counters.ts b/src/mono/browser/runtime/diagnostics/dotnet-counters.ts index eb9ed471bd18af..35ad332394c32b 100644 --- a/src/mono/browser/runtime/diagnostics/dotnet-counters.ts +++ b/src/mono/browser/runtime/diagnostics/dotnet-counters.ts @@ -25,7 +25,7 @@ export function collectPerfCounters (options?:DiagnosticCommandOptions):Promise< setupJsClient({ onClosePromise:onClosePromise.promise_control, skipDownload:options.skipDownload, - commandOnAdvertise:() => commandCounters(options.intervalSeconds || 1, options.extraProviders || []), + commandOnAdvertise:() => commandCounters(options), onSessionStart, }); return onClosePromise.promise; diff --git a/src/mono/browser/runtime/diagnostics/dotnet-cpu-profiler.ts b/src/mono/browser/runtime/diagnostics/dotnet-cpu-profiler.ts index ab9b1887434861..4dc34cb8d21484 100644 --- a/src/mono/browser/runtime/diagnostics/dotnet-cpu-profiler.ts +++ b/src/mono/browser/runtime/diagnostics/dotnet-cpu-profiler.ts @@ -28,7 +28,7 @@ export function collectCpuSamples (options?:DiagnosticCommandOptions):Promise commandSampleProfiler(options.extraProviders || []), + commandOnAdvertise: () => commandSampleProfiler(options), onSessionStart, }); return onClosePromise.promise; diff --git a/src/mono/browser/runtime/diagnostics/dotnet-gcdump.ts b/src/mono/browser/runtime/diagnostics/dotnet-gcdump.ts index 2690231976eabd..d0d503db4d5ceb 100644 --- a/src/mono/browser/runtime/diagnostics/dotnet-gcdump.ts +++ b/src/mono/browser/runtime/diagnostics/dotnet-gcdump.ts @@ -34,7 +34,7 @@ export function collectGcDump (options?:DiagnosticCommandOptions):Promise commandGcHeapDump(options.extraProviders || []), + commandOnAdvertise: () => commandGcHeapDump(options), onData, }); return onClosePromise.promise; diff --git a/src/mono/browser/runtime/dotnet.d.ts b/src/mono/browser/runtime/dotnet.d.ts index 4da9600e037e19..85cc8a6b7106e7 100644 --- a/src/mono/browser/runtime/dotnet.d.ts +++ b/src/mono/browser/runtime/dotnet.d.ts @@ -665,6 +665,7 @@ type DiagnosticCommandOptions = { durationSeconds?: number; intervalSeconds?: number; skipDownload?: boolean; + circularBufferMB?: number; extraProviders?: DiagnosticCommandProviderV2[]; }; type APIType = RunAPIType & MemoryAPIType & DiagnosticsAPIType; diff --git a/src/mono/browser/runtime/types/index.ts b/src/mono/browser/runtime/types/index.ts index 0ba9c0e242ca7c..b651b6828e210f 100644 --- a/src/mono/browser/runtime/types/index.ts +++ b/src/mono/browser/runtime/types/index.ts @@ -633,6 +633,7 @@ export type DiagnosticCommandOptions = { durationSeconds?:number, intervalSeconds?:number, skipDownload?:boolean, + circularBufferMB?:number, extraProviders?:DiagnosticCommandProviderV2[], }