diff --git a/src/libraries/tests.proj b/src/libraries/tests.proj index c0f3bfc5dec511..784906c82385a8 100644 --- a/src/libraries/tests.proj +++ b/src/libraries/tests.proj @@ -57,6 +57,11 @@ <ProjectExclusions Include="$(MonoProjectRoot)sample\wasm\browser-mt-eventpipe\Wasm.Browser.ThreadsEP.Sample.csproj" /> </ItemGroup> + <!-- Samples that require a perf-tracing wasm runtime --> + <ItemGroup Condition="'$(TargetOS)' == 'Browser' and '$(WasmEnablePerfTracing)' != 'true'" > + <ProjectExclusions Include="$(MonoProjectRoot)sample\wasm\browser-eventpipe\Wasm.Browser.EventPipe.Sample.csproj" /> + </ItemGroup> + <!-- Wasm aot on all platforms --> <ItemGroup Condition="'$(TargetOS)' == 'Browser' and '$(BuildAOTTestsOnHelix)' == 'true' and '$(RunDisabledWasmTests)' != 'true' and '$(RunAOTCompilation)' == 'true'"> <!-- https://github.com/dotnet/runtime/issues/66118 --> 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 602c2c27bcaeac..708c6d04f200b7 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 7b8a193da0c24b..7dd80ddbaf8619 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 c0fd0ecf43222e..f00430fc9500d6 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 c00f11b8415861..6e34cbf41b5022 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 cb82a216b2d3d6..5069532985e769 100644 --- a/src/mono/mono/component/event_pipe-stub.c +++ b/src/mono/mono/component/event_pipe-stub.c @@ -4,6 +4,7 @@ #include <config.h> #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 00000000000000..90e4a4d8b99482 --- /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 <stdint.h> +#include <eventpipe/ep-ipc-pal-types-forward.h> +#include <eventpipe/ep-types-forward.h> +#include <glib.h> + +#ifdef HOST_WASM + +#include <emscripten.h> + +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 3c950bf11ccb43..f933b416d68406 100644 --- a/src/mono/mono/component/event_pipe.c +++ b/src/mono/mono/component/event_pipe.c @@ -4,13 +4,16 @@ #include <config.h> #include <mono/component/event_pipe.h> +#include <mono/component/event_pipe-wasm.h> #include <mono/utils/mono-publib.h> #include <mono/utils/mono-compiler.h> +#include <mono/utils/mono-threads-api.h> #include <eventpipe/ep.h> #include <eventpipe/ep-event.h> #include <eventpipe/ep-event-instance.h> #include <eventpipe/ep-session.h> + 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 418ee6100a95ed..50576527215d1d 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 00000000000000..6f4130b5b64b55 --- /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 00000000000000..2755fcd7e254a6 --- /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<int> 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 00000000000000..477e50104edce2 --- /dev/null +++ b/src/mono/sample/wasm/browser-eventpipe/Wasm.Browser.EventPipe.Sample.csproj @@ -0,0 +1,41 @@ +<Project Sdk="Microsoft.NET.Sdk"> + <PropertyGroup> + <WasmCopyAppZipToHelixTestDir Condition="'$(ArchiveTests)' == 'true'">true</WasmCopyAppZipToHelixTestDir> + <WasmMainJSPath>main.js</WasmMainJSPath> + <DebugSymbols>true</DebugSymbols> + <DebugType>embedded</DebugType> + <WasmDebugLevel>1</WasmDebugLevel> + <WasmEnableES6>false</WasmEnableES6> + <WasmBuildNative>true</WasmBuildNative> + <GenerateRunScriptForSample Condition="'$(ArchiveTests)' == 'true'">true</GenerateRunScriptForSample> + <RunScriptCommand>$(ExecXHarnessCmd) wasm test-browser --app=. --browser=Chrome $(XHarnessBrowserPathArg) --html-file=index.html --output-directory=$(XHarnessOutput) -- $(MSBuildProjectName).dll</RunScriptCommand> + <FeatureWasmPerfTracing>true</FeatureWasmPerfTracing> + </PropertyGroup> + + <ItemGroup> + <WasmExtraFilesToDeploy Include="index.html" /> + <WasmExtraConfig Condition="false" Include="environment_variables" Value=' +{ + "MONO_LOG_LEVEL": "debug", + "MONO_LOG_MASK": "diagnostics" +}' /> + </ItemGroup> + + <PropertyGroup> + <_SampleProject>Wasm.Browser.CJS.Sample.csproj</_SampleProject> + </PropertyGroup> + + + <PropertyGroup> + <RunAnalyzers>true</RunAnalyzers> + </PropertyGroup> + + <!-- set the condition to false and you will get a CA1416 errors about calls to create DiagnosticCounter instances --> + <ItemGroup Condition="true"> + <!-- TODO: some .props file that automates this. Unfortunately just adding a ProjectReference to Microsoft.NET.WebAssembly.Threading.proj doesn't work - it ends up bundling the ref assemblies into the publish directory and breaking the app. --> + <!-- it's a reference assembly, but the project system doesn't know that - include it during compilation, but don't publish it --> + <ProjectReference Include="$(LibrariesProjectRoot)\System.Diagnostics.Tracing.WebAssembly.PerfTracing\ref\System.Diagnostics.Tracing.WebAssembly.PerfTracing.csproj" IncludeAssets="compile" PrivateAssets="none" ExcludeAssets="runtime" Private="false" /> + </ItemGroup> + + <Target Name="RunSample" DependsOnTargets="RunSampleWithBrowser" /> +</Project> 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..87ef4b46638f9a --- /dev/null +++ b/src/mono/sample/wasm/browser-eventpipe/index.html @@ -0,0 +1,20 @@ +<!DOCTYPE html> +<!-- Licensed to the .NET Foundation under one or more agreements. --> +<!-- The .NET Foundation licenses this file to you under the MIT license. --> +<html> + +<head> + <title>Sample EventPipe profile session</title> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> +</head> + +<body> + <h3 id="header">Wasm Browser EventPipe profiling Sample</h3> + Computing Fib repeatedly: <span id="out"></span> + <script type="text/javascript" src="./dotnet.js"></script> + <script type="text/javascript" src="./dotnet.worker.js"></script> + <script type="text/javascript" src="./main.js"></script> +</body> + +</html> 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..ef8029db5094f3 --- /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 `<a download="filename" href="data:..."/>` 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 118c8d43162e23..3a607275fa643e 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, <any>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 00000000000000..20ab2c88ca422f --- /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 861824f56d77bd..52663f7f39aa6b 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 00000000000000..eee3f061b979f6 --- /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 e31742188e500a..07c1db8ac7c6f8 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<unknown>; 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 229caf9d9843fe..e370c2e97c3456 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 b61b379e87e8c5..e524bb48046e38 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<VoidPtr> = []; const alloca_buffer_size = 32 * 1024; @@ -48,7 +49,7 @@ export function setU16(offset: _MemOffset, value: number): void { Module.HEAPU16[<any>offset >>> 1] = value; } -export function setU32 (offset: _MemOffset, value: _NumberOrPointer) : void { +export function setU32(offset: _MemOffset, value: _NumberOrPointer): void { Module.HEAPU32[<any>offset >>> 2] = <number><any>value; } @@ -60,7 +61,7 @@ export function setI16(offset: _MemOffset, value: number): void { Module.HEAP16[<any>offset >>> 1] = value; } -export function setI32 (offset: _MemOffset, value: _NumberOrPointer) : void { +export function setI32(offset: _MemOffset, value: _NumberOrPointer): void { Module.HEAP32[<any>offset >>> 2] = <number><any>value; } @@ -114,3 +115,33 @@ export function getF32(offset: _MemOffset): number { export function getF64(offset: _MemOffset): number { return Module.HEAPF64[<any>offset >>> 3]; } + +export function getCU64(offset: _MemOffset): cuint64.CUInt64 { + const lo = getU32(offset); + const hi = getU32(<any>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(<any>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<TResult>(bytesWanted: number, f: (ptr: VoidPtr) => TResult): TResult; +export function withStackAlloc<T1, TResult>(bytesWanted: number, f: (ptr: VoidPtr, ud1: T1) => TResult, ud1: T1): TResult; +export function withStackAlloc<T1, T2, TResult>(bytesWanted: number, f: (ptr: VoidPtr, ud1: T1, ud2: T2) => TResult, ud1: T1, ud2: T2): TResult; +export function withStackAlloc<T1, T2, T3, TResult>(bytesWanted: number, f: (ptr: VoidPtr, ud1: T1, ud2: T2, ud3: T3) => TResult, ud1: T1, ud2: T2, ud3: T3): TResult; +export function withStackAlloc<T1, T2, T3, TResult>(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 a7e065a6d99242..d26277e38fb60f 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 <CLASS>::<METHODNAME>, 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 dde4798263b2f7..6a9ea3605c21e8 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<unknown>; 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;