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 = '&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);
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;