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;