From 69324d87089c666cae1df60ab67e1761fd2a34c2 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Mon, 13 Apr 2026 11:15:09 +0200 Subject: [PATCH 1/3] - EP typescript for CoreCLR - renamed SystemJS_ExecuteDiagnosticServerCallback, SystemJS_DiagnosticServerQueueJob --- src/mono/browser/runtime/cwraps.ts | 4 +- .../browser/runtime/diagnostics/common.ts | 2 +- .../runtime/diagnostics/diagnostics-ws.ts | 4 +- src/mono/browser/runtime/exports.ts | 2 +- src/mono/browser/runtime/types/internal.ts | 2 +- src/mono/mono/eventpipe/ep-rt-mono.h | 4 +- src/mono/mono/mini/mini-wasm.c | 2 +- src/mono/mono/utils/mono-threads-wasm.c | 10 +- src/mono/mono/utils/mono-threads-wasm.h | 2 +- src/mono/mono/utils/mono-threads.h | 2 +- .../libs/Common/JavaScript/CMakeLists.txt | 9 + .../Common/JavaScript/cross-module/index.ts | 6 + .../libs/Common/JavaScript/loader/dotnet.d.ts | 2 +- .../Common/JavaScript/types/ems-ambient.ts | 3 + .../libs/Common/JavaScript/types/exchange.ts | 14 + .../Common/JavaScript/types/public-api.ts | 2 +- .../libs/System.Native.Browser/CMakeLists.txt | 2 +- .../diagnostic_server_jobs.c | 54 ++++ .../diagnostic_server_jobs.h | 5 + .../diagnostics/client-commands.ts | 215 ++++++++++++++ .../diagnostics/common.ts | 50 ++++ .../diagnostics/diagnostic-server-js.ts | 169 +++++++++++ .../diagnostics/diagnostic-server-ws.ts | 63 +++++ .../diagnostics/diagnostic-server.ts | 98 +++++++ .../diagnostics/dotnet-counters.ts | 31 ++ .../diagnostics/dotnet-cpu-profiler.ts | 35 +++ .../diagnostics/dotnet-gcdump.ts | 41 +++ .../diagnostics/index.ts | 22 +- .../diagnostics/types.ts | 264 ++++++++++++++++++ .../libSystem.Native.Browser.footer.js | 1 + .../native/diagnostics.ts | 25 ++ .../System.Native.Browser/native/index.ts | 6 +- .../native/scheduling.ts | 26 ++ .../System.Native.Browser/utils/scheduling.ts | 6 + src/native/rollup.config.plugins.js | 6 +- 35 files changed, 1166 insertions(+), 23 deletions(-) create mode 100644 src/native/libs/System.Native.Browser/diagnostic_server_jobs.c create mode 100644 src/native/libs/System.Native.Browser/diagnostic_server_jobs.h create mode 100644 src/native/libs/System.Native.Browser/diagnostics/client-commands.ts create mode 100644 src/native/libs/System.Native.Browser/diagnostics/common.ts create mode 100644 src/native/libs/System.Native.Browser/diagnostics/diagnostic-server-js.ts create mode 100644 src/native/libs/System.Native.Browser/diagnostics/diagnostic-server-ws.ts create mode 100644 src/native/libs/System.Native.Browser/diagnostics/diagnostic-server.ts create mode 100644 src/native/libs/System.Native.Browser/diagnostics/dotnet-counters.ts create mode 100644 src/native/libs/System.Native.Browser/diagnostics/dotnet-cpu-profiler.ts create mode 100644 src/native/libs/System.Native.Browser/diagnostics/dotnet-gcdump.ts create mode 100644 src/native/libs/System.Native.Browser/native/diagnostics.ts diff --git a/src/mono/browser/runtime/cwraps.ts b/src/mono/browser/runtime/cwraps.ts index a910b442e8b81e..1acc3e00204907 100644 --- a/src/mono/browser/runtime/cwraps.ts +++ b/src/mono/browser/runtime/cwraps.ts @@ -41,7 +41,7 @@ const fn_signatures: SigLine[] = [ [true, "mono_wasm_parse_runtime_options", null, ["number", "number"]], [true, "mono_wasm_strdup", "number", ["string"]], [true, "mono_background_exec", null, []], - [true, "mono_wasm_ds_exec", null, []], + [true, "SystemJS_ExecuteDiagnosticServerCallback", null, []], [true, "mono_wasm_execute_timer", null, []], [true, "wasm_load_icu_data", "number", ["number"]], [false, "mono_wasm_add_assembly", "number", ["string", "number", "number"]], @@ -168,7 +168,7 @@ export interface t_Cwraps { mono_wasm_strdup(value: string): number; mono_wasm_parse_runtime_options(length: number, argv: VoidPtr): void; mono_background_exec(): void; - mono_wasm_ds_exec(): void; + SystemJS_ExecuteDiagnosticServerCallback(): void; mono_wasm_execute_timer(): void; wasm_load_icu_data(offset: VoidPtr): number; mono_wasm_add_assembly(name: string, data: VoidPtr, size: number): number; diff --git a/src/mono/browser/runtime/diagnostics/common.ts b/src/mono/browser/runtime/diagnostics/common.ts index d67e03e3204be3..58b62fa4924f24 100644 --- a/src/mono/browser/runtime/diagnostics/common.ts +++ b/src/mono/browser/runtime/diagnostics/common.ts @@ -16,7 +16,7 @@ export function diagnosticServerEventLoop () { if (loaderHelpers.is_runtime_running()) { try { runtimeHelpers.mono_background_exec();// give GC chance to run - runtimeHelpers.mono_wasm_ds_exec(); + runtimeHelpers.SystemJS_ExecuteDiagnosticServerCallback(); scheduleDiagnosticServerEventLoop(100); } catch (ex) { loaderHelpers.mono_exit(1, ex); diff --git a/src/mono/browser/runtime/diagnostics/diagnostics-ws.ts b/src/mono/browser/runtime/diagnostics/diagnostics-ws.ts index a5a18143c18c7b..f88eb388ad8a3b 100644 --- a/src/mono/browser/runtime/diagnostics/diagnostics-ws.ts +++ b/src/mono/browser/runtime/diagnostics/diagnostics-ws.ts @@ -27,7 +27,7 @@ class DiagnosticConnectionWS extends DiagnosticConnectionBase implements IDiagno }; ws.addEventListener("open", () => { for (const data of this.messagesToSend) { - ws.send(data); + ws.send(data as any); } this.messagesToSend = []; diagnosticServerEventLoop(); @@ -49,7 +49,7 @@ class DiagnosticConnectionWS extends DiagnosticConnectionBase implements IDiagno return super.store(message); } - this.ws!.send(message); + this.ws!.send(message as any); return message.length; } diff --git a/src/mono/browser/runtime/exports.ts b/src/mono/browser/runtime/exports.ts index 95d464a64b068a..7af4b8647b5bdc 100644 --- a/src/mono/browser/runtime/exports.ts +++ b/src/mono/browser/runtime/exports.ts @@ -44,7 +44,7 @@ function initializeExports (globalObjects: GlobalObjects): RuntimeAPI { utf8ToString, SystemJS_GetCurrentProcessId, mono_background_exec: () => tcwraps.mono_background_exec(), - mono_wasm_ds_exec: () => tcwraps.mono_wasm_ds_exec(), + SystemJS_ExecuteDiagnosticServerCallback: () => tcwraps.SystemJS_ExecuteDiagnosticServerCallback(), }; if (WasmEnableThreads) { rh.dumpThreads = mono_wasm_dump_threads; diff --git a/src/mono/browser/runtime/types/internal.ts b/src/mono/browser/runtime/types/internal.ts index 7f831e8bcde8d1..5412d6292ec6ad 100644 --- a/src/mono/browser/runtime/types/internal.ts +++ b/src/mono/browser/runtime/types/internal.ts @@ -240,7 +240,7 @@ export type RuntimeHelpers = { mono_wasm_print_thread_dump: () => void, utf8ToString: (ptr: CharPtr) => string, mono_background_exec: () => void, - mono_wasm_ds_exec: () => void, + SystemJS_ExecuteDiagnosticServerCallback: () => void, SystemJS_GetCurrentProcessId: () => number, } diff --git a/src/mono/mono/eventpipe/ep-rt-mono.h b/src/mono/mono/eventpipe/ep-rt-mono.h index 4da928f956a79f..ce5c7f7e95af96 100644 --- a/src/mono/mono/eventpipe/ep-rt-mono.h +++ b/src/mono/mono/eventpipe/ep-rt-mono.h @@ -1011,7 +1011,7 @@ ep_rt_queue_job ( // see if it's done or needs to be scheduled again if (!done) { // self schedule again - mono_schedule_ds_job (cb, params); + SystemJS_DiagnosticServerQueueJob (cb, params); } return true; @@ -1045,7 +1045,7 @@ ep_rt_thread_sleep (uint64_t ns) g_usleep ((gulong)(ns / 1000)); MONO_EXIT_GC_SAFE; } -#endif +#endif // PERFTRACING_DISABLE_THREADS } static diff --git a/src/mono/mono/mini/mini-wasm.c b/src/mono/mono/mini/mini-wasm.c index b27a9835c4d3c2..d66f918537c06c 100644 --- a/src/mono/mono/mini/mini-wasm.c +++ b/src/mono/mono/mini/mini-wasm.c @@ -453,7 +453,7 @@ G_BEGIN_DECLS #ifdef DISABLE_THREADS EMSCRIPTEN_KEEPALIVE void mono_wasm_execute_timer (void); EMSCRIPTEN_KEEPALIVE void mono_background_exec (void); -EMSCRIPTEN_KEEPALIVE void mono_wasm_ds_exec (void); +EMSCRIPTEN_KEEPALIVE void SystemJS_ExecuteDiagnosticServerCallback (void); extern void SystemJS_ScheduleTimerImpl (int shortestDueTimeMs); #else extern void SystemJS_ScheduleSynchronizationContext(MonoNativeThreadId target_thread); diff --git a/src/mono/mono/utils/mono-threads-wasm.c b/src/mono/mono/utils/mono-threads-wasm.c index 88ae9557381000..6b43c1155bc821 100644 --- a/src/mono/mono/utils/mono-threads-wasm.c +++ b/src/mono/mono/utils/mono-threads-wasm.c @@ -342,7 +342,7 @@ typedef struct { } DsJobRegistration; void -mono_schedule_ds_job (ds_job_cb cb, void* data) +SystemJS_DiagnosticServerQueueJob (ds_job_cb cb, void* data) { g_assert (cb); DsJobRegistration* reg = g_new0 (DsJobRegistration, 1); @@ -372,7 +372,7 @@ mono_background_exec (void) G_EXTERN_C EMSCRIPTEN_KEEPALIVE void -mono_wasm_ds_exec (void) +SystemJS_ExecuteDiagnosticServerCallback (void) { MONO_ENTER_GC_UNSAFE; GSList *j1 = jobs_ds, *cur1; @@ -381,13 +381,13 @@ mono_wasm_ds_exec (void) for (cur1 = j1; cur1; cur1 = cur1->next) { DsJobRegistration* reg = (DsJobRegistration*)cur1->data; g_assert (reg->cb); - THREADS_DEBUG ("mono_wasm_ds_exec running job %p \n", (gpointer)cb); + THREADS_DEBUG ("SystemJS_ExecuteDiagnosticServerCallback running job %p \n", (gpointer)cb); gsize done = reg->cb (reg->data); if (done){ - THREADS_DEBUG ("mono_wasm_ds_exec done job %p \n", (gpointer)cb); + THREADS_DEBUG ("SystemJS_ExecuteDiagnosticServerCallback done job %p \n", (gpointer)cb); g_free (reg); } else { - THREADS_DEBUG ("mono_wasm_ds_exec scheduling job %p again \n", (gpointer)cb); + THREADS_DEBUG ("SystemJS_ExecuteDiagnosticServerCallback scheduling job %p again \n", (gpointer)cb); jobs_ds = g_slist_prepend (jobs_ds, (gpointer)reg); } } diff --git a/src/mono/mono/utils/mono-threads-wasm.h b/src/mono/mono/utils/mono-threads-wasm.h index 64fbd11a20ba5f..e8a06f5f1834e8 100644 --- a/src/mono/mono/utils/mono-threads-wasm.h +++ b/src/mono/mono/utils/mono-threads-wasm.h @@ -89,7 +89,7 @@ mono_wasm_atomic_wait_i32 (volatile int32_t *addr, int32_t expected, int32_t tim #else /* DISABLE_THREADS */ void mono_background_exec (void); -void mono_wasm_ds_exec (void); +void SystemJS_ExecuteDiagnosticServerCallback (void); #endif /* DISABLE_THREADS */ void diff --git a/src/mono/mono/utils/mono-threads.h b/src/mono/mono/utils/mono-threads.h index 0cd83701898047..1adfba5e5f7ad6 100644 --- a/src/mono/mono/utils/mono-threads.h +++ b/src/mono/mono/utils/mono-threads.h @@ -848,7 +848,7 @@ typedef void (*background_job_cb)(void); typedef gsize (*ds_job_cb)(void* data); #ifdef DISABLE_THREADS void SystemJS_ScheduleBackgroundJob (background_job_cb cb); -void mono_schedule_ds_job (ds_job_cb cb, void* data); +void SystemJS_DiagnosticServerQueueJob (ds_job_cb cb, void* data); #else void SystemJS_ScheduleSynchronizationContext(MonoNativeThreadId target_thread); #endif // DISABLE_THREADS diff --git a/src/native/libs/Common/JavaScript/CMakeLists.txt b/src/native/libs/Common/JavaScript/CMakeLists.txt index debd4adad78ffc..89d503c08561e1 100644 --- a/src/native/libs/Common/JavaScript/CMakeLists.txt +++ b/src/native/libs/Common/JavaScript/CMakeLists.txt @@ -58,14 +58,23 @@ set(ROLLUP_TS_SOURCES "${CLR_SRC_NATIVE_DIR}/libs/Common/JavaScript/types/public-api.ts" "${CLR_SRC_NATIVE_DIR}/libs/Common/JavaScript/types/v8.d.ts" + "${CLR_SRC_NATIVE_DIR}/libs/System.Native.Browser/diagnostics/client-commands.ts" + "${CLR_SRC_NATIVE_DIR}/libs/System.Native.Browser/diagnostics/common.ts" "${CLR_SRC_NATIVE_DIR}/libs/System.Native.Browser/diagnostics/console-proxy.ts" "${CLR_SRC_NATIVE_DIR}/libs/System.Native.Browser/diagnostics/cross-module.ts" + "${CLR_SRC_NATIVE_DIR}/libs/System.Native.Browser/diagnostics/diagnostic-server.ts" + "${CLR_SRC_NATIVE_DIR}/libs/System.Native.Browser/diagnostics/diagnostic-server-js.ts" + "${CLR_SRC_NATIVE_DIR}/libs/System.Native.Browser/diagnostics/diagnostic-server-ws.ts" + "${CLR_SRC_NATIVE_DIR}/libs/System.Native.Browser/diagnostics/dotnet-counters.ts" + "${CLR_SRC_NATIVE_DIR}/libs/System.Native.Browser/diagnostics/dotnet-cpu-profiler.ts" + "${CLR_SRC_NATIVE_DIR}/libs/System.Native.Browser/diagnostics/dotnet-gcdump.ts" "${CLR_SRC_NATIVE_DIR}/libs/System.Native.Browser/diagnostics/exit.ts" "${CLR_SRC_NATIVE_DIR}/libs/System.Native.Browser/diagnostics/index.ts" "${CLR_SRC_NATIVE_DIR}/libs/System.Native.Browser/diagnostics/per-module.ts" "${CLR_SRC_NATIVE_DIR}/libs/System.Native.Browser/diagnostics/symbolicate.ts" "${CLR_SRC_NATIVE_DIR}/libs/System.Native.Browser/diagnostics/types.ts" "${CLR_SRC_NATIVE_DIR}/libs/System.Native.Browser/native/crypto.ts" + "${CLR_SRC_NATIVE_DIR}/libs/System.Native.Browser/native/diagnostics.ts" "${CLR_SRC_NATIVE_DIR}/libs/System.Native.Browser/native/globalization-locale.ts" "${CLR_SRC_NATIVE_DIR}/libs/System.Native.Browser/native/index.ts" "${CLR_SRC_NATIVE_DIR}/libs/System.Native.Browser/native/main.ts" diff --git a/src/native/libs/Common/JavaScript/cross-module/index.ts b/src/native/libs/Common/JavaScript/cross-module/index.ts index 613fa6f09d49df..27e4bf51b4e0c4 100644 --- a/src/native/libs/Common/JavaScript/cross-module/index.ts +++ b/src/native/libs/Common/JavaScript/cross-module/index.ts @@ -177,6 +177,7 @@ export function dotnetUpdateInternalsSubscriber() { const interopLocal: NativeBrowserExports = { getWasmMemory: table[0], getWasmTable: table[1], + SystemJS_ScheduleDiagnosticServer: table[2], }; Object.assign(interop, interopLocal); } @@ -186,6 +187,11 @@ export function dotnetUpdateInternalsSubscriber() { const interopLocal: DiagnosticsExports = { symbolicateStackTrace: table[0], installNativeSymbols: table[1], + ds_rt_websocket_create: table[2], + ds_rt_websocket_send: table[3], + ds_rt_websocket_poll: table[4], + ds_rt_websocket_recv: table[5], + ds_rt_websocket_close: table[6], }; Object.assign(interop, interopLocal); } diff --git a/src/native/libs/Common/JavaScript/loader/dotnet.d.ts b/src/native/libs/Common/JavaScript/loader/dotnet.d.ts index 1468d116b94289..869762963c06b2 100644 --- a/src/native/libs/Common/JavaScript/loader/dotnet.d.ts +++ b/src/native/libs/Common/JavaScript/loader/dotnet.d.ts @@ -688,7 +688,7 @@ type DiagnosticsAPIType = { type DiagnosticCommandProviderV2 = { keywords: [number, number]; logLevel: number; - provider_name: string; + providerName: string; arguments: string | null; }; type DiagnosticCommandOptions = { diff --git a/src/native/libs/Common/JavaScript/types/ems-ambient.ts b/src/native/libs/Common/JavaScript/types/ems-ambient.ts index 68a147b9fbd22c..0f394656c9c632 100644 --- a/src/native/libs/Common/JavaScript/types/ems-ambient.ts +++ b/src/native/libs/Common/JavaScript/types/ems-ambient.ts @@ -28,6 +28,8 @@ export type EmsAmbientSymbolsType = EmscriptenModuleInternal & { _SystemJS_ExecuteTimerCallback: () => void; _SystemJS_ExecuteBackgroundJobCallback: () => void; _SystemJS_ExecuteFinalizationCallback: () => void; + _SystemJS_ExecuteDiagnosticServerCallback: () => void; + _SystemJS_ScheduleDiagnosticServer: () => void; _BrowserHost_CreateHostContract: () => VoidPtr; _BrowserHost_InitializeDotnet: (propertiesCount: number, propertyKeys: CharPtrPtr, propertyValues: CharPtrPtr) => number; _BrowserHost_ExecuteAssembly: (mainAssemblyNamePtr: number, argsLength: number, argsPtr: number) => number; @@ -51,6 +53,7 @@ export type EmsAmbientSymbolsType = EmscriptenModuleInternal & { lastScheduledTimerId?: number; lastScheduledThreadPoolId?: number; lastScheduledFinalizationId?: number; + lastScheduledDiagnosticServerId?: number; cryptoWarnOnce?: boolean; isAborting?: boolean; isAsyncMain?: boolean; diff --git a/src/native/libs/Common/JavaScript/types/exchange.ts b/src/native/libs/Common/JavaScript/types/exchange.ts index 22c98a6d294fee..4da02ce6917023 100644 --- a/src/native/libs/Common/JavaScript/types/exchange.ts +++ b/src/native/libs/Common/JavaScript/types/exchange.ts @@ -24,6 +24,8 @@ import type { cancelPromise } from "../../../System.Runtime.InteropServices.Java import type { abortInteropTimers } from "../../../System.Runtime.InteropServices.JavaScript.Native/interop/scheduling"; import type { installNativeSymbols, symbolicateStackTrace } from "../../../System.Native.Browser/diagnostics/symbolicate"; +import type { SystemJS_ScheduleDiagnosticServer } from "../../../System.Native.Browser/native"; +import type { ds_rt_websocket_close, ds_rt_websocket_create, ds_rt_websocket_poll, ds_rt_websocket_recv, ds_rt_websocket_send } from "../../../System.Native.Browser/diagnostics/diagnostic-server"; type getWasmMemoryType = () => WebAssembly.Memory; @@ -144,11 +146,13 @@ export type InteropJavaScriptExportsTable = [ export type NativeBrowserExports = { getWasmMemory: getWasmMemoryType, getWasmTable: getWasmTableType, + SystemJS_ScheduleDiagnosticServer: typeof SystemJS_ScheduleDiagnosticServer, } export type NativeBrowserExportsTable = [ getWasmMemoryType, getWasmTableType, + typeof SystemJS_ScheduleDiagnosticServer, ] export type BrowserUtilsExports = { @@ -186,9 +190,19 @@ export type BrowserUtilsExportsTable = [ export type DiagnosticsExportsTable = [ typeof symbolicateStackTrace, typeof installNativeSymbols, + typeof ds_rt_websocket_create, + typeof ds_rt_websocket_send, + typeof ds_rt_websocket_poll, + typeof ds_rt_websocket_recv, + typeof ds_rt_websocket_close, ] export type DiagnosticsExports = { symbolicateStackTrace: typeof symbolicateStackTrace, installNativeSymbols: typeof installNativeSymbols, + ds_rt_websocket_create: typeof ds_rt_websocket_create, + ds_rt_websocket_send: typeof ds_rt_websocket_send, + ds_rt_websocket_poll: typeof ds_rt_websocket_poll, + ds_rt_websocket_recv: typeof ds_rt_websocket_recv, + ds_rt_websocket_close: typeof ds_rt_websocket_close, } diff --git a/src/native/libs/Common/JavaScript/types/public-api.ts b/src/native/libs/Common/JavaScript/types/public-api.ts index e1c635a2b5897d..eca35c36b9fa06 100644 --- a/src/native/libs/Common/JavaScript/types/public-api.ts +++ b/src/native/libs/Common/JavaScript/types/public-api.ts @@ -658,7 +658,7 @@ export type DiagnosticsAPIType = { export type DiagnosticCommandProviderV2 = { keywords: [number, number]; logLevel: number; - provider_name: string; + providerName: string; arguments: string | null; }; diff --git a/src/native/libs/System.Native.Browser/CMakeLists.txt b/src/native/libs/System.Native.Browser/CMakeLists.txt index 909e70230b212b..1d060f0b25a350 100644 --- a/src/native/libs/System.Native.Browser/CMakeLists.txt +++ b/src/native/libs/System.Native.Browser/CMakeLists.txt @@ -1,6 +1,6 @@ project(System.Native.Browser C) -set(BROWSER_SOURCES ${BROWSER_SOURCES} entrypoints.c) +set(BROWSER_SOURCES ${BROWSER_SOURCES} entrypoints.c diagnostic_server_jobs.c) add_library(System.Native.Browser-Static STATIC diff --git a/src/native/libs/System.Native.Browser/diagnostic_server_jobs.c b/src/native/libs/System.Native.Browser/diagnostic_server_jobs.c new file mode 100644 index 00000000000000..70c4e6e70d6511 --- /dev/null +++ b/src/native/libs/System.Native.Browser/diagnostic_server_jobs.c @@ -0,0 +1,54 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#include +#include +#include +#include + +typedef size_t (*ds_job_cb)(void *data); + +extern void SystemJS_ScheduleDiagnosticServer(void); + +typedef struct DsJobNode { + ds_job_cb cb; + void *data; + struct DsJobNode *next; +} DsJobNode; + +static DsJobNode *jobs; + +void +SystemJS_DiagnosticServerQueueJob (ds_job_cb cb, void *data) +{ + int wasEmpty = jobs == NULL; + assert (cb); + DsJobNode *node = (DsJobNode *)calloc (1, sizeof (DsJobNode)); + node->cb = cb; + node->data = data; + node->next = jobs; + jobs = node; + if (wasEmpty) { + SystemJS_ScheduleDiagnosticServer (); + } +} + +void +SystemJS_ExecuteDiagnosticServerCallback (void) +{ + DsJobNode *list = jobs; + jobs = NULL; + + while (list) { + DsJobNode *cur = list; + list = cur->next; + assert (cur->cb); + size_t done = cur->cb (cur->data); + if (done) { + free (cur); + } else { + cur->next = jobs; + jobs = cur; + } + } +} diff --git a/src/native/libs/System.Native.Browser/diagnostic_server_jobs.h b/src/native/libs/System.Native.Browser/diagnostic_server_jobs.h new file mode 100644 index 00000000000000..52d05082713974 --- /dev/null +++ b/src/native/libs/System.Native.Browser/diagnostic_server_jobs.h @@ -0,0 +1,5 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma once + diff --git a/src/native/libs/System.Native.Browser/diagnostics/client-commands.ts b/src/native/libs/System.Native.Browser/diagnostics/client-commands.ts new file mode 100644 index 00000000000000..1746349005d4d6 --- /dev/null +++ b/src/native/libs/System.Native.Browser/diagnostics/client-commands.ts @@ -0,0 +1,215 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import type { DiagnosticCommandOptions } from "../types"; +import { CommandSetId, EventPipeCommandId, Keywords, PayloadV2, ProcessCommandId, ServerCommandId, SessionId } from "./types"; + + +// ADVR_V1\0 +export const advert1 = [65, 68, 86, 82, 95, 86, 49, 0,]; +// DOTNET_IPC_V1\0 +export const dotnetIpcV1 = [68, 79, 84, 78, 69, 84, 95, 73, 80, 67, 95, 86, 49, 0]; + +// this file contains the IPC commands that are sent by client (like dotnet-trace) to the diagnostic server (like Mono VM in the browser) +// just formatting bytes, no sessions management here + + +export function advertise() { + // xxxxxxxx-xxxx-4xxx-xxxx-xxxxxxxxxxxx + const uuid = new Uint8Array(16); + globalThis.crypto.getRandomValues(uuid); + uuid[7] = (uuid[7] & 0xf) | 0x40;// version 4 + + const pid = 42; + + return Uint8Array.from([ + ...advert1, + ...uuid, + ...serializeUint64([0, pid]), + 0, 0// future + ]); +} + +export function commandStopTracing(sessionID: SessionId) { + return Uint8Array.from([ + ...serializeHeader(CommandSetId.EventPipe, EventPipeCommandId.StopTracing, computeMessageByteLength(8)), + ...serializeUint64(sessionID), + ]); +} + +export function commandResumeRuntime() { + return Uint8Array.from([ + ...serializeHeader(CommandSetId.Process, ProcessCommandId.ResumeRuntime, computeMessageByteLength(0)), + ]); +} + +export function commandProcessInfo3() { + return Uint8Array.from([ + ...serializeHeader(CommandSetId.Process, ProcessCommandId.ProcessInfo3, computeMessageByteLength(0)), + ]); +} + +export function commandGcHeapDump(options: DiagnosticCommandOptions) { + return commandCollectTracing2({ + circularBufferMB: options.circularBufferMB ?? 256, + format: 1, + requestRundown: true, + providers: [ + { + keywords: [ + 0x0000_0000, + Keywords.GCHeapSnapshot, // 0x1980001 + // GC_HEAP_DUMP_VTABLE_CLASS_REF_KEYWORD 0x8000000 + // GC_FINALIZATION_KEYWORD 0x1000000 + // GC_HEAP_COLLECT_KEYWORD 0x0800000 + // GC_KEYWORD 0x0000001 + ], + logLevel: 5, + providerName: "Microsoft-Windows-DotNETRuntime", + arguments: null + }, + ...options.extraProviders || [], + ] + }); +} + +function uuidv4() { + return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, c => + (+c ^ globalThis.crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> +c / 4).toString(16) + ); +} + +export function commandCounters(options: DiagnosticCommandOptions) { + return commandCollectTracing2({ + circularBufferMB: options.circularBufferMB ?? 256, + format: 1, + requestRundown: false, + providers: [ + { + keywords: [0, Keywords.GCHandle], + logLevel: 4, + providerName: "System.Diagnostics.Metrics", + arguments: `SessionId=SHARED;Metrics=System.Runtime;RefreshInterval=${options.intervalSeconds || 1};MaxTimeSeries=1000;MaxHistograms=10;ClientId=${uuidv4()};`, + }, + ...options.extraProviders || [], + ] + }); +} + +export function commandSampleProfiler(options: DiagnosticCommandOptions) { + return commandCollectTracing2({ + circularBufferMB: options.circularBufferMB ?? 256, + format: 1, + requestRundown: true, + providers: [ + { + keywords: [ + 0x0000_0000, + 0x0000_0000, + ], + logLevel: 4, + providerName: "Microsoft-DotNETCore-SampleProfiler", + arguments: null + }, + ...options.extraProviders || [], + ] + }); +} + +function commandCollectTracing2(payload2: PayloadV2) { + const payloadLength = computeCollectTracing2PayloadByteLength(payload2); + const messageLength = computeMessageByteLength(payloadLength); + const message = [ + ...serializeHeader(CommandSetId.EventPipe, EventPipeCommandId.CollectTracing2, messageLength), + ...serializeUint32(payload2.circularBufferMB), + ...serializeUint32(payload2.format), + ...serializeUint8(payload2.requestRundown ? 1 : 0), + ...serializeUint32(payload2.providers.length), + ]; + for (const provider of payload2.providers) { + message.push(...serializeUint64(provider.keywords)); + message.push(...serializeUint32(provider.logLevel)); + message.push(...serializeString(provider.providerName)); + message.push(...serializeString(provider.arguments)); + } + return Uint8Array.from(message); +} + + +function serializeMagic() { + return Uint8Array.from(dotnetIpcV1); +} + +function serializeUint8(value: number) { + return Uint8Array.from([value]); +} + +function serializeUint16(value: number) { + return new Uint8Array(Uint16Array.from([value]).buffer); +} + +function serializeUint32(value: number) { + return new Uint8Array(Uint32Array.from([value]).buffer); +} + +function serializeUint64(value: [number, number]) { + // value == [hi, lo] + return new Uint8Array(Uint32Array.from([value[1], value[0]]).buffer); +} + +function serializeString(value: string | null) { + const message = []; + if (value === null || value === undefined || value === "") { + message.push(...serializeUint32(1)); + message.push(...serializeUint16(0)); + } else { + const len = value.length; + const hasNul = value[len - 1] === "\0"; + message.push(...serializeUint32(len + (hasNul ? 0 : 1))); + for (let i = 0; i < len; i++) { + message.push(...serializeUint16(value.charCodeAt(i))); + } + if (!hasNul) { + message.push(...serializeUint16(0)); + } + } + return message; +} + +function computeStringByteLength(s: string | null) { + if (s === undefined || s === null || s === "") + return 4 + 2; // just length of empty zero terminated string + return 4 + 2 * s.length + 2; // length + UTF16 + null +} + +function computeMessageByteLength(payloadLength: number) { + const fullHeaderSize = 14 + 2 // magic, len + + 1 + 1 // commandSet, command + + 2; // reserved ; + return fullHeaderSize + payloadLength; +} + +function serializeHeader(commandSet: CommandSetId, command: ServerCommandId | EventPipeCommandId | ProcessCommandId, len: number) { + return Uint8Array.from([ + ...serializeMagic(), + ...serializeUint16(len), + ...serializeUint8(commandSet), + ...serializeUint8(command), + ...serializeUint16(0), // reserved*/ + ]); +} + +function computeCollectTracing2PayloadByteLength(payload2: PayloadV2) { + let len = 0; + len += 4; // circularBufferMB + len += 4; // format + len += 1; // requestRundown + len += 4; // providers length + for (const provider of payload2.providers) { + len += 8; // keywords + len += 4; // level + len += computeStringByteLength(provider.providerName); + len += computeStringByteLength(provider.arguments); + } + return len; +} diff --git a/src/native/libs/System.Native.Browser/diagnostics/common.ts b/src/native/libs/System.Native.Browser/diagnostics/common.ts new file mode 100644 index 00000000000000..096929bf464b11 --- /dev/null +++ b/src/native/libs/System.Native.Browser/diagnostics/common.ts @@ -0,0 +1,50 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import type { VoidPtr } from "../../Common/JavaScript/types/emscripten"; +import { dotnetApi, dotnetLogger } from "./cross-module"; + +export class DiagnosticConnectionBase { + protected messagesToSend: Uint8Array[] = []; + protected messagesReceived: Uint8Array[] = []; + constructor(public clientSocket: number) { + } + + store(message: Uint8Array): number { + this.messagesToSend.push(message); + return message.byteLength; + } + + poll(): number { + return this.messagesReceived.length; + } + + recv(buffer: VoidPtr, bytesToRead: number): number { + if (this.messagesReceived.length === 0) { + return 0; + } + const message = this.messagesReceived[0]!; + const bytesRead = Math.min(message.length, bytesToRead); + const view = dotnetApi.localHeapViewU8(); + view.set(message.subarray(0, bytesRead), buffer as any >>> 0); + if (bytesRead === message.length) { + this.messagesReceived.shift(); + } else { + this.messagesReceived[0] = message.subarray(bytesRead); + } + return bytesRead; + } +} + +export function downloadBlob(messages: Uint8Array[]) { + const blob = new Blob(messages as BlobPart[], { type: "application/octet-stream" }); + const blobUrl = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.download = "trace." + (new Date()).valueOf() + ".nettrace"; + dotnetLogger.info(`Downloading trace ${link.download} - ${blob.size} bytes`); + link.href = blobUrl; + document.body.appendChild(link); + link.dispatchEvent(new MouseEvent("click", { + bubbles: true, cancelable: true, view: window + })); +} diff --git a/src/native/libs/System.Native.Browser/diagnostics/diagnostic-server-js.ts b/src/native/libs/System.Native.Browser/diagnostics/diagnostic-server-js.ts new file mode 100644 index 00000000000000..e2765157a711a0 --- /dev/null +++ b/src/native/libs/System.Native.Browser/diagnostics/diagnostic-server-js.ts @@ -0,0 +1,169 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import { advert1, dotnetIpcV1 } from "./client-commands"; +import { CommandSetId, FnClientProvider, IDiagnosticClient, IDiagnosticConnection, IDiagnosticSession, PromiseCompletionSource, ServerCommandId, SessionId } from "./types"; +import { DiagnosticConnectionBase, downloadBlob } from "./common"; +import { dotnetLoaderExports, dotnetLogger, dotnetNativeBrowserExports } from "./cross-module"; +import { collectGcDump } from "./dotnet-gcdump"; +import { collectMetrics } from "./dotnet-counters"; +import { collectCpuSamples } from "./dotnet-cpu-profiler"; + +//let diagClient:IDiagClient|undefined = undefined as any; +//let server:DiagServer = undefined as any; + +// configure your application +// .withEnvironmentVariable("DOTNET_DiagnosticPorts", "download:gcdump") +// or implement function globalThis.dotnetDiagnosticClient with IDiagClient interface + +let nextJsClient: PromiseCompletionSource; +let fromScenarioNameOnce = false; + +// Only the last which sent advert is receiving commands for all sessions +export let serverSession: DiagnosticSession | undefined = undefined; + +// singleton wrapping the protocol with the diagnostic server in the Mono VM +// there could be multiple connection at the same time. +// DS:advert ->1 +// 1<- DC1: command to start tracing session +// DS:OK, session ID ->1 +// DS:advert ->2 +// DS:events ->1 +// DS:events ->1 +// DS:events ->1 +// DS:events ->1 +// 2<- DC1: command to stop tracing session +// DS:close ->1 + +class DiagnosticSession extends DiagnosticConnectionBase implements IDiagnosticConnection, IDiagnosticSession { + public sessionId: SessionId = undefined as any; + public diagClient?: IDiagnosticClient; + public stopDelayedAfterLastMessage: number | undefined = undefined; + public resumedRuntime = false; + + constructor(public clientSocket: number) { + super(clientSocket); + } + + sendCommand(message: Uint8Array): void { + if (!serverSession) { + dotnetLogger.warn("no server yet"); + return; + } + serverSession.respond(message); + } + + async connectNewClient() { + this.diagClient = await nextJsClient.promise; + initializeJsClient(); + const firstCommand = this.diagClient.commandOnAdvertise(); + this.respond(firstCommand); + } + + isAdvertMessage(message: Uint8Array): boolean { + return advert1.every((v, i) => v === message[i]); + } + + isResponseMessage(message: Uint8Array): boolean { + return dotnetIpcV1.every((v, i) => v === message[i]) && message[16] == CommandSetId.Server; + } + + isResponseOkWithSession(message: Uint8Array): boolean { + return message.byteLength === 28 && message[17] == ServerCommandId.OK; + } + + parseSessionId(message: Uint8Array): SessionId { + const view = message.subarray(20, 28); + const sessionIDLo = view[0] | (view[1] << 8) | (view[2] << 16) | (view[3] << 24); + const sessionIDHi = view[4] | (view[5] << 8) | (view[6] << 16) | (view[7] << 24); + return [sessionIDHi, sessionIDLo] as SessionId; + } + + // this is message from the diagnostic server, which is Mono VM in this browser + send(message: Uint8Array): number { + dotnetNativeBrowserExports.SystemJS_ScheduleDiagnosticServer(); + if (this.isAdvertMessage(message)) { + // eslint-disable-next-line @typescript-eslint/no-this-alias + serverSession = this; + this.connectNewClient(); + } else if (this.isResponseMessage(message)) { + if (this.isResponseOkWithSession(message)) { + this.sessionId = this.parseSessionId(message); + if (this.diagClient?.onSessionStart) { + this.diagClient.onSessionStart(this); + } + } else { + if (this.diagClient?.onError) { + this.diagClient.onError(this, message); + } else { + dotnetLogger.warn("Diagnostic session " + this.sessionId + " error : " + message.toString()); + } + } + } else { + if (this.diagClient?.onData) + this.diagClient.onData(this, message); + else { + this.store(message); + } + } + + return message.length; + } + + // this is message to the diagnostic server, which is Mono VM in this browser + respond(message: Uint8Array): void { + this.messagesReceived.push(message); + dotnetNativeBrowserExports.SystemJS_ScheduleDiagnosticServer(); + } + + close(): number { + if (this.diagClient?.onClose) { + this.diagClient.onClose(this.messagesToSend); + } + if (this.diagClient?.onClosePromise) { + this.diagClient.onClosePromise.resolve(this.messagesToSend); + } + if (this.messagesToSend.length === 0) { + return 0; + } + if (this.diagClient && !this.diagClient.skipDownload) { + downloadBlob(this.messagesToSend); + } + this.messagesToSend = []; + return 0; + } +} + +export function initializeJsClient() { + nextJsClient = dotnetLoaderExports.createPromiseCompletionSource(); +} + +export function setupJsClient(client: IDiagnosticClient) { + if (!dotnetLoaderExports.isRuntimeRunning()) { + throw new Error("Runtime is not running"); + } + if (nextJsClient.isDone) { + throw new Error("multiple clients in parallel are not allowed"); + } + nextJsClient.resolve(client); +} + +export function createDiagConnectionJs(socketHandle: number, scenarioName: string): DiagnosticSession { + if (!fromScenarioNameOnce) { + fromScenarioNameOnce = true; + if (scenarioName.startsWith("js://gcdump")) { + collectGcDump({}); + } + if (scenarioName.startsWith("js://counters")) { + collectMetrics({}); + } + if (scenarioName.startsWith("js://cpu-samples")) { + collectCpuSamples({}); + } + const dotnetDiagnosticClient: FnClientProvider = (globalThis as any).dotnetDiagnosticClient; + if (typeof dotnetDiagnosticClient === "function") { + nextJsClient.resolve(dotnetDiagnosticClient(scenarioName)); + } + } + return new DiagnosticSession(socketHandle); +} diff --git a/src/native/libs/System.Native.Browser/diagnostics/diagnostic-server-ws.ts b/src/native/libs/System.Native.Browser/diagnostics/diagnostic-server-ws.ts new file mode 100644 index 00000000000000..ce5891366e9ff0 --- /dev/null +++ b/src/native/libs/System.Native.Browser/diagnostics/diagnostic-server-ws.ts @@ -0,0 +1,63 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import { DiagnosticConnectionBase } from "./common"; +import { dotnetBrowserUtilsExports, dotnetLogger, dotnetNativeBrowserExports } from "./cross-module"; +import { IDiagnosticConnection } from "./types"; + +export function createDiagConnectionWs(socketHandle: number, url: string): IDiagnosticConnection { + return new DiagnosticConnectionWS(socketHandle, url); +} + +// this is used together with `dotnet-dsrouter` which will create IPC pipe on your local machine +// 1. run `dotnet-dsrouter server-websocket` this will print process ID and websocket URL +// 2. configure your wasm dotnet application `.withEnvironmentVariable("DOTNET_DiagnosticPorts", "ws://127.0.0.1:8088/diagnostics")` +// 3. run your wasm application +// 4. run `dotnet-gcdump -p ` or `dotnet-trace collect -p ` +class DiagnosticConnectionWS extends DiagnosticConnectionBase implements IDiagnosticConnection { + private ws: WebSocket; + + constructor(clientSocket: number, url: string) { + super(clientSocket); + const ws = this.ws = new WebSocket(url); + const onMessage = async (evt: MessageEvent) => { + const buffer = await evt.data.arrayBuffer(); + const message = new Uint8Array(buffer); + this.messagesReceived.push(message); + dotnetBrowserUtilsExports.runBackgroundTimers(); + }; + ws.addEventListener("open", () => { + for (const data of this.messagesToSend) { + ws.send(data as any); + } + this.messagesToSend = []; + dotnetBrowserUtilsExports.runBackgroundTimers(); + }, { once: true }); + ws.addEventListener("message", onMessage); + ws.addEventListener("error", () => { + dotnetLogger.warn("Diagnostic server WebSocket connection was closed unexpectedly."); + ws.removeEventListener("message", onMessage); + }, { once: true }); + } + + send(message: Uint8Array): number { + dotnetNativeBrowserExports.SystemJS_ScheduleDiagnosticServer(); + // copy the message + if (this.ws!.readyState == WebSocket.CLOSED || this.ws!.readyState == WebSocket.CLOSING) { + return -1; + } + if (this.ws!.readyState == WebSocket.CONNECTING) { + return super.store(message); + } + + this.ws!.send(message as any); + + return message.length; + } + + close(): number { + dotnetNativeBrowserExports.SystemJS_ScheduleDiagnosticServer(); + this.ws.close(); + return 0; + } +} diff --git a/src/native/libs/System.Native.Browser/diagnostics/diagnostic-server.ts b/src/native/libs/System.Native.Browser/diagnostics/diagnostic-server.ts new file mode 100644 index 00000000000000..3fbb633140b22e --- /dev/null +++ b/src/native/libs/System.Native.Browser/diagnostics/diagnostic-server.ts @@ -0,0 +1,98 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import type { CharPtr, VoidPtr } from "../../Common/JavaScript/types/emscripten"; + +import { createDiagConnectionJs, initializeJsClient, serverSession } from "./diagnostic-server-js"; +import { createDiagConnectionWs } from "./diagnostic-server-ws"; +import { advertise } from "./client-commands"; +import { IDiagnosticConnection } from "./types"; +import { dotnetApi, dotnetBrowserUtilsExports, Module } from "./cross-module"; + +let socketHandles: Map = undefined as any; +let nextSocketHandle = 1; +let urlOverride: string | undefined = undefined; + +export function ds_rt_websocket_create(urlPtr: CharPtr): number { + if (!socketHandles) { + socketHandles = new Map(); + } + let url; + if (!urlOverride) { + const heapU8 = dotnetApi.localHeapViewU8(); + const bufferPtr = urlPtr as any >>> 0; + const buff = heapU8.subarray(bufferPtr, Math.min(bufferPtr + 1000, heapU8.length)); + url = dotnetBrowserUtilsExports.utf8ToStringRelaxed(buff); + } else { + url = urlOverride; + } + + Module.UTF8ToString(urlPtr); + const socketHandle = nextSocketHandle++; + const isWebSocket = url.startsWith("ws://") || url.startsWith("wss://"); + const wrapper = isWebSocket + ? createDiagConnectionWs(socketHandle, url) + : createDiagConnectionJs(socketHandle, url); + socketHandles.set(socketHandle, wrapper); + return socketHandle; +} + +export function ds_rt_websocket_send(clientSocket: number, buffer: VoidPtr, bytesToWrite: number): number { + const wrapper = socketHandles.get(clientSocket); + if (!wrapper) { + return -1; + } + const view = dotnetApi.localHeapViewU8(); + const bufferPtr = buffer as any >>> 0; + const message = view.slice(bufferPtr, bufferPtr + bytesToWrite); + return wrapper.send(message); +} + +export function ds_rt_websocket_poll(clientSocket: number): number { + const wrapper = socketHandles.get(clientSocket); + if (!wrapper) { + return 0; + } + return wrapper.poll(); +} + +export function ds_rt_websocket_recv(clientSocket: number, buffer: VoidPtr, bytesToRead: number): number { + const wrapper = socketHandles.get(clientSocket); + if (!wrapper) { + return -1; + } + const bufferPtr: VoidPtr = buffer as any >>> 0 as any; + return wrapper.recv(bufferPtr, bytesToRead); +} + +export function ds_rt_websocket_close(clientSocket: number): number { + const wrapper = socketHandles.get(clientSocket); + if (!wrapper) { + return -1; + } + socketHandles.delete(clientSocket); + return wrapper.close(); +} + +// this will take over the existing connection to JS and send new advert message to WS client +// use dotnet-dsrouter server-websocket -v trace +export function connectDSRouter(url: string): void { + if (!serverSession) { + throw new Error("No active session to reconnect"); + } + + // make sure new sessions hit the new URL + urlOverride = url; + + const wrapper = createDiagConnectionWs(serverSession.clientSocket, url); + socketHandles.set(serverSession.clientSocket, wrapper); + wrapper.send(advertise()); +} + +export function initializeDS() { + const loaderConfig = dotnetApi.getConfig(); + const diagnosticPorts = "DOTNET_DiagnosticPorts"; + // WASM-TODO, do this only when true + loaderConfig.environmentVariables![diagnosticPorts] = "js://ready"; + initializeJsClient(); +} diff --git a/src/native/libs/System.Native.Browser/diagnostics/dotnet-counters.ts b/src/native/libs/System.Native.Browser/diagnostics/dotnet-counters.ts new file mode 100644 index 00000000000000..c7e2b94b5ebe02 --- /dev/null +++ b/src/native/libs/System.Native.Browser/diagnostics/dotnet-counters.ts @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import type { DiagnosticCommandOptions } from "../types"; + +import { commandStopTracing, commandCounters } from "./client-commands"; +import { dotnetLoaderExports, Module } from "./cross-module"; +import { serverSession, setupJsClient } from "./diagnostic-server-js"; +import { IDiagnosticSession } from "./types"; + +export function collectMetrics(options?: DiagnosticCommandOptions): Promise { + if (!options) options = {}; + if (!serverSession) { + throw new Error("No active JS diagnostic session"); + } + + const onClosePromise = dotnetLoaderExports.createPromiseCompletionSource(); + function onSessionStart(session: IDiagnosticSession): void { + // stop tracing after period of monitoring + Module.safeSetTimeout(() => { + session.sendCommand(commandStopTracing(session.sessionId)); + }, 1000 * (options?.durationSeconds ?? 60)); + } + setupJsClient({ + onClosePromise: onClosePromise, + skipDownload: options.skipDownload, + commandOnAdvertise: () => commandCounters(options), + onSessionStart, + }); + return onClosePromise.promise; +} diff --git a/src/native/libs/System.Native.Browser/diagnostics/dotnet-cpu-profiler.ts b/src/native/libs/System.Native.Browser/diagnostics/dotnet-cpu-profiler.ts new file mode 100644 index 00000000000000..d13807369c5711 --- /dev/null +++ b/src/native/libs/System.Native.Browser/diagnostics/dotnet-cpu-profiler.ts @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import type { DiagnosticCommandOptions } from "../types"; + +import { commandStopTracing, commandSampleProfiler } from "./client-commands"; +import { dotnetApi, dotnetLoaderExports, Module } from "./cross-module"; +import { serverSession, setupJsClient } from "./diagnostic-server-js"; +import { IDiagnosticSession } from "./types"; + +export function collectCpuSamples(options?: DiagnosticCommandOptions): Promise { + if (!options) options = {}; + if (!serverSession) { + throw new Error("No active JS diagnostic session"); + } + if (!dotnetApi.getConfig().environmentVariables!["DOTNET_WasmPerformanceInstrumentation"]) { + throw new Error("method instrumentation is not enabled, please enable it with WasmPerformanceInstrumentation MSBuild property"); + } + + const onClosePromise = dotnetLoaderExports.createPromiseCompletionSource(); + function onSessionStart(session: IDiagnosticSession): void { + // stop tracing after period of monitoring + Module.safeSetTimeout(() => { + session.sendCommand(commandStopTracing(session.sessionId)); + }, 1000 * (options?.durationSeconds ?? 60)); + } + + setupJsClient({ + onClosePromise: onClosePromise, + skipDownload: options.skipDownload, + commandOnAdvertise: () => commandSampleProfiler(options), + onSessionStart, + }); + return onClosePromise.promise; +} diff --git a/src/native/libs/System.Native.Browser/diagnostics/dotnet-gcdump.ts b/src/native/libs/System.Native.Browser/diagnostics/dotnet-gcdump.ts new file mode 100644 index 00000000000000..c9e8a1b3541dbf --- /dev/null +++ b/src/native/libs/System.Native.Browser/diagnostics/dotnet-gcdump.ts @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import type { DiagnosticCommandOptions } from "../types"; + +import { commandStopTracing, commandGcHeapDump, } from "./client-commands"; +import { dotnetLoaderExports, Module } from "./cross-module"; +import { serverSession, setupJsClient } from "./diagnostic-server-js"; +import { IDiagnosticSession } from "./types"; + +export function collectGcDump(options?: DiagnosticCommandOptions): Promise { + if (!options) options = {}; + if (!serverSession) { + throw new Error("No active JS diagnostic session"); + } + + const onClosePromise = dotnetLoaderExports.createPromiseCompletionSource(); + let stopDelayedAfterLastMessage = 0; + let stopSent = false; + function onData(session: IDiagnosticSession, message: Uint8Array): void { + session.store(message); + if (!stopSent) { + // stop 1000ms after last GC message on this session, there will be more messages after that + if (stopDelayedAfterLastMessage) { + clearTimeout(stopDelayedAfterLastMessage); + } + stopDelayedAfterLastMessage = Module.safeSetTimeout(() => { + stopSent = true; + session.sendCommand(commandStopTracing(session.sessionId)); + }, 1000 * (options?.durationSeconds ?? 1)); + } + } + + setupJsClient({ + onClosePromise: onClosePromise, + skipDownload: options.skipDownload, + commandOnAdvertise: () => commandGcHeapDump(options), + onData, + }); + return onClosePromise.promise; +} diff --git a/src/native/libs/System.Native.Browser/diagnostics/index.ts b/src/native/libs/System.Native.Browser/diagnostics/index.ts index c011132f3df71a..f42cb92b2318ba 100644 --- a/src/native/libs/System.Native.Browser/diagnostics/index.ts +++ b/src/native/libs/System.Native.Browser/diagnostics/index.ts @@ -6,10 +6,14 @@ import { InternalExchangeIndex } from "../types"; import GitHash from "consts:gitHash"; -import { dotnetUpdateInternals, dotnetUpdateInternalsSubscriber } from "./cross-module"; +import { dotnetApi, dotnetUpdateInternals, dotnetUpdateInternalsSubscriber } from "./cross-module"; import { registerExit } from "./exit"; import { installNativeSymbols, symbolicateStackTrace } from "./symbolicate"; import { installLoggingProxy } from "./console-proxy"; +import { collectMetrics } from "./dotnet-counters"; +import { collectGcDump } from "./dotnet-gcdump"; +import { collectCpuSamples } from "./dotnet-cpu-profiler"; +import { connectDSRouter, ds_rt_websocket_close, ds_rt_websocket_create, ds_rt_websocket_poll, ds_rt_websocket_recv, ds_rt_websocket_send, initializeDS } from "./diagnostic-server"; export function dotnetInitializeModule(internals: InternalExchange): void { if (!Array.isArray(internals)) throw new Error("Expected internals to be an array"); @@ -24,11 +28,22 @@ export function dotnetInitializeModule(internals: InternalExchange): void { internals[InternalExchangeIndex.DiagnosticsExportsTable] = diagnosticsExportsToTable({ symbolicateStackTrace, installNativeSymbols, + ds_rt_websocket_create, + ds_rt_websocket_send, + ds_rt_websocket_poll, + ds_rt_websocket_recv, + ds_rt_websocket_close, }); dotnetUpdateInternals(internals, dotnetUpdateInternalsSubscriber); registerExit(); installLoggingProxy(); + initializeDS(); + + dotnetApi.collectCpuSamples = collectCpuSamples; + dotnetApi.collectMetrics = collectMetrics; + dotnetApi.collectGcDump = collectGcDump; + dotnetApi.connectDSRouter = connectDSRouter; // eslint-disable-next-line @typescript-eslint/no-unused-vars function diagnosticsExportsToTable(map: DiagnosticsExports): DiagnosticsExportsTable { @@ -36,6 +51,11 @@ export function dotnetInitializeModule(internals: InternalExchange): void { return [ map.symbolicateStackTrace, map.installNativeSymbols, + map.ds_rt_websocket_create, + map.ds_rt_websocket_send, + map.ds_rt_websocket_poll, + map.ds_rt_websocket_recv, + map.ds_rt_websocket_close, ]; } } diff --git a/src/native/libs/System.Native.Browser/diagnostics/types.ts b/src/native/libs/System.Native.Browser/diagnostics/types.ts index 2786379af7369f..38acccfbde59db 100644 --- a/src/native/libs/System.Native.Browser/diagnostics/types.ts +++ b/src/native/libs/System.Native.Browser/diagnostics/types.ts @@ -1,4 +1,268 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +import type { PromiseCompletionSource, VoidPtr } from "../types"; + +export interface IDiagnosticConnection { + send(message: Uint8Array): number; + poll(): number; + recv(buffer: VoidPtr, bytesToRead: number): number; + close(): number; +} + +// [hi,lo] +export type SessionId = [number, number]; + +export interface IDiagnosticSession { + sessionId: SessionId; + store(message: Uint8Array): number; + sendCommand(message: Uint8Array): void; +} + +export interface IDiagnosticClient { + skipDownload?: boolean; + onClosePromise: PromiseCompletionSource; + commandOnAdvertise(): Uint8Array; + onSessionStart?(session: IDiagnosticSession): void; + onData?(session: IDiagnosticSession, message: Uint8Array): void; + onClose?(messages: Uint8Array[]): void; + onError?(session: IDiagnosticSession, message: Uint8Array): void; +} + +export type FnClientProvider = (scenarioName: string) => IDiagnosticClient; + + +export type ProviderV2 = { + keywords: [number, Keywords], + logLevel: number, + providerName: string, + arguments: string | null +} + +export type PayloadV2 = { + circularBufferMB: number, + format: number, + requestRundown: boolean, + providers: ProviderV2[] +} + +export const enum Keywords { + None = 0, + All = 0xFFFF_FFFF, + // + // Summary: + // Logging when garbage collections and finalization happen. + GC = 1, + // + // Summary: + // Events when GC handles are set or destroyed. + GCHandle = 2, + Binder = 4, + // + // Summary: + // Logging when modules actually get loaded and unloaded. + Loader = 8, + // + // Summary: + // Logging when Just in time (JIT) compilation occurs. + Jit = 0x10, + // + // Summary: + // Logging when precompiled native (NGEN) images are loaded. + NGen = 0x20, + // + // Summary: + // Indicates that on attach or module load , a rundown of all existing methods should + // be done + StartEnumeration = 0x40, + // + // Summary: + // Indicates that on detach or process shutdown, a rundown of all existing methods + // should be done + StopEnumeration = 0x80, + // + // Summary: + // Events associated with validating security restrictions. + Security = 0x400, + // + // Summary: + // Events for logging resource consumption on an app-domain level granularity + AppDomainResourceManagement = 0x800, + // + // Summary: + // Logging of the internal workings of the Just In Time compiler. This is fairly + // verbose. It details decisions about interesting optimization (like inlining and + // tail call) + JitTracing = 0x1000, + // + // Summary: + // Log information about code thunks that transition between managed and unmanaged + // code. + Interop = 0x2000, + // + // Summary: + // Log when lock contention occurs. (Monitor.Enters actually blocks) + Contention = 0x4000, + // + // Summary: + // Log exception processing. + Exception = 0x8000, + // + // Summary: + // Log events associated with the threadpoo, and other threading events. + Threading = 0x10000, + // + // Summary: + // Dump the native to IL mapping of any method that is JIT compiled. (V4.5 runtimes + // and above). + JittedMethodILToNativeMap = 0x20000, + // + // Summary: + // If enabled will suppress the rundown of NGEN events on V4.0 runtime (has no effect + // on Pre-V4.0 runtimes). + OverrideAndSuppressNGenEvents = 0x40000, + // + // Summary: + // Enables the 'BulkType' event + Type = 0x80000, + // + // Summary: + // Enables the events associated with dumping the GC heap + GCHeapDump = 0x100000, + // + // Summary: + // Enables allocation sampling with the 'fast'. Sample to limit to 100 allocations + // per second per type. This is good for most detailed performance investigations. + // Note that this DOES update the allocation path to be slower and only works if + // the process start with this on. + GCSampledObjectAllocationHigh = 0x200000, + // + // Summary: + // Enables events associate with object movement or survival with each GC. + GCHeapSurvivalAndMovement = 0x400000, + // + // Summary: + // Triggers a GC. Can pass a 64 bit value that will be logged with the GC Start + // event so you know which GC you actually triggered. + GCHeapCollect = 0x800000, + // + // Summary: + // Indicates that you want type names looked up and put into the events (not just + // meta-data tokens). + GCHeapAndTypeNames = 0x1000000, + // + // Summary: + // Enables allocation sampling with the 'slow' rate, Sample to limit to 5 allocations + // per second per type. This is reasonable for monitoring. Note that this DOES update + // the allocation path to be slower and only works if the process start with this + // on. + GCSampledObjectAllocationLow = 0x2000000, + // + // Summary: + // Turns on capturing the stack and type of object allocation made by the .NET Runtime. + // This is only supported after V4.5.3 (Late 2014) This can be very verbose and + // you should seriously using GCSampledObjectAllocationHigh instead (and GCSampledObjectAllocationLow + // for production scenarios). + GCAllObjectAllocation = 0x2200000, + // + // Summary: + // This suppresses NGEN events on V4.0 (where you have NGEN PDBs), but not on V2.0 + // (which does not know about this bit and also does not have NGEN PDBS). + // eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values + SupressNGen = 0x40000, + // + // Summary: + // TODO document + PerfTrack = 0x20000000, + // + // Summary: + // Also log the stack trace of events for which this is valuable. + Stack = 0x40000000, + // + // Summary: + // This allows tracing work item transfer events (thread pool enqueue/dequeue/ioenqueue/iodequeue/a.o.) + ThreadTransfer = 0x80000000, + // + // Summary: + // .NET Debugger events + Debugger = 0x100000000, + // + // Summary: + // Events intended for monitoring on an ongoing basis. + Monitoring = 0x200000000, + // + // Summary: + // Events that will dump PDBs of dynamically generated assemblies to the ETW stream. + Codesymbols = 0x400000000, + // + // Summary: + // Events that provide information about compilation. + Compilation = 0x1000000000, + // + // Summary: + // Diagnostic events for diagnosing compilation and pre-compilation features. + CompilationDiagnostic = 0x2000000000, + // + // Summary: + // Diagnostic events for capturing token information for events that express MethodID + MethodDiagnostic = 0x4000000000, + // + // Summary: + // Diagnostic events for diagnosing issues involving the type loader. + TypeDiagnostic = 0x8000000000, + // + // Summary: + // Events for wait handle waits. + WaitHandle = 0x40000000000, + // + // Summary: + // Recommend default flags (good compromise on verbosity). + Default = 0x14C14FCCBD, + // + // Summary: + // What is needed to get symbols for JIT compiled code. + JITSymbols = 0x60098, + // + // Summary: + // This provides the flags commonly needed to take a heap .NET Heap snapshot with + // ETW. + GCHeapSnapshot = 0x1980001 +} + +export const enum CommandSetId { + Reserved = 0, + Dump = 1, + EventPipe = 2, + Profiler = 3, + Process = 4, + + // replies + Server = 0xFF, +} + +export const enum EventPipeCommandId { + StopTracing = 1, + CollectTracing = 2, + CollectTracing2 = 3, + CollectTracing3 = 4, + CollectTracing4 = 5, +} + +export const enum ProcessCommandId { + ProcessInfo = 0, + ResumeRuntime = 1, + ProcessEnvironment = 2, + SetEnvVar = 3, + ProcessInfo2 = 4, + EnablePerfmap = 5, + DisablePerfmap = 6, + ApplyStartupHook = 7, + ProcessInfo3 = 8, +} + +export const enum ServerCommandId { + OK = 0, + Error = 0xFF, +} + export * from "../types"; diff --git a/src/native/libs/System.Native.Browser/libSystem.Native.Browser.footer.js b/src/native/libs/System.Native.Browser/libSystem.Native.Browser.footer.js index 1a6d0b0928ed2b..1379c9f5e15c6e 100644 --- a/src/native/libs/System.Native.Browser/libSystem.Native.Browser.footer.js +++ b/src/native/libs/System.Native.Browser/libSystem.Native.Browser.footer.js @@ -23,6 +23,7 @@ function libDotnetFactory() { "SystemJS_ExecuteTimerCallback", "SystemJS_ExecuteBackgroundJobCallback", "SystemJS_ExecuteFinalizationCallback", + "SystemJS_ExecuteDiagnosticServerCallback", "__funcs_on_exit", ]; const mergeDotnet = { diff --git a/src/native/libs/System.Native.Browser/native/diagnostics.ts b/src/native/libs/System.Native.Browser/native/diagnostics.ts new file mode 100644 index 00000000000000..06eb7588644ca9 --- /dev/null +++ b/src/native/libs/System.Native.Browser/native/diagnostics.ts @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import { CharPtr, VoidPtr } from "../types"; +import { dotnetDiagnosticsExports } from "../utils/cross-module"; + +export function ds_rt_websocket_create(urlPtr: CharPtr): number { + return dotnetDiagnosticsExports.ds_rt_websocket_create(urlPtr); +} + +export function ds_rt_websocket_send(clientSocket: number, buffer: VoidPtr, bytesToWrite: number): number { + return dotnetDiagnosticsExports.ds_rt_websocket_send(clientSocket, buffer, bytesToWrite); +} + +export function ds_rt_websocket_poll(clientSocket: number): number { + return dotnetDiagnosticsExports.ds_rt_websocket_poll(clientSocket); +} + +export function ds_rt_websocket_recv(clientSocket: number, buffer: VoidPtr, bytesToRead: number): number { + return dotnetDiagnosticsExports.ds_rt_websocket_recv(clientSocket, buffer, bytesToRead); +} + +export function ds_rt_websocket_close(clientSocket: number): number { + return dotnetDiagnosticsExports.ds_rt_websocket_close(clientSocket); +} diff --git a/src/native/libs/System.Native.Browser/native/index.ts b/src/native/libs/System.Native.Browser/native/index.ts index 5a5e7de3ec545e..abc0ec51175c75 100644 --- a/src/native/libs/System.Native.Browser/native/index.ts +++ b/src/native/libs/System.Native.Browser/native/index.ts @@ -10,7 +10,9 @@ import GitHash from "consts:gitHash"; export { SystemJS_RandomBytes } from "./crypto"; export { SystemJS_GetLocaleInfo } from "./globalization-locale"; export { SystemJS_RejectMainPromise, SystemJS_ResolveMainPromise, SystemJS_MarkAsyncMain, SystemJS_ConsoleClear } from "./main"; -export { SystemJS_ScheduleTimer, SystemJS_ScheduleBackgroundJob, SystemJS_ScheduleFinalization } from "./scheduling"; +export { SystemJS_ScheduleTimer, SystemJS_ScheduleBackgroundJob, SystemJS_ScheduleFinalization, SystemJS_ScheduleDiagnosticServer } from "./scheduling"; +export { ds_rt_websocket_close, ds_rt_websocket_create, ds_rt_websocket_poll, ds_rt_websocket_recv, ds_rt_websocket_send } from "./diagnostics"; + export const gitHash = GitHash; export function dotnetInitializeModule(internals: InternalExchange): void { @@ -26,6 +28,7 @@ export function dotnetInitializeModule(internals: InternalExchange): void { internals[InternalExchangeIndex.NativeBrowserExportsTable] = nativeBrowserExportsToTable({ getWasmMemory, getWasmTable, + SystemJS_ScheduleDiagnosticServer: _ems_._SystemJS_ScheduleDiagnosticServer, }); _ems_.dotnetUpdateInternals(internals, _ems_.dotnetUpdateInternalsSubscriber); @@ -37,6 +40,7 @@ export function dotnetInitializeModule(internals: InternalExchange): void { return [ map.getWasmMemory, map.getWasmTable, + map.SystemJS_ScheduleDiagnosticServer, ]; } diff --git a/src/native/libs/System.Native.Browser/native/scheduling.ts b/src/native/libs/System.Native.Browser/native/scheduling.ts index 2e3bb534286c80..09708677dfaf64 100644 --- a/src/native/libs/System.Native.Browser/native/scheduling.ts +++ b/src/native/libs/System.Native.Browser/native/scheduling.ts @@ -80,3 +80,29 @@ export function SystemJS_ScheduleFinalization(): void { } } } + +export function SystemJS_ScheduleDiagnosticServer(): void { + if (_ems_.ABORT || _ems_.DOTNET.isAborting) { + // runtime is shutting down + return; + } + if (_ems_.DOTNET.lastScheduledDiagnosticServerId) { + globalThis.clearTimeout(_ems_.DOTNET.lastScheduledDiagnosticServerId); + _ems_.runtimeKeepalivePop(); + _ems_.DOTNET.lastScheduledDiagnosticServerId = undefined; + } + _ems_.DOTNET.lastScheduledDiagnosticServerId = _ems_.safeSetTimeout(SystemJS_ScheduleDiagnosticServerTick, 0); + + function SystemJS_ScheduleDiagnosticServerTick(): void { + try { + _ems_.DOTNET.lastScheduledDiagnosticServerId = undefined; + _ems_._SystemJS_ExecuteDiagnosticServerCallback(); + } catch (error: any) { + // do not propagate ExitStatus exception + if (!error || typeof error.status !== "number") { + _ems_.dotnetApi.exit(1, error); + throw error; + } + } + } +} diff --git a/src/native/libs/System.Native.Browser/utils/scheduling.ts b/src/native/libs/System.Native.Browser/utils/scheduling.ts index d7939cd154431f..77dc7781b45e83 100644 --- a/src/native/libs/System.Native.Browser/utils/scheduling.ts +++ b/src/native/libs/System.Native.Browser/utils/scheduling.ts @@ -12,6 +12,7 @@ export async function runBackgroundTimers(): Promise { _ems_._SystemJS_ExecuteTimerCallback(); _ems_._SystemJS_ExecuteBackgroundJobCallback(); _ems_._SystemJS_ExecuteFinalizationCallback(); + _ems_._SystemJS_ExecuteDiagnosticServerCallback(); } catch (error: any) { // do not propagate ExitStatus exception if (!error || typeof error.status !== "number") { @@ -37,5 +38,10 @@ export function abortBackgroundTimers(): void { _ems_.runtimeKeepalivePop(); _ems_.DOTNET.lastScheduledFinalizationId = undefined; } + if (_ems_.DOTNET.lastScheduledDiagnosticServerId) { + globalThis.clearTimeout(_ems_.DOTNET.lastScheduledDiagnosticServerId); + _ems_.runtimeKeepalivePop(); + _ems_.DOTNET.lastScheduledDiagnosticServerId = undefined; + } } diff --git a/src/native/rollup.config.plugins.js b/src/native/rollup.config.plugins.js index 641a425e689871..a3fafd32d1ca63 100644 --- a/src/native/rollup.config.plugins.js +++ b/src/native/rollup.config.plugins.js @@ -164,7 +164,11 @@ function checkFileExists(file) { } export function onwarn(warning) { - if (warning.code === "CIRCULAR_DEPENDENCY" && warning.ids.findIndex(id => id.includes("marshal-to-cs") || id.includes("marshal-to-js")) !== -1) { + if (warning.code === "CIRCULAR_DEPENDENCY" && warning.ids.findIndex(id => { + return id.includes("marshal-to-cs") + || id.includes("marshal-to-js") + || id.includes("diagnostics-js"); + }) !== -1) { // ignore circular dependency warnings from marshal-to-cs <-> marshal-to-js return; } From e1124b8f22dfeb9701d9d23d425e10e420ad6243 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Mon, 13 Apr 2026 11:59:25 +0200 Subject: [PATCH 2/3] cleanup --- src/mono/mono/utils/mono-threads-wasm.c | 6 +++--- .../System.Native.Browser/diagnostic_server_jobs.c | 5 ++++- .../System.Native.Browser/diagnostic_server_jobs.h | 7 +++++++ .../libs/System.Native.Browser/diagnostics/common.ts | 6 +++--- .../diagnostics/diagnostic-server.ts | 11 ++++++++--- src/native/rollup.config.plugins.js | 2 +- 6 files changed, 26 insertions(+), 11 deletions(-) diff --git a/src/mono/mono/utils/mono-threads-wasm.c b/src/mono/mono/utils/mono-threads-wasm.c index 6b43c1155bc821..9874cb3934e8c3 100644 --- a/src/mono/mono/utils/mono-threads-wasm.c +++ b/src/mono/mono/utils/mono-threads-wasm.c @@ -381,13 +381,13 @@ SystemJS_ExecuteDiagnosticServerCallback (void) for (cur1 = j1; cur1; cur1 = cur1->next) { DsJobRegistration* reg = (DsJobRegistration*)cur1->data; g_assert (reg->cb); - THREADS_DEBUG ("SystemJS_ExecuteDiagnosticServerCallback running job %p \n", (gpointer)cb); + THREADS_DEBUG ("SystemJS_ExecuteDiagnosticServerCallback running job %p \n", (gpointer)reg->cb); gsize done = reg->cb (reg->data); if (done){ - THREADS_DEBUG ("SystemJS_ExecuteDiagnosticServerCallback done job %p \n", (gpointer)cb); + THREADS_DEBUG ("SystemJS_ExecuteDiagnosticServerCallback done job %p \n", (gpointer)reg->cb); g_free (reg); } else { - THREADS_DEBUG ("SystemJS_ExecuteDiagnosticServerCallback scheduling job %p again \n", (gpointer)cb); + THREADS_DEBUG ("SystemJS_ExecuteDiagnosticServerCallback scheduling job %p again \n", (gpointer)reg->cb); jobs_ds = g_slist_prepend (jobs_ds, (gpointer)reg); } } diff --git a/src/native/libs/System.Native.Browser/diagnostic_server_jobs.c b/src/native/libs/System.Native.Browser/diagnostic_server_jobs.c index 70c4e6e70d6511..eb1ef950233814 100644 --- a/src/native/libs/System.Native.Browser/diagnostic_server_jobs.c +++ b/src/native/libs/System.Native.Browser/diagnostic_server_jobs.c @@ -6,7 +6,7 @@ #include #include -typedef size_t (*ds_job_cb)(void *data); +#include "diagnostic_server_jobs.h" extern void SystemJS_ScheduleDiagnosticServer(void); @@ -24,6 +24,9 @@ SystemJS_DiagnosticServerQueueJob (ds_job_cb cb, void *data) int wasEmpty = jobs == NULL; assert (cb); DsJobNode *node = (DsJobNode *)calloc (1, sizeof (DsJobNode)); + if (!node) { + abort (); + } node->cb = cb; node->data = data; node->next = jobs; diff --git a/src/native/libs/System.Native.Browser/diagnostic_server_jobs.h b/src/native/libs/System.Native.Browser/diagnostic_server_jobs.h index 52d05082713974..03debcad89041e 100644 --- a/src/native/libs/System.Native.Browser/diagnostic_server_jobs.h +++ b/src/native/libs/System.Native.Browser/diagnostic_server_jobs.h @@ -3,3 +3,10 @@ #pragma once +#include + +typedef size_t (*ds_job_cb)(void *data); + +void SystemJS_DiagnosticServerQueueJob (ds_job_cb cb, void *data); +void SystemJS_ExecuteDiagnosticServerCallback (void); + diff --git a/src/native/libs/System.Native.Browser/diagnostics/common.ts b/src/native/libs/System.Native.Browser/diagnostics/common.ts index 096929bf464b11..ec7413dc0e83f5 100644 --- a/src/native/libs/System.Native.Browser/diagnostics/common.ts +++ b/src/native/libs/System.Native.Browser/diagnostics/common.ts @@ -44,7 +44,7 @@ export function downloadBlob(messages: Uint8Array[]) { dotnetLogger.info(`Downloading trace ${link.download} - ${blob.size} bytes`); link.href = blobUrl; document.body.appendChild(link); - link.dispatchEvent(new MouseEvent("click", { - bubbles: true, cancelable: true, view: window - })); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(blobUrl); } diff --git a/src/native/libs/System.Native.Browser/diagnostics/diagnostic-server.ts b/src/native/libs/System.Native.Browser/diagnostics/diagnostic-server.ts index 3fbb633140b22e..8ab5ccd52ad6c4 100644 --- a/src/native/libs/System.Native.Browser/diagnostics/diagnostic-server.ts +++ b/src/native/libs/System.Native.Browser/diagnostics/diagnostic-server.ts @@ -7,7 +7,7 @@ import { createDiagConnectionJs, initializeJsClient, serverSession } from "./dia import { createDiagConnectionWs } from "./diagnostic-server-ws"; import { advertise } from "./client-commands"; import { IDiagnosticConnection } from "./types"; -import { dotnetApi, dotnetBrowserUtilsExports, Module } from "./cross-module"; +import { dotnetApi, dotnetBrowserUtilsExports } from "./cross-module"; let socketHandles: Map = undefined as any; let nextSocketHandle = 1; @@ -27,7 +27,6 @@ export function ds_rt_websocket_create(urlPtr: CharPtr): number { url = urlOverride; } - Module.UTF8ToString(urlPtr); const socketHandle = nextSocketHandle++; const isWebSocket = url.startsWith("ws://") || url.startsWith("wss://"); const wrapper = isWebSocket @@ -84,6 +83,10 @@ export function connectDSRouter(url: string): void { // make sure new sessions hit the new URL urlOverride = url; + const oldWrapper = socketHandles.get(serverSession.clientSocket); + if (oldWrapper) { + oldWrapper.close(); + } const wrapper = createDiagConnectionWs(serverSession.clientSocket, url); socketHandles.set(serverSession.clientSocket, wrapper); wrapper.send(advertise()); @@ -93,6 +96,8 @@ export function initializeDS() { const loaderConfig = dotnetApi.getConfig(); const diagnosticPorts = "DOTNET_DiagnosticPorts"; // WASM-TODO, do this only when true - loaderConfig.environmentVariables![diagnosticPorts] = "js://ready"; + if (!loaderConfig.environmentVariables![diagnosticPorts]) { + loaderConfig.environmentVariables![diagnosticPorts] = "js://ready"; + } initializeJsClient(); } diff --git a/src/native/rollup.config.plugins.js b/src/native/rollup.config.plugins.js index a3fafd32d1ca63..efbcffaf6eb9a3 100644 --- a/src/native/rollup.config.plugins.js +++ b/src/native/rollup.config.plugins.js @@ -169,7 +169,7 @@ export function onwarn(warning) { || id.includes("marshal-to-js") || id.includes("diagnostics-js"); }) !== -1) { - // ignore circular dependency warnings from marshal-to-cs <-> marshal-to-js + // ignore circular dependency warnings from marshal-to-cs <-> marshal-to-js and diagnostics return; } // eslint-disable-next-line no-console From e177de2adef3005b3bd49cacfe543a42ba641da7 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Mon, 13 Apr 2026 22:26:25 +0200 Subject: [PATCH 3/3] fix --- src/native/libs/System.Native.Browser/native/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/native/libs/System.Native.Browser/native/index.ts b/src/native/libs/System.Native.Browser/native/index.ts index abc0ec51175c75..ab3abee36ce7b1 100644 --- a/src/native/libs/System.Native.Browser/native/index.ts +++ b/src/native/libs/System.Native.Browser/native/index.ts @@ -6,6 +6,7 @@ import { InternalExchangeIndex } from "../types"; import { _ems_ } from "../../Common/JavaScript/ems-ambient"; import GitHash from "consts:gitHash"; +import { SystemJS_ScheduleDiagnosticServer } from "./scheduling"; export { SystemJS_RandomBytes } from "./crypto"; export { SystemJS_GetLocaleInfo } from "./globalization-locale"; @@ -28,7 +29,7 @@ export function dotnetInitializeModule(internals: InternalExchange): void { internals[InternalExchangeIndex.NativeBrowserExportsTable] = nativeBrowserExportsToTable({ getWasmMemory, getWasmTable, - SystemJS_ScheduleDiagnosticServer: _ems_._SystemJS_ScheduleDiagnosticServer, + SystemJS_ScheduleDiagnosticServer, }); _ems_.dotnetUpdateInternals(internals, _ems_.dotnetUpdateInternalsSubscriber);