[wasm-ep] Minimal diagnostic tracing configuration and sample (#69158)
authorAleksey Kliger (λgeek) <aleksey@lambdageek.org>
Thu, 19 May 2022 00:27:44 +0000 (20:27 -0400)
committerGitHub <noreply@github.com>
Thu, 19 May 2022 00:27:44 +0000 (20:27 -0400)
* Adds a `/p:WasmEnablePerfTracing=true` configuration.

   In this configuration the runtime is built with threading (`MonoWasmThreads` property is true), but user C# code is not allowed to start threads and doesn't use the portable threadpool (`MonoWasmThreadsNoUser` property is also true).

   The upshot is that EventPipe can start threads but user code is still single-threaded.

* Adds a `MONO.diagnostics` interface in JavaScript.  There's a single method for now `createEventPipeSession` which creates a session that can save a trace to a file on the virtual file system.  JS code (or a user in a dev console) needs to call `start()` and `stop()` on the session object to begin collecting samples.    The data is saved temporarily to the Emscripten VFS and can be retrived into a JavaScript Blob (and from there downloaded to a file outside the browser).

* Adds a sample that runs an async task for five seconds and collects samples and then triggers a `click()` to download the trace file out of the browser.

* Adds a TS module to help with working with uint64_t values in the emscripten heap.

* Exposes empscripten Module.stackSave, stackRestore and stackAlloc operations to the runtime TS modules.  Use for working with event pipe session ID outparam.

---

* add DISABLE_WASM_USER_THREADS mono cmake option

* Disable Thread.StartInternal icall if DISABLE_WASM_USER_THREADS

   if threading is enabled for the runtime internally, but disabled for user code, throw PNSE

* add an eventpipe sample

* [wasm-ep] (browser-eventpipe sample) run loop for longer

* [samples/wasm-eventpipe] make an async task sample

   change the sample to do some work asynchronously using setTimeout instead of blocking

* [wasm] Add MONO.diagnostics interface

   Binds enable, start, disable methods defaulting to non-streaming FILE mode

* if wasm threads are disabled, but perftracing is enabled, don't log overlapped io events

* fix whitespace and nits

* don't need try/finally in the sample anymore

* more whitespace

* add start method to EventPipeSession interface

* don't run wasm-eventpipe sample on CI lanes without perftracing

* more whitespace

* fix eslint warnings, default rundown to true, allow callback for traceFilePath option

* add EventPipeSession.getTraceBlob

   for retrieving the collected traces instead of exposing the emscripten VFS directly.

   update the sample to use URL.createObjectURL (session.getTraceBlob()) to create the download link

* [browser-eventpipe sample] remove unnecessary ref assemblies

* use ep_char8_t for C decls of event pipe wasm exports

* Use stack allocation for temporaries

   Expose the emscripten stack allocation API

* Use 32-bit EventPipe session ID on WASM

   64 bit integers are awkward to work with in JavaScript.

   The EventPipe session ID is derived from a pointer address, so even though it is nominally a 64-bit value, in practice the top bits are zero.

   Use a 32-bit int to represent the session ID on the javascript side and convert to 64-bit in C when calling down to the EventPipe APIs

* Make the sample do more work in managed

   give the sample profiler some non-empty samples to collect

* Move withStackAlloc to memory.ts

* simplify VFS .nettrace file naming

   Just use consecutive integers to uniquify the session traces.  Dont' need a fancy timestamp in the VFS (which would also not be unique if you create sessions below the timestamp resolution)

* Add overloads to memory.withStackAlloc to avoid creating closures

   Pass additional arguments to the callback function

* sample: explain why there's a 10s pause

* move createEventPipeSession callback to a function

   ensures the closure is created once

* Use a tuple type for withStackAlloc

* use unsigned 32-bit get/set in cuint64 get/set

* fix whitespace

23 files changed:
src/libraries/tests.proj
src/mono/System.Private.CoreLib/src/System/Threading/Overlapped.cs
src/mono/cmake/config.h.in
src/mono/cmake/options.cmake
src/mono/mono/component/CMakeLists.txt
src/mono/mono/component/event_pipe-stub.c
src/mono/mono/component/event_pipe-wasm.h [new file with mode: 0644]
src/mono/mono/component/event_pipe.c
src/mono/mono/metadata/threads.c
src/mono/sample/wasm/browser-eventpipe/Makefile [new file with mode: 0644]
src/mono/sample/wasm/browser-eventpipe/Program.cs [new file with mode: 0644]
src/mono/sample/wasm/browser-eventpipe/Wasm.Browser.EventPipe.Sample.csproj [new file with mode: 0644]
src/mono/sample/wasm/browser-eventpipe/index.html [new file with mode: 0644]
src/mono/sample/wasm/browser-eventpipe/main.js [new file with mode: 0644]
src/mono/wasm/runtime/buffers.ts
src/mono/wasm/runtime/cuint64.ts [new file with mode: 0644]
src/mono/wasm/runtime/cwraps.ts
src/mono/wasm/runtime/diagnostics.ts [new file with mode: 0644]
src/mono/wasm/runtime/dotnet.d.ts
src/mono/wasm/runtime/exports.ts
src/mono/wasm/runtime/memory.ts
src/mono/wasm/runtime/types.ts
src/mono/wasm/runtime/types/emscripten.ts

index c0f3bfc..784906c 100644 (file)
     <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 -->
index 602c2c2..708c6d0 100644 (file)
@@ -99,9 +99,11 @@ namespace System.Threading
 
                 success = true;
 #if FEATURE_PERFTRACING
+#if !(TARGET_BROWSER && !FEATURE_WASM_THREADS)
                 if (NativeRuntimeEventSource.Log.IsEnabled())
                     NativeRuntimeEventSource.Log.ThreadPoolIOPack(pNativeOverlapped);
 #endif
+#endif
                 return _pNativeOverlapped;
             }
             finally
index 7b8a193..7dd80dd 100644 (file)
 /* 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
 
index c0fd0ec..f00430f 100644 (file)
@@ -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)")
index c00f11b..6e34cbf 100644 (file)
@@ -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
   )
index cb82a21..5069532 100644 (file)
@@ -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 (file)
index 0000000..90e4a4d
--- /dev/null
@@ -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 */
+
index 3c950bf..f933b41 100644 (file)
@@ -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 */
index 418ee61..5057652 100644 (file)
@@ -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 (file)
index 0000000..6f4130b
--- /dev/null
@@ -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 (file)
index 0000000..2755fcd
--- /dev/null
@@ -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 (file)
index 0000000..477e501
--- /dev/null
@@ -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 (file)
index 0000000..87ef4b4
--- /dev/null
@@ -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 (file)
index 0000000..ef8029d
--- /dev/null
@@ -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 = '&lt;&lt;running&gt;&gt;';
+    await delay(5000); // let it run for 5 seconds
+
+    stopWork();
+
+    document.getElementById("out").innerHTML = '&lt;&lt;stopping&gt;&gt;';
+
+    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);
index 118c8d4..3a60727 100644 (file)
@@ -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 (file)
index 0000000..20ab2c8
--- /dev/null
@@ -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];
+
+
+
index 861824f..52663f7 100644 (file)
@@ -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 (file)
index 0000000..eee3f06
--- /dev/null
@@ -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;
index e317421..07c1db8 100644 (file)
@@ -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: {
index 229caf9..e370c2e 100644 (file)
@@ -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
+}
index b61b379..e524bb4 100644 (file)
@@ -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);
+    }
+}
+
index a7e065a..d26277e 100644 (file)
@@ -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;
 
index dde4798..6a9ea36 100644 (file)
@@ -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;