diff --git a/src/libraries/tests.proj b/src/libraries/tests.proj index c0f3bfc5dec51..784906c82385a 100644 --- a/src/libraries/tests.proj +++ b/src/libraries/tests.proj @@ -57,6 +57,11 @@ + + + + + diff --git a/src/mono/System.Private.CoreLib/src/System/Threading/Overlapped.cs b/src/mono/System.Private.CoreLib/src/System/Threading/Overlapped.cs index 602c2c27bcaea..708c6d04f200b 100644 --- a/src/mono/System.Private.CoreLib/src/System/Threading/Overlapped.cs +++ b/src/mono/System.Private.CoreLib/src/System/Threading/Overlapped.cs @@ -99,8 +99,10 @@ internal sealed unsafe class OverlappedData success = true; #if FEATURE_PERFTRACING +#if !(TARGET_BROWSER && !FEATURE_WASM_THREADS) if (NativeRuntimeEventSource.Log.IsEnabled()) NativeRuntimeEventSource.Log.ThreadPoolIOPack(pNativeOverlapped); +#endif #endif return _pNativeOverlapped; } diff --git a/src/mono/cmake/config.h.in b/src/mono/cmake/config.h.in index 7b8a193da0c24..7dd80ddbaf861 100644 --- a/src/mono/cmake/config.h.in +++ b/src/mono/cmake/config.h.in @@ -330,6 +330,9 @@ /* Disable Threads */ #cmakedefine DISABLE_THREADS 1 +/* Disable user thread creation on WebAssembly */ +#cmakedefine DISABLE_WASM_USER_THREADS 1 + /* Disable MONO_LOG_DEST */ #cmakedefine DISABLE_LOG_DEST diff --git a/src/mono/cmake/options.cmake b/src/mono/cmake/options.cmake index c0fd0ecf43222..f00430fc9500d 100644 --- a/src/mono/cmake/options.cmake +++ b/src/mono/cmake/options.cmake @@ -56,6 +56,7 @@ option (ENABLE_OVERRIDABLE_ALLOCATORS "Enable overridable allocator support") option (ENABLE_SIGALTSTACK "Enable support for using sigaltstack for SIGSEGV and stack overflow handling, this doesn't work on some platforms") option (USE_MALLOC_FOR_MEMPOOLS "Use malloc for each single mempool allocation, so tools like Valgrind can run better") option (STATIC_COMPONENTS "Compile mono runtime components as static (not dynamic) libraries") +option (DISABLE_WASM_USER_THREADS "Disable creation of user managed threads on WebAssembly, only allow runtime internal managed and native threads") set (MONO_GC "sgen" CACHE STRING "Garbage collector implementation (sgen or boehm). Default: sgen") set (GC_SUSPEND "default" CACHE STRING "GC suspend method (default, preemptive, coop, hybrid)") diff --git a/src/mono/mono/component/CMakeLists.txt b/src/mono/mono/component/CMakeLists.txt index c00f11b841586..6e34cbf41b502 100644 --- a/src/mono/mono/component/CMakeLists.txt +++ b/src/mono/mono/component/CMakeLists.txt @@ -65,6 +65,7 @@ set(${MONO_DIAGNOSTICS_TRACING_COMPONENT_NAME}-sources ${diagnostic_server_sources} ${MONO_COMPONENT_PATH}/event_pipe.c ${MONO_COMPONENT_PATH}/event_pipe.h + ${MONO_COMPONENT_PATH}/event_pipe-wasm.h ${MONO_COMPONENT_PATH}/diagnostics_server.c ${MONO_COMPONENT_PATH}/diagnostics_server.h ) diff --git a/src/mono/mono/component/event_pipe-stub.c b/src/mono/mono/component/event_pipe-stub.c index cb82a216b2d3d..5069532985e76 100644 --- a/src/mono/mono/component/event_pipe-stub.c +++ b/src/mono/mono/component/event_pipe-stub.c @@ -4,6 +4,7 @@ #include #include "mono/component/event_pipe.h" +#include "mono/component/event_pipe-wasm.h" #include "mono/metadata/components.h" static EventPipeSessionID _dummy_session_id; @@ -495,3 +496,37 @@ mono_component_event_pipe_init (void) { return component_event_pipe_stub_init (); } + +#ifdef HOST_WASM + +EMSCRIPTEN_KEEPALIVE gboolean +mono_wasm_event_pipe_enable (const ep_char8_t *output_path, + uint32_t circular_buffer_size_in_mb, + const ep_char8_t *providers, + /* EventPipeSessionType session_type = EP_SESSION_TYPE_FILE, */ + /* EventPipieSerializationFormat format = EP_SERIALIZATION_FORMAT_NETTRACE_V4, */ + /* bool */ gboolean rundown_requested, + /* IpcStream stream = NULL, */ + /* EventPipeSessionSycnhronousCallback sync_callback = NULL, */ + /* void *callback_additional_data, */ + MonoWasmEventPipeSessionID *out_session_id) +{ + if (out_session_id) + *out_session_id = 0; + return 0; +} + + +EMSCRIPTEN_KEEPALIVE gboolean +mono_wasm_event_pipe_session_start_streaming (MonoWasmEventPipeSessionID session_id) +{ + g_assert_not_reached (); +} + +EMSCRIPTEN_KEEPALIVE gboolean +mono_wasm_event_pipe_session_disable (MonoWasmEventPipeSessionID session_id) +{ + g_assert_not_reached (); +} + +#endif /* HOST_WASM */ diff --git a/src/mono/mono/component/event_pipe-wasm.h b/src/mono/mono/component/event_pipe-wasm.h new file mode 100644 index 0000000000000..90e4a4d8b9948 --- /dev/null +++ b/src/mono/mono/component/event_pipe-wasm.h @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// + +#ifndef _MONO_COMPONENT_EVENT_PIPE_WASM_H +#define _MONO_COMPONENT_EVENT_PIPE_WASM_H + +#include +#include +#include +#include + +#ifdef HOST_WASM + +#include + +G_BEGIN_DECLS + +#if SIZEOF_VOID_P == 4 +/* EventPipeSessionID is 64 bits, which is awkward to work with in JS. + Fortunately the actual session IDs are derived from pointers which + are 32-bit on wasm32, so the top bits are zero. */ +typedef uint32_t MonoWasmEventPipeSessionID; +#else +#error "EventPipeSessionID is 64-bits, update the JS side to work with it" +#endif + +EMSCRIPTEN_KEEPALIVE gboolean +mono_wasm_event_pipe_enable (const ep_char8_t *output_path, + uint32_t circular_buffer_size_in_mb, + const ep_char8_t *providers, + /* EventPipeSessionType session_type = EP_SESSION_TYPE_FILE, */ + /* EventPipieSerializationFormat format = EP_SERIALIZATION_FORMAT_NETTRACE_V4, */ + /* bool */ gboolean rundown_requested, + /* IpcStream stream = NULL, */ + /* EventPipeSessionSycnhronousCallback sync_callback = NULL, */ + /* void *callback_additional_data, */ + MonoWasmEventPipeSessionID *out_session_id); + +EMSCRIPTEN_KEEPALIVE gboolean +mono_wasm_event_pipe_session_start_streaming (MonoWasmEventPipeSessionID session_id); + +EMSCRIPTEN_KEEPALIVE gboolean +mono_wasm_event_pipe_session_disable (MonoWasmEventPipeSessionID session_id); + +G_END_DECLS + +#endif /* HOST_WASM */ + + +#endif /* _MONO_COMPONENT_EVENT_PIPE_WASM_H */ + diff --git a/src/mono/mono/component/event_pipe.c b/src/mono/mono/component/event_pipe.c index 3c950bf11ccb4..f933b416d6840 100644 --- a/src/mono/mono/component/event_pipe.c +++ b/src/mono/mono/component/event_pipe.c @@ -4,13 +4,16 @@ #include #include +#include #include #include +#include #include #include #include #include + extern void ep_rt_mono_component_init (void); static bool _event_pipe_component_inited = false; @@ -327,3 +330,73 @@ mono_component_event_pipe_init (void) return &fn_table; } + + +#ifdef HOST_WASM + + +static MonoWasmEventPipeSessionID +ep_to_wasm_session_id (EventPipeSessionID session_id) +{ + g_assert (0 == (uint64_t)session_id >> 32); + return (uint32_t)session_id; +} + +static EventPipeSessionID +wasm_to_ep_session_id (MonoWasmEventPipeSessionID session_id) +{ + return session_id; +} + +EMSCRIPTEN_KEEPALIVE gboolean +mono_wasm_event_pipe_enable (const ep_char8_t *output_path, + uint32_t circular_buffer_size_in_mb, + const ep_char8_t *providers, + /* EventPipeSessionType session_type = EP_SESSION_TYPE_FILE, */ + /* EventPipieSerializationFormat format = EP_SERIALIZATION_FORMAT_NETTRACE_V4, */ + /* bool */ gboolean rundown_requested, + /* IpcStream stream = NULL, */ + /* EventPipeSessionSycnhronousCallback sync_callback = NULL, */ + /* void *callback_additional_data, */ + MonoWasmEventPipeSessionID *out_session_id) +{ + MONO_ENTER_GC_UNSAFE; + EventPipeSerializationFormat format = EP_SERIALIZATION_FORMAT_NETTRACE_V4; + EventPipeSessionType session_type = EP_SESSION_TYPE_FILE; + + EventPipeSessionID session; + session = ep_enable_2 (output_path, + circular_buffer_size_in_mb, + providers, + session_type, + format, + !!rundown_requested, + /* stream */NULL, + /* callback*/ NULL, + /* callback_data*/ NULL); + + if (out_session_id) + *out_session_id = ep_to_wasm_session_id (session); + MONO_EXIT_GC_UNSAFE; + return TRUE; +} + +EMSCRIPTEN_KEEPALIVE gboolean +mono_wasm_event_pipe_session_start_streaming (MonoWasmEventPipeSessionID session_id) +{ + MONO_ENTER_GC_UNSAFE; + ep_start_streaming (wasm_to_ep_session_id (session_id)); + MONO_EXIT_GC_UNSAFE; + return TRUE; +} + +EMSCRIPTEN_KEEPALIVE gboolean +mono_wasm_event_pipe_session_disable (MonoWasmEventPipeSessionID session_id) +{ + MONO_ENTER_GC_UNSAFE; + ep_disable (wasm_to_ep_session_id (session_id)); + MONO_EXIT_GC_UNSAFE; + return TRUE; +} + +#endif /* HOST_WASM */ diff --git a/src/mono/mono/metadata/threads.c b/src/mono/mono/metadata/threads.c index 418ee6100a95e..50576527215d1 100644 --- a/src/mono/mono/metadata/threads.c +++ b/src/mono/mono/metadata/threads.c @@ -4815,7 +4815,7 @@ ves_icall_System_Threading_Thread_StartInternal (MonoThreadObjectHandle thread_h MonoThread *internal = MONO_HANDLE_RAW (thread_handle); gboolean res; -#ifdef DISABLE_THREADS +#if defined (DISABLE_THREADS) || defined (DISABLE_WASM_USER_THREADS) mono_error_set_platform_not_supported (error, "Cannot start threads on this runtime."); return; #endif diff --git a/src/mono/sample/wasm/browser-eventpipe/Makefile b/src/mono/sample/wasm/browser-eventpipe/Makefile new file mode 100644 index 0000000000000..6f4130b5b64b5 --- /dev/null +++ b/src/mono/sample/wasm/browser-eventpipe/Makefile @@ -0,0 +1,11 @@ +TOP=../../../../.. + +include ../wasm.mk + +ifneq ($(AOT),) +override MSBUILD_ARGS+=/p:RunAOTCompilation=true +endif + +PROJECT_NAME=Wasm.Browser.EventPipe.Sample.csproj + +run: run-browser 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 0000000000000..2755fcd7e254a --- /dev/null +++ b/src/mono/sample/wasm/browser-eventpipe/Program.cs @@ -0,0 +1,67 @@ +// 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.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + + +namespace Sample +{ + public class Test + { + public static void Main(string[] args) + { + // not called. See main.js for all the interesting bits + } + + private static int iterations; + private static CancellationTokenSource cts; + + public static CancellationToken GetCancellationToken() + { + if (cts == null) { + cts = new CancellationTokenSource (); + } + return cts.Token; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static long recursiveFib (int n) + { + if (n < 1) + return 0; + if (n == 1) + return 1; + return recursiveFib (n - 1) + recursiveFib (n - 2); + } + + public static async Task StartAsyncWork() + { + CancellationToken ct = GetCancellationToken(); + long b; + const int N = 35; + const long expected = 9227465; + while (true) + { + await Task.Delay(1).ConfigureAwait(false); + b = recursiveFib (N); + if (ct.IsCancellationRequested) + break; + iterations++; + } + return b == expected ? 42 : 0; + } + + public static void StopWork() + { + cts.Cancel(); + } + + public static string GetIterationsDone() + { + return iterations.ToString(); + } + } +} 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 0000000000000..477e50104edce --- /dev/null +++ b/src/mono/sample/wasm/browser-eventpipe/Wasm.Browser.EventPipe.Sample.csproj @@ -0,0 +1,41 @@ + + + true + main.js + true + embedded + 1 + false + true + true + $(ExecXHarnessCmd) wasm test-browser --app=. --browser=Chrome $(XHarnessBrowserPathArg) --html-file=index.html --output-directory=$(XHarnessOutput) -- $(MSBuildProjectName).dll + true + + + + + + + + + <_SampleProject>Wasm.Browser.CJS.Sample.csproj + + + + + 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 0000000000000..87ef4b46638f9 --- /dev/null +++ b/src/mono/sample/wasm/browser-eventpipe/index.html @@ -0,0 +1,20 @@ + + + + + + + Sample EventPipe profile session + + + + + + + Computing Fib repeatedly: + + + + + + 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 0000000000000..ef8029db5094f --- /dev/null +++ b/src/mono/sample/wasm/browser-eventpipe/main.js @@ -0,0 +1,102 @@ +function wasm_exit(exit_code) { + /* Set result in a tests_done element, to be read by xharness in runonly CI test */ + const tests_done_elem = document.createElement("label"); + tests_done_elem.id = "tests_done"; + tests_done_elem.innerHTML = exit_code.toString(); + document.body.appendChild(tests_done_elem); + + console.log(`WASM EXIT ${exit_code}`); +} + +function downloadData(dataURL,filename) +{ + // make an `` link and click on it to trigger a download with the given name + const elt = document.createElement('a'); + elt.download = filename; + elt.href = dataURL; + + document.body.appendChild(elt); + + elt.click(); + + document.body.removeChild(elt); +} + +function makeTimestamp() +{ + // ISO date string, but with : and . replaced by - + const t = new Date(); + const s = t.toISOString(); + return s.replace(/[:.]/g, '-'); +} + +async function loadRuntime() { + globalThis.exports = {}; + await import("./dotnet.js"); + return globalThis.exports.createDotnetRuntime; +} + + +const delay = (ms) => new Promise((resolve) => setTimeout (resolve, ms)) + +const saveUsingBlob = true; + +async function main() { + const createDotnetRuntime = await loadRuntime(); + const { MONO, BINDING, Module, RuntimeBuildInfo } = await createDotnetRuntime(() => { + console.log('user code in createDotnetRuntime') + return { + disableDotnet6Compatibility: true, + configSrc: "./mono-config.json", + preInit: () => { console.log('user code Module.preInit') }, + preRun: () => { console.log('user code Module.preRun') }, + onRuntimeInitialized: () => { console.log('user code Module.onRuntimeInitialized') }, + postRun: () => { console.log('user code Module.postRun') }, + } + }); + globalThis.__Module = Module; + globalThis.MONO = MONO; + console.log('after createDotnetRuntime') + + const startWork = BINDING.bind_static_method("[Wasm.Browser.EventPipe.Sample] Sample.Test:StartAsyncWork"); + const stopWork = BINDING.bind_static_method("[Wasm.Browser.EventPipe.Sample] Sample.Test:StopWork"); + const getIterationsDone = BINDING.bind_static_method("[Wasm.Browser.EventPipe.Sample] Sample.Test:GetIterationsDone"); + const eventSession = MONO.diagnostics.createEventPipeSession(); + eventSession.start(); + const workPromise = startWork(); + + document.getElementById("out").innerHTML = '<<running>>'; + await delay(5000); // let it run for 5 seconds + + stopWork(); + + document.getElementById("out").innerHTML = '<<stopping>>'; + + const ret = await workPromise; // get the answer + const iterations = getIterationsDone(); // get how many times the loop ran + + eventSession.stop(); + + document.getElementById("out").innerHTML = `${ret} as computed in ${iterations} iterations on dotnet ver ${RuntimeBuildInfo.ProductVersion}`; + + console.debug(`ret: ${ret}`); + + const filename = "dotnet-wasm-" + makeTimestamp() + ".nettrace"; + + if (saveUsingBlob) { + const blob = eventSession.getTraceBlob(); + const uri = URL.createObjectURL(blob); + downloadData(uri, filename); + URL.revokeObjectURL(uri); + } else { + const dataUri = eventSession.getTraceDataURI(); + + downloadData(dataUri, filename); + } + const exit_code = ret == 42 ? 0 : 1; + + wasm_exit(exit_code); +} + +console.log("Waiting 10s for curious human before starting the program"); +setTimeout(main, 10000); diff --git a/src/mono/wasm/runtime/buffers.ts b/src/mono/wasm/runtime/buffers.ts index 118c8d43162e2..3a607275fa643 100644 --- a/src/mono/wasm/runtime/buffers.ts +++ b/src/mono/wasm/runtime/buffers.ts @@ -202,4 +202,4 @@ export function mono_wasm_load_bytes_into_heap(bytes: Uint8Array): VoidPtr { const heapBytes = new Uint8Array(Module.HEAPU8.buffer, memoryOffset, bytes.length); heapBytes.set(bytes); return memoryOffset; -} \ No newline at end of file +} diff --git a/src/mono/wasm/runtime/cuint64.ts b/src/mono/wasm/runtime/cuint64.ts new file mode 100644 index 0000000000000..20ab2c88ca422 --- /dev/null +++ b/src/mono/wasm/runtime/cuint64.ts @@ -0,0 +1,48 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +/// Define a type that can hold a 64 bit integer value from Emscripten. +/// Import this module with 'import * as cuint64 from "./cuint64";' +/// and 'import type { CUInt64 } from './cuint64'; +export type CUInt64 = readonly [number, number]; + +export function toBigInt (x: CUInt64): bigint { + return BigInt(x[0]) | BigInt(x[1]) << BigInt(32); +} + +export function fromBigInt (x: bigint): CUInt64 { + if (x < BigInt(0)) + throw new Error(`${x} is not a valid 64 bit integer`); + if (x > BigInt(0xFFFFFFFFFFFFFFFF)) + throw new Error(`${x} is not a valid 64 bit integer`); + const low = Number(x & BigInt(0xFFFFFFFF)); + const high = Number(x >> BigInt(32)); + return [low, high]; +} + +export function dangerousToNumber (x: CUInt64): number { + return x[0] | x[1] << 32; +} + +export function fromNumber (x: number): CUInt64 { + if (x < 0) + throw new Error(`${x} is not a valid 64 bit integer`); + if ((x >> 32) > 0xFFFFFFFF) + throw new Error(`${x} is not a valid 64 bit integer`); + if (Math.trunc(x) != x) + throw new Error(`${x} is not a valid 64 bit integer`); + return [x & 0xFFFFFFFF, x >> 32]; +} + +export function pack32 (lo: number, hi: number): CUInt64 { + return [lo, hi]; +} + +export function unpack32 (x: CUInt64): [number, number] { + return [x[0], x[1]]; +} + +export const zero: CUInt64 = [0, 0]; + + + diff --git a/src/mono/wasm/runtime/cwraps.ts b/src/mono/wasm/runtime/cwraps.ts index 861824f56d77b..52663f7f39aa6 100644 --- a/src/mono/wasm/runtime/cwraps.ts +++ b/src/mono/wasm/runtime/cwraps.ts @@ -65,6 +65,11 @@ const fn_signatures: [ident: string, returnType: string | null, argTypes?: strin ["mono_wasm_get_type_name", "string", ["number"]], ["mono_wasm_get_type_aqn", "string", ["number"]], + // MONO.diagnostics + ["mono_wasm_event_pipe_enable", "bool", ["string", "number", "string", "bool", "number"]], + ["mono_wasm_event_pipe_session_start_streaming", "bool", ["number"]], + ["mono_wasm_event_pipe_session_disable", "bool", ["number"]], + //DOTNET ["mono_wasm_string_from_js", "number", ["string"]], @@ -156,6 +161,11 @@ export interface t_Cwraps { */ mono_wasm_obj_array_set(array: MonoArray, idx: number, obj: MonoObject): void; + // MONO.diagnostics + mono_wasm_event_pipe_enable(outputPath: string, bufferSizeInMB: number, providers: string, rundownRequested: boolean, outSessionId: VoidPtr): boolean; + mono_wasm_event_pipe_session_start_streaming(sessionId: number): boolean; + mono_wasm_event_pipe_session_disable(sessionId: number): boolean; + //DOTNET /** * @deprecated Not GC or thread safe @@ -191,4 +201,4 @@ export function wrap_c_function(name: string): Function { const fce = Module.cwrap(sig[0], sig[1], sig[2], sig[3]); wf[sig[0]] = fce; return fce; -} \ No newline at end of file +} diff --git a/src/mono/wasm/runtime/diagnostics.ts b/src/mono/wasm/runtime/diagnostics.ts new file mode 100644 index 0000000000000..eee3f061b979f --- /dev/null +++ b/src/mono/wasm/runtime/diagnostics.ts @@ -0,0 +1,128 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import { Module } from "./imports"; +import cwraps from "./cwraps"; +import type { EventPipeSessionOptions } from "./types"; +import type { VoidPtr } from "./types/emscripten"; +import * as memory from "./memory"; + +const sizeOfInt32 = 4; + +export type EventPipeSessionID = bigint; +type EventPipeSessionIDImpl = number; + +/// An EventPipe session object represents a single diagnostic tracing session that is collecting +/// events from the runtime and managed libraries. There may be multiple active sessions at the same time. +/// Each session subscribes to a number of providers and will collect events from the time that start() is called, until stop() is called. +/// Upon completion the session saves the events to a file on the VFS. +/// The data can then be retrieved as Blob. +export interface EventPipeSession { + // session ID for debugging logging only + get sessionID(): EventPipeSessionID; + start(): void; + stop(): void; + getTraceBlob(): Blob; +} + +// internal session state of the JS instance +enum State { + Initialized, + Started, + Done, +} + +function start_streaming(sessionID: EventPipeSessionIDImpl): void { + cwraps.mono_wasm_event_pipe_session_start_streaming(sessionID); +} + +function stop_streaming(sessionID: EventPipeSessionIDImpl): void { + cwraps.mono_wasm_event_pipe_session_disable(sessionID); +} + +/// An EventPipe session that saves the event data to a file in the VFS. +class EventPipeFileSession implements EventPipeSession { + private _state: State; + private _sessionID: EventPipeSessionIDImpl; + private _tracePath: string; // VFS file path to the trace file + + get sessionID(): bigint { return BigInt(this._sessionID); } + + constructor(sessionID: EventPipeSessionIDImpl, tracePath: string) { + this._state = State.Initialized; + this._sessionID = sessionID; + this._tracePath = tracePath; + console.debug(`EventPipe session ${this.sessionID} created`); + } + + start = () => { + if (this._state !== State.Initialized) { + throw new Error(`EventPipe session ${this.sessionID} already started`); + } + this._state = State.Started; + start_streaming(this._sessionID); + console.debug(`EventPipe session ${this.sessionID} started`); + } + + stop = () => { + if (this._state !== State.Started) { + throw new Error(`cannot stop an EventPipe session in state ${this._state}, not 'Started'`); + } + this._state = State.Done; + stop_streaming(this._sessionID); + console.debug(`EventPipe session ${this.sessionID} stopped`); + } + + getTraceBlob = () => { + if (this._state !== State.Done) { + throw new Error(`session is in state ${this._state}, not 'Done'`); + } + const data = Module.FS_readFile(this._tracePath, { encoding: "binary" }) as Uint8Array; + return new Blob([data], { type: "application/octet-stream" }); + } +} + +// a conter for the number of sessions created +let totalSessions = 0; + +function createSessionWithPtrCB(sessionIdOutPtr: VoidPtr, options: EventPipeSessionOptions | undefined, tracePath: string): false | number { + const defaultRundownRequested = true; + const defaultProviders = ""; + const defaultBufferSizeInMB = 1; + + const rundown = options?.collectRundownEvents ?? defaultRundownRequested; + + memory.setI32(sessionIdOutPtr, 0); + if (!cwraps.mono_wasm_event_pipe_enable(tracePath, defaultBufferSizeInMB, defaultProviders, rundown, sessionIdOutPtr)) { + return false; + } else { + return memory.getI32(sessionIdOutPtr); + } +} + +export interface Diagnostics { + createEventPipeSession(options?: EventPipeSessionOptions): EventPipeSession | null; +} + +/// APIs for working with .NET diagnostics from JavaScript. +export const diagnostics: Diagnostics = { + /// Creates a new EventPipe session that will collect trace events from the runtime and managed libraries. + /// Use the options to control the kinds of events to be collected. + /// Multiple sessions may be created and started at the same time. + createEventPipeSession(options?: EventPipeSessionOptions): EventPipeSession | null { + // The session trace is saved to a file in the VFS. The file name doesn't matter, + // but we'd like it to be distinct from other traces. + const tracePath = `/trace-${totalSessions++}.nettrace`; + + const success = memory.withStackAlloc(sizeOfInt32, createSessionWithPtrCB, options, tracePath); + + if (success === false) + return null; + const sessionID = success; + + const session = new EventPipeFileSession(sessionID, tracePath); + return session; + }, +}; + +export default diagnostics; diff --git a/src/mono/wasm/runtime/dotnet.d.ts b/src/mono/wasm/runtime/dotnet.d.ts index e31742188e500..07c1db8ac7c6f 100644 --- a/src/mono/wasm/runtime/dotnet.d.ts +++ b/src/mono/wasm/runtime/dotnet.d.ts @@ -46,6 +46,9 @@ declare interface EmscriptenModule { FS_readFile(filename: string, opts: any): any; removeRunDependency(id: string): void; addRunDependency(id: string): void; + stackSave(): VoidPtr; + stackRestore(stack: VoidPtr): void; + stackAlloc(size: number): VoidPtr; ready: Promise; preInit?: (() => any)[]; preRun?: (() => any)[]; @@ -205,6 +208,9 @@ declare type CoverageProfilerOptions = { write_at?: string; send_to?: string; }; +interface EventPipeSessionOptions { + collectRundownEvents?: boolean; +} declare type DotnetModuleConfig = { disableDotnet6Compatibility?: boolean; config?: MonoConfig | MonoConfigError; @@ -236,6 +242,17 @@ declare type DotnetModuleConfigImports = { url?: any; }; +declare type EventPipeSessionID = bigint; +interface EventPipeSession { + get sessionID(): EventPipeSessionID; + start(): void; + stop(): void; + getTraceBlob(): Blob; +} +interface Diagnostics { + createEventPipeSession(options?: EventPipeSessionOptions): EventPipeSession | null; +} + declare function mono_wasm_runtime_ready(): void; declare function mono_wasm_setenv(name: string, value: string): void; @@ -344,6 +361,7 @@ declare const MONO: { getU32: typeof getU32; getF32: typeof getF32; getF64: typeof getF64; + diagnostics: Diagnostics; }; declare type MONOType = typeof MONO; declare const BINDING: { diff --git a/src/mono/wasm/runtime/exports.ts b/src/mono/wasm/runtime/exports.ts index 229caf9d9843f..e370c2e97c345 100644 --- a/src/mono/wasm/runtime/exports.ts +++ b/src/mono/wasm/runtime/exports.ts @@ -67,6 +67,7 @@ import { create_weak_ref } from "./weak-ref"; import { fetch_like, readAsync_like } from "./polyfills"; import { EmscriptenModule } from "./types/emscripten"; import { mono_run_main, mono_run_main_and_exit } from "./run"; +import { diagnostics } from "./diagnostics"; const MONO = { // current "public" MONO API @@ -110,6 +111,9 @@ const MONO = { getU32, getF32, getF64, + + // Diagnostics + diagnostics }; export type MONOType = typeof MONO; @@ -418,4 +422,4 @@ class RuntimeList { const wr = this.list[runtimeId]; return wr ? wr.deref() : undefined; } -} \ No newline at end of file +} diff --git a/src/mono/wasm/runtime/memory.ts b/src/mono/wasm/runtime/memory.ts index b61b379e87e8c..e524bb48046e3 100644 --- a/src/mono/wasm/runtime/memory.ts +++ b/src/mono/wasm/runtime/memory.ts @@ -1,5 +1,6 @@ import { Module } from "./imports"; import { VoidPtr, NativePointer, ManagedPointer } from "./types/emscripten"; +import * as cuint64 from "./cuint64"; const alloca_stack: Array = []; const alloca_buffer_size = 32 * 1024; @@ -48,7 +49,7 @@ export function setU16(offset: _MemOffset, value: number): void { Module.HEAPU16[offset >>> 1] = value; } -export function setU32 (offset: _MemOffset, value: _NumberOrPointer) : void { +export function setU32(offset: _MemOffset, value: _NumberOrPointer): void { Module.HEAPU32[offset >>> 2] = value; } @@ -60,7 +61,7 @@ export function setI16(offset: _MemOffset, value: number): void { Module.HEAP16[offset >>> 1] = value; } -export function setI32 (offset: _MemOffset, value: _NumberOrPointer) : void { +export function setI32(offset: _MemOffset, value: _NumberOrPointer): void { Module.HEAP32[offset >>> 2] = value; } @@ -114,3 +115,33 @@ export function getF32(offset: _MemOffset): number { export function getF64(offset: _MemOffset): number { return Module.HEAPF64[offset >>> 3]; } + +export function getCU64(offset: _MemOffset): cuint64.CUInt64 { + const lo = getU32(offset); + const hi = getU32(offset + 4); + return cuint64.pack32(lo, hi); +} + +export function setCU64(offset: _MemOffset, value: cuint64.CUInt64): void { + const [lo, hi] = cuint64.unpack32(value); + setU32(offset, lo); + setU32(offset + 4, hi); +} + +/// Allocates a new buffer of the given size on the Emscripten stack and passes a pointer to it to the callback. +/// Returns the result of the callback. As usual with stack allocations, the buffer is freed when the callback returns. +/// Do not attempt to use the stack pointer after the callback is finished. +export function withStackAlloc(bytesWanted: number, f: (ptr: VoidPtr) => TResult): TResult; +export function withStackAlloc(bytesWanted: number, f: (ptr: VoidPtr, ud1: T1) => TResult, ud1: T1): TResult; +export function withStackAlloc(bytesWanted: number, f: (ptr: VoidPtr, ud1: T1, ud2: T2) => TResult, ud1: T1, ud2: T2): TResult; +export function withStackAlloc(bytesWanted: number, f: (ptr: VoidPtr, ud1: T1, ud2: T2, ud3: T3) => TResult, ud1: T1, ud2: T2, ud3: T3): TResult; +export function withStackAlloc(bytesWanted: number, f: (ptr: VoidPtr, ud1?: T1, ud2?: T2, ud3?: T3) => TResult, ud1?: T1, ud2?: T2, ud3?: T3): TResult { + const sp = Module.stackSave(); + const ptr = Module.stackAlloc(bytesWanted); + try { + return f(ptr, ud1, ud2, ud3); + } finally { + Module.stackRestore(sp); + } +} + diff --git a/src/mono/wasm/runtime/types.ts b/src/mono/wasm/runtime/types.ts index a7e065a6d9924..d26277e38fb60 100644 --- a/src/mono/wasm/runtime/types.ts +++ b/src/mono/wasm/runtime/types.ts @@ -178,6 +178,13 @@ export type CoverageProfilerOptions = { send_to?: string // should be in the format ::, default: 'WebAssembly.Runtime::DumpCoverageProfileData' (DumpCoverageProfileData stores the data into INTERNAL.coverage_profile_data.) } +/// Options to configure the event pipe session +export interface EventPipeSessionOptions { + /// Whether to collect additional details (such as method and type names) at EventPipeSession.stop() time (default: true) + /// This is required for some use cases, and may allow some tools to better understand the events. + collectRundownEvents?: boolean; +} + // how we extended emscripten Module export type DotnetModule = EmscriptenModule & DotnetModuleConfig; diff --git a/src/mono/wasm/runtime/types/emscripten.ts b/src/mono/wasm/runtime/types/emscripten.ts index dde4798263b2f..6a9ea3605c21e 100644 --- a/src/mono/wasm/runtime/types/emscripten.ts +++ b/src/mono/wasm/runtime/types/emscripten.ts @@ -52,6 +52,10 @@ export declare interface EmscriptenModule { FS_readFile(filename: string, opts: any): any; removeRunDependency(id: string): void; addRunDependency(id: string): void; + stackSave(): VoidPtr; + stackRestore(stack: VoidPtr): void; + stackAlloc(size: number): VoidPtr; + ready: Promise; preInit?: (() => any)[]; @@ -62,4 +66,4 @@ export declare interface EmscriptenModule { instantiateWasm: (imports: any, successCallback: Function) => any; } -export declare type TypedArray = Int8Array | Uint8Array | Uint8ClampedArray | Int16Array | Uint16Array | Int32Array | Uint32Array | Float32Array | Float64Array; \ No newline at end of file +export declare type TypedArray = Int8Array | Uint8Array | Uint8ClampedArray | Int16Array | Uint16Array | Int32Array | Uint32Array | Float32Array | Float64Array;