From db60a64cda07fe6a63218fd88217d9d71df30cca Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 27 May 2026 15:36:39 +0200 Subject: [PATCH 1/6] - EventPipeThreadSamplingRate unset detection - ds_rt_browser_performance_measure - support for JS diagnostic client during startup --- src/coreclr/inc/clrconfigvalues.h | 2 +- .../nativeaot/Runtime/eventpipe/ep-rt-aot.h | 2 +- src/mono/mono/eventpipe/ep-rt-mono.h | 4 +-- src/native/eventpipe/ep-shared-config.h.in | 2 ++ src/native/eventpipe/ep.c | 4 ++- .../Common/JavaScript/cross-module/index.ts | 1 + .../libs/Common/JavaScript/types/exchange.ts | 4 ++- .../diagnostics/client-commands.ts | 2 +- .../diagnostics/common.ts | 15 ++++++---- .../diagnostics/diagnostic-server-js.ts | 28 +++++++++++++------ .../diagnostics/diagnostic-server-ws.ts | 6 +++- .../diagnostics/diagnostic-server.ts | 2 -- .../diagnostics/dotnet-counters.ts | 9 +++--- .../diagnostics/dotnet-cpu-profiler.ts | 11 ++++---- .../diagnostics/dotnet-gcdump.ts | 14 ++++++---- .../diagnostics/index.ts | 10 +++++-- .../native/diagnostics.ts | 4 +++ .../System.Native.Browser/native/index.ts | 2 +- src/native/rollup.config.plugins.js | 6 +++- 19 files changed, 86 insertions(+), 42 deletions(-) diff --git a/src/coreclr/inc/clrconfigvalues.h b/src/coreclr/inc/clrconfigvalues.h index 3d457c8fd5460b..5339fa20f97b3c 100644 --- a/src/coreclr/inc/clrconfigvalues.h +++ b/src/coreclr/inc/clrconfigvalues.h @@ -609,7 +609,7 @@ RETAIL_CONFIG_DWORD_INFO(INTERNAL_EventPipeCircularMB, W("EventPipeCircularMB"), RETAIL_CONFIG_DWORD_INFO(INTERNAL_EventPipeProcNumbers, W("EventPipeProcNumbers"), 0, "Enable/disable capturing processor numbers in EventPipe event headers") RETAIL_CONFIG_DWORD_INFO(INTERNAL_EventPipeOutputStreaming, W("EventPipeOutputStreaming"), 1, "Enable/disable streaming for trace file set in DOTNET_EventPipeOutputPath. Non-zero values enable streaming.") RETAIL_CONFIG_DWORD_INFO(INTERNAL_EventPipeEnableStackwalk, W("EventPipeEnableStackwalk"), 1, "Set to 0 to disable collecting stacks for EventPipe events.") -RETAIL_CONFIG_DWORD_INFO(INTERNAL_EventPipeThreadSamplingRate, W("EventPipeThreadSamplingRate"), 0, "Desired sample interval in milliseconds for EventPipe thread time sampling profiler. 0 means use the default.") +RETAIL_CONFIG_DWORD_INFO(INTERNAL_EventPipeThreadSamplingRate, W("EventPipeThreadSamplingRate"), UINT32_MAX, "Desired sample interval in milliseconds for EventPipe thread time sampling profiler. 0 means maximum frequency (sample every opportunity). Unset uses the default.") // // Generational Aware Analysis diff --git a/src/coreclr/nativeaot/Runtime/eventpipe/ep-rt-aot.h b/src/coreclr/nativeaot/Runtime/eventpipe/ep-rt-aot.h index c9ae8dcbd80c8e..de0c84b98aac37 100644 --- a/src/coreclr/nativeaot/Runtime/eventpipe/ep-rt-aot.h +++ b/src/coreclr/nativeaot/Runtime/eventpipe/ep-rt-aot.h @@ -521,7 +521,7 @@ ep_rt_config_value_get_sampling_rate (void) return static_cast(value); } - return 0; + return UINT32_MAX; } /* diff --git a/src/mono/mono/eventpipe/ep-rt-mono.h b/src/mono/mono/eventpipe/ep-rt-mono.h index a5f3c2626509ed..b54b649c36b48b 100644 --- a/src/mono/mono/eventpipe/ep-rt-mono.h +++ b/src/mono/mono/eventpipe/ep-rt-mono.h @@ -642,7 +642,7 @@ inline uint32_t ep_rt_config_value_get_sampling_rate (void) { - uint32_t value_uint32_t = 0; + uint32_t value_uint32_t = G_MAXUINT32; gchar *value = g_getenv ("DOTNET_EventPipeThreadSamplingRate"); if (!value) value = g_getenv ("COMPlus_EventPipeThreadSamplingRate"); @@ -1024,7 +1024,7 @@ ep_rt_queue_job ( // it's called from browser event loop ds_job_cb cb = (ds_job_cb)job_func; - // invoke the callback inline for the fist time + // invoke the callback inline for the first time gsize done = cb (params); // see if it's done or needs to be scheduled again diff --git a/src/native/eventpipe/ep-shared-config.h.in b/src/native/eventpipe/ep-shared-config.h.in index 025bbab83a557e..cb9759ee79aeea 100644 --- a/src/native/eventpipe/ep-shared-config.h.in +++ b/src/native/eventpipe/ep-shared-config.h.in @@ -25,8 +25,10 @@ #cmakedefine FEATURE_PERFTRACING_DISABLE_THREADS #ifdef FEATURE_PERFTRACING_DISABLE_THREADS +#ifndef PERFTRACING_DISABLE_THREADS #define PERFTRACING_DISABLE_THREADS #endif +#endif #cmakedefine FEATURE_PERFTRACING_DISABLE_PERFTRACING_LISTEN_PORTS #ifdef FEATURE_PERFTRACING_DISABLE_PERFTRACING_LISTEN_PORTS diff --git a/src/native/eventpipe/ep.c b/src/native/eventpipe/ep.c index 9cf4acff6bdfed..cb7549f9c0e49b 100644 --- a/src/native/eventpipe/ep.c +++ b/src/native/eventpipe/ep.c @@ -1498,8 +1498,10 @@ ep_init (void) #endif // PERFTRACING_DISABLE_THREADS // Allow overriding the sampling rate via DOTNET_EventPipeThreadSamplingRate (in milliseconds). + // UINT32_MAX is the sentinel for "unset" (use default). 0 means maximum frequency + // (sample at every opportunity — 0 nanosecond interval). uint32_t configured_rate_ms = ep_rt_config_value_get_sampling_rate (); - if (configured_rate_ms > 0) + if (configured_rate_ms != UINT32_MAX) ep_sample_profiler_set_sampling_rate ((uint64_t)configured_rate_ms * 1000000); else ep_sample_profiler_set_sampling_rate (default_profiler_sample_rate_in_nanoseconds); diff --git a/src/native/libs/Common/JavaScript/cross-module/index.ts b/src/native/libs/Common/JavaScript/cross-module/index.ts index aa9e2b3f66a2e7..97cb18ca5da174 100644 --- a/src/native/libs/Common/JavaScript/cross-module/index.ts +++ b/src/native/libs/Common/JavaScript/cross-module/index.ts @@ -193,6 +193,7 @@ export function dotnetUpdateInternalsSubscriber() { ds_rt_websocket_poll: table[4], ds_rt_websocket_recv: table[5], ds_rt_websocket_close: table[6], + ds_rt_browser_performance_measure: table[7], }; Object.assign(interop, interopLocal); } diff --git a/src/native/libs/Common/JavaScript/types/exchange.ts b/src/native/libs/Common/JavaScript/types/exchange.ts index 376d33e09bb103..e78aea223d4707 100644 --- a/src/native/libs/Common/JavaScript/types/exchange.ts +++ b/src/native/libs/Common/JavaScript/types/exchange.ts @@ -1,7 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -import type { EmsAmbientSymbolsType } from "../types"; +import type { CharPtr, EmsAmbientSymbolsType } from "../types"; import type { check, error, info, warn, debug, fastCheck, normalizeException } from "../loader/logging"; import type { resolveRunMainPromise, rejectRunMainPromise, getRunMainPromise, abortStartup } from "../loader/run"; @@ -197,6 +197,7 @@ export type DiagnosticsExportsTable = [ typeof ds_rt_websocket_poll, typeof ds_rt_websocket_recv, typeof ds_rt_websocket_close, + (namePtr: CharPtr, start: number) => void ] export type DiagnosticsExports = { @@ -207,4 +208,5 @@ export type DiagnosticsExports = { 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, + ds_rt_browser_performance_measure: (namePtr: CharPtr, start: number) => void } diff --git a/src/native/libs/System.Native.Browser/diagnostics/client-commands.ts b/src/native/libs/System.Native.Browser/diagnostics/client-commands.ts index e7883c0b6ebd3b..ee31e21cdc2a9e 100644 --- a/src/native/libs/System.Native.Browser/diagnostics/client-commands.ts +++ b/src/native/libs/System.Native.Browser/diagnostics/client-commands.ts @@ -89,7 +89,7 @@ export function commandCounters(options: DiagnosticCommandOptions) { 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()};`, + arguments: `SessionId=SHARED;Metrics=System.Runtime;RefreshInterval=${options.intervalSeconds ?? 1};MaxTimeSeries=1000;MaxHistograms=10;ClientId=${uuidv4()};`, }, ...options.extraProviders || [], ] diff --git a/src/native/libs/System.Native.Browser/diagnostics/common.ts b/src/native/libs/System.Native.Browser/diagnostics/common.ts index 092c3ac23949b8..de90bc34ce86ec 100644 --- a/src/native/libs/System.Native.Browser/diagnostics/common.ts +++ b/src/native/libs/System.Native.Browser/diagnostics/common.ts @@ -7,6 +7,7 @@ import { dotnetApi, dotnetLogger } from "./cross-module"; export class DiagnosticConnectionBase { protected messagesToSend: Uint8Array[] = []; protected messagesReceived: Uint8Array[] = []; + private messagesReceivedHead = 0; constructor(public clientSocket: number) { } @@ -16,21 +17,25 @@ export class DiagnosticConnectionBase { } poll(): number { - return this.messagesReceived.length; + return this.messagesReceived.length - this.messagesReceivedHead; } recv(buffer: VoidPtr, bytesToRead: number): number { - if (this.messagesReceived.length === 0) { + if (this.messagesReceivedHead >= this.messagesReceived.length) { return 0; } - const message = this.messagesReceived[0]!; + const message = this.messagesReceived[this.messagesReceivedHead]!; 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(); + this.messagesReceivedHead++; + if (this.messagesReceivedHead > 128 && this.messagesReceivedHead >= (this.messagesReceived.length >>> 1)) { + this.messagesReceived = this.messagesReceived.slice(this.messagesReceivedHead); + this.messagesReceivedHead = 0; + } } else { - this.messagesReceived[0] = message.subarray(bytesRead); + this.messagesReceived[this.messagesReceivedHead] = message.subarray(bytesRead); } return bytesRead; } 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 index e0e86a2653aeb0..1d33837252390f 100644 --- a/src/native/libs/System.Native.Browser/diagnostics/diagnostic-server-js.ts +++ b/src/native/libs/System.Native.Browser/diagnostics/diagnostic-server-js.ts @@ -16,6 +16,7 @@ import { collectCpuSamples } from "./dotnet-cpu-profiler"; // .withEnvironmentVariable("DOTNET_DiagnosticPorts", "download:gcdump") // or implement function globalThis.dotnetDiagnosticClient with IDiagClient interface +let startupJsClient: IDiagnosticClient | undefined = undefined; let nextJsClient: PromiseCompletionSource; let fromScenarioNameOnce = false; @@ -54,7 +55,11 @@ class DiagnosticSession extends DiagnosticConnectionBase implements IDiagnosticC } async connectNewClient() { - this.diagClient = await nextJsClient.promise; + if (startupJsClient) { + this.diagClient = startupJsClient; + } else { + this.diagClient = await nextJsClient.promise; + } initializeJsClient(); const firstCommand = this.diagClient.commandOnAdvertise(); this.respond(firstCommand); @@ -136,29 +141,34 @@ class DiagnosticSession extends DiagnosticConnectionBase implements IDiagnosticC export function initializeJsClient() { nextJsClient = dotnetLoaderExports.createPromiseCompletionSource(); + startupJsClient = undefined; } -export function setupJsClient(client: IDiagnosticClient) { - if (!dotnetLoaderExports.isRuntimeRunning()) { +export function setupJsClient(client: IDiagnosticClient, startup?: boolean) { + if (!startup && !dotnetLoaderExports.isRuntimeRunning()) { throw new Error("Runtime is not running"); } - if (nextJsClient.isDone) { - throw new Error("multiple clients in parallel are not allowed"); + if (startup) { + startupJsClient = client; + } else { + if (nextJsClient.isDone) { + throw new Error("multiple clients in parallel are not allowed"); + } + nextJsClient.resolve(client); } - nextJsClient.resolve(client); } export function createDiagConnectionJs(socketHandle: number, scenarioName: string): DiagnosticSession { if (!fromScenarioNameOnce) { fromScenarioNameOnce = true; if (scenarioName.startsWith("js://gcdump")) { - collectGcDump({}); + collectGcDump({}, true); } if (scenarioName.startsWith("js://counters")) { - collectMetrics({}); + collectMetrics({}, true); } if (scenarioName.startsWith("js://cpu-samples")) { - collectCpuSamples({}); + collectCpuSamples({}, true); } const dotnetDiagnosticClient: FnClientProvider = (globalThis as any).dotnetDiagnosticClient; if (typeof dotnetDiagnosticClient === "function") { 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 index ce5891366e9ff0..8114d53e876706 100644 --- a/src/native/libs/System.Native.Browser/diagnostics/diagnostic-server-ws.ts +++ b/src/native/libs/System.Native.Browser/diagnostics/diagnostic-server-ws.ts @@ -50,7 +50,11 @@ class DiagnosticConnectionWS extends DiagnosticConnectionBase implements IDiagno return super.store(message); } - this.ws!.send(message as any); + try { + this.ws!.send(message as any); + } catch { + return -1; + } return message.length; } 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 9b07c7d21eafc9..d65005a122c310 100644 --- a/src/native/libs/System.Native.Browser/diagnostics/diagnostic-server.ts +++ b/src/native/libs/System.Native.Browser/diagnostics/diagnostic-server.ts @@ -90,12 +90,10 @@ export function connectDSRouter(url: string): void { } export function initializeDS() { - /* WASM-TODO, do this only when true const loaderConfig = dotnetApi.getConfig(); const diagnosticPorts = "DOTNET_DiagnosticPorts"; if (!loaderConfig.environmentVariables![diagnosticPorts]) { 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 index c7e2b94b5ebe02..dcf38fd3dcbd09 100644 --- a/src/native/libs/System.Native.Browser/diagnostics/dotnet-counters.ts +++ b/src/native/libs/System.Native.Browser/diagnostics/dotnet-counters.ts @@ -3,19 +3,20 @@ import type { DiagnosticCommandOptions } from "../types"; -import { commandStopTracing, commandCounters } from "./client-commands"; +import { commandResumeRuntime, 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 { +export function collectMetrics(options?: DiagnosticCommandOptions, startup?: boolean): Promise { if (!options) options = {}; - if (!serverSession) { + if (!startup && !serverSession) { throw new Error("No active JS diagnostic session"); } const onClosePromise = dotnetLoaderExports.createPromiseCompletionSource(); function onSessionStart(session: IDiagnosticSession): void { + session.sendCommand(commandResumeRuntime()); // stop tracing after period of monitoring Module.safeSetTimeout(() => { session.sendCommand(commandStopTracing(session.sessionId)); @@ -26,6 +27,6 @@ export function collectMetrics(options?: DiagnosticCommandOptions): Promise commandCounters(options), onSessionStart, - }); + }, startup); 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 index d13807369c5711..0aec4d498521c7 100644 --- a/src/native/libs/System.Native.Browser/diagnostics/dotnet-cpu-profiler.ts +++ b/src/native/libs/System.Native.Browser/diagnostics/dotnet-cpu-profiler.ts @@ -3,14 +3,14 @@ import type { DiagnosticCommandOptions } from "../types"; -import { commandStopTracing, commandSampleProfiler } from "./client-commands"; +import { commandResumeRuntime, 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 { +export function collectCpuSamples(options?: DiagnosticCommandOptions, startup?: boolean): Promise { if (!options) options = {}; - if (!serverSession) { + if (!startup && !serverSession) { throw new Error("No active JS diagnostic session"); } if (!dotnetApi.getConfig().environmentVariables!["DOTNET_WasmPerformanceInstrumentation"]) { @@ -19,10 +19,11 @@ export function collectCpuSamples(options?: DiagnosticCommandOptions): Promise(); function onSessionStart(session: IDiagnosticSession): void { + session.sendCommand(commandResumeRuntime()); // stop tracing after period of monitoring Module.safeSetTimeout(() => { session.sendCommand(commandStopTracing(session.sessionId)); - }, 1000 * (options?.durationSeconds ?? 60)); + }, 1000 * (options?.durationSeconds ?? 10)); } setupJsClient({ @@ -30,6 +31,6 @@ export function collectCpuSamples(options?: DiagnosticCommandOptions): Promise commandSampleProfiler(options), onSessionStart, - }); + }, startup); 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 index c9e8a1b3541dbf..02a4be2a0552b5 100644 --- a/src/native/libs/System.Native.Browser/diagnostics/dotnet-gcdump.ts +++ b/src/native/libs/System.Native.Browser/diagnostics/dotnet-gcdump.ts @@ -3,24 +3,27 @@ import type { DiagnosticCommandOptions } from "../types"; -import { commandStopTracing, commandGcHeapDump, } from "./client-commands"; +import { commandResumeRuntime, 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 { +export function collectGcDump(options?: DiagnosticCommandOptions, startup?: boolean): Promise { if (!options) options = {}; - if (!serverSession) { + if (!startup && !serverSession) { throw new Error("No active JS diagnostic session"); } const onClosePromise = dotnetLoaderExports.createPromiseCompletionSource(); let stopDelayedAfterLastMessage = 0; let stopSent = false; + function onSessionStart(session: IDiagnosticSession): void { + session.sendCommand(commandResumeRuntime()); + } 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 + // stop durationSeconds (default 1s) after last GC message on this session, there will be more messages after that if (stopDelayedAfterLastMessage) { clearTimeout(stopDelayedAfterLastMessage); } @@ -35,7 +38,8 @@ export function collectGcDump(options?: DiagnosticCommandOptions): Promise commandGcHeapDump(options), + onSessionStart, onData, - }); + }, startup); 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 f42cb92b2318ba..d93ef4381ebec6 100644 --- a/src/native/libs/System.Native.Browser/diagnostics/index.ts +++ b/src/native/libs/System.Native.Browser/diagnostics/index.ts @@ -1,12 +1,12 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -import type { DiagnosticsExportsTable, InternalExchange, DiagnosticsExports } from "./types"; +import type { DiagnosticsExportsTable, InternalExchange, DiagnosticsExports, CharPtr } from "./types"; import { InternalExchangeIndex } from "../types"; import GitHash from "consts:gitHash"; -import { dotnetApi, dotnetUpdateInternals, dotnetUpdateInternalsSubscriber } from "./cross-module"; +import { dotnetApi, dotnetUpdateInternals, dotnetUpdateInternalsSubscriber, Module } from "./cross-module"; import { registerExit } from "./exit"; import { installNativeSymbols, symbolicateStackTrace } from "./symbolicate"; import { installLoggingProxy } from "./console-proxy"; @@ -24,6 +24,10 @@ export function dotnetInitializeModule(internals: InternalExchange): void { if (runtimeApi.runtimeBuildInfo.gitHash && runtimeApi.runtimeBuildInfo.gitHash !== GitHash) { throw new Error(`Mismatched git hashes between loader and runtime. Loader: ${runtimeApi.runtimeBuildInfo.gitHash}, Diagnostics: ${GitHash}`); } + const ds_rt_browser_performance_measure = + globalThis.performance && typeof globalThis.performance.measure === "function" + ? (namePtr: CharPtr, start: number) => globalThis.performance.measure(Module.UTF8ToString(namePtr), { start: start }) + : () => { }; internals[InternalExchangeIndex.DiagnosticsExportsTable] = diagnosticsExportsToTable({ symbolicateStackTrace, @@ -33,6 +37,7 @@ export function dotnetInitializeModule(internals: InternalExchange): void { ds_rt_websocket_poll, ds_rt_websocket_recv, ds_rt_websocket_close, + ds_rt_browser_performance_measure, }); dotnetUpdateInternals(internals, dotnetUpdateInternalsSubscriber); @@ -56,6 +61,7 @@ export function dotnetInitializeModule(internals: InternalExchange): void { map.ds_rt_websocket_poll, map.ds_rt_websocket_recv, map.ds_rt_websocket_close, + map.ds_rt_browser_performance_measure, ]; } } diff --git a/src/native/libs/System.Native.Browser/native/diagnostics.ts b/src/native/libs/System.Native.Browser/native/diagnostics.ts index 06eb7588644ca9..5ce37cb515a75b 100644 --- a/src/native/libs/System.Native.Browser/native/diagnostics.ts +++ b/src/native/libs/System.Native.Browser/native/diagnostics.ts @@ -23,3 +23,7 @@ export function ds_rt_websocket_recv(clientSocket: number, buffer: VoidPtr, byte export function ds_rt_websocket_close(clientSocket: number): number { return dotnetDiagnosticsExports.ds_rt_websocket_close(clientSocket); } + +export function ds_rt_browser_performance_measure(namePtr: CharPtr, start: number): void { + return dotnetDiagnosticsExports.ds_rt_browser_performance_measure(namePtr, start); +} diff --git a/src/native/libs/System.Native.Browser/native/index.ts b/src/native/libs/System.Native.Browser/native/index.ts index abc0ec51175c75..0e5a8d68619685 100644 --- a/src/native/libs/System.Native.Browser/native/index.ts +++ b/src/native/libs/System.Native.Browser/native/index.ts @@ -11,7 +11,7 @@ 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, 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 { ds_rt_websocket_close, ds_rt_websocket_create, ds_rt_websocket_poll, ds_rt_websocket_recv, ds_rt_websocket_send, ds_rt_browser_performance_measure } from "./diagnostics"; export const gitHash = GitHash; diff --git a/src/native/rollup.config.plugins.js b/src/native/rollup.config.plugins.js index efbcffaf6eb9a3..f3e66ed90e2501 100644 --- a/src/native/rollup.config.plugins.js +++ b/src/native/rollup.config.plugins.js @@ -167,7 +167,11 @@ export function onwarn(warning) { if (warning.code === "CIRCULAR_DEPENDENCY" && warning.ids.findIndex(id => { return id.includes("marshal-to-cs") || id.includes("marshal-to-js") - || id.includes("diagnostics-js"); + || id.includes("diagnostics-js") + || id.includes("dotnet-gcdump") + || id.includes("dotnet-cpu-profiler") + || id.includes("dotnet-counters") + || id.includes("diagnostic-server-js"); }) !== -1) { // ignore circular dependency warnings from marshal-to-cs <-> marshal-to-js and diagnostics return; From d9ef9e526b597d145beaf6dedac44f602d82e814 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 27 May 2026 15:50:35 +0200 Subject: [PATCH 2/6] feedback --- .../diagnostics/diagnostic-server-js.ts | 1 + .../diagnostics/diagnostic-server-ws.ts | 1 + .../diagnostics/diagnostic-server.ts | 1 + .../libs/System.Native.Browser/diagnostics/index.ts | 8 +++++++- 4 files changed, 10 insertions(+), 1 deletion(-) 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 index 1d33837252390f..3b952fca07b5a6 100644 --- a/src/native/libs/System.Native.Browser/diagnostics/diagnostic-server-js.ts +++ b/src/native/libs/System.Native.Browser/diagnostics/diagnostic-server-js.ts @@ -57,6 +57,7 @@ class DiagnosticSession extends DiagnosticConnectionBase implements IDiagnosticC async connectNewClient() { if (startupJsClient) { this.diagClient = startupJsClient; + startupJsClient = undefined; } else { this.diagClient = await nextJsClient.promise; } 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 index 8114d53e876706..28b84362b966ee 100644 --- a/src/native/libs/System.Native.Browser/diagnostics/diagnostic-server-ws.ts +++ b/src/native/libs/System.Native.Browser/diagnostics/diagnostic-server-ws.ts @@ -53,6 +53,7 @@ class DiagnosticConnectionWS extends DiagnosticConnectionBase implements IDiagno try { this.ws!.send(message as any); } catch { + dotnetLogger.warn("Diagnostic server WebSocket connection failed unexpectedly."); return -1; } 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 d65005a122c310..b2b7827eb24f23 100644 --- a/src/native/libs/System.Native.Browser/diagnostics/diagnostic-server.ts +++ b/src/native/libs/System.Native.Browser/diagnostics/diagnostic-server.ts @@ -92,6 +92,7 @@ export function connectDSRouter(url: string): void { export function initializeDS() { const loaderConfig = dotnetApi.getConfig(); const diagnosticPorts = "DOTNET_DiagnosticPorts"; + loaderConfig.environmentVariables ??= {}; if (!loaderConfig.environmentVariables![diagnosticPorts]) { loaderConfig.environmentVariables![diagnosticPorts] = "js://ready"; } diff --git a/src/native/libs/System.Native.Browser/diagnostics/index.ts b/src/native/libs/System.Native.Browser/diagnostics/index.ts index d93ef4381ebec6..055078f2eebed8 100644 --- a/src/native/libs/System.Native.Browser/diagnostics/index.ts +++ b/src/native/libs/System.Native.Browser/diagnostics/index.ts @@ -26,7 +26,13 @@ export function dotnetInitializeModule(internals: InternalExchange): void { } const ds_rt_browser_performance_measure = globalThis.performance && typeof globalThis.performance.measure === "function" - ? (namePtr: CharPtr, start: number) => globalThis.performance.measure(Module.UTF8ToString(namePtr), { start: start }) + ? (namePtr: CharPtr, start: number) => { + try { + globalThis.performance.measure(Module.UTF8ToString(namePtr), { start: start }); + } catch (e) { + // Ignore + } + } : () => { }; internals[InternalExchangeIndex.DiagnosticsExportsTable] = diagnosticsExportsToTable({ From 498cec9c44f8a2666fe0585d78aed0ba5096ca42 Mon Sep 17 00:00:00 2001 From: Pavel Savara Date: Wed, 27 May 2026 16:08:14 +0200 Subject: [PATCH 3/6] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/native/libs/System.Native.Browser/diagnostics/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/native/libs/System.Native.Browser/diagnostics/index.ts b/src/native/libs/System.Native.Browser/diagnostics/index.ts index 055078f2eebed8..c90784df6d60d9 100644 --- a/src/native/libs/System.Native.Browser/diagnostics/index.ts +++ b/src/native/libs/System.Native.Browser/diagnostics/index.ts @@ -29,7 +29,7 @@ export function dotnetInitializeModule(internals: InternalExchange): void { ? (namePtr: CharPtr, start: number) => { try { globalThis.performance.measure(Module.UTF8ToString(namePtr), { start: start }); - } catch (e) { + } catch { // Ignore } } From 87458fb3c7db13464504326b0b24b3468ce84d0f Mon Sep 17 00:00:00 2001 From: Pavel Savara Date: Wed, 27 May 2026 16:25:44 +0200 Subject: [PATCH 4/6] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../libs/System.Native.Browser/diagnostics/diagnostic-server.ts | 2 +- .../System.Native.Browser/diagnostics/dotnet-cpu-profiler.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 b2b7827eb24f23..4a59c891e53381 100644 --- a/src/native/libs/System.Native.Browser/diagnostics/diagnostic-server.ts +++ b/src/native/libs/System.Native.Browser/diagnostics/diagnostic-server.ts @@ -93,7 +93,7 @@ export function initializeDS() { const loaderConfig = dotnetApi.getConfig(); const diagnosticPorts = "DOTNET_DiagnosticPorts"; loaderConfig.environmentVariables ??= {}; - if (!loaderConfig.environmentVariables![diagnosticPorts]) { + if (loaderConfig.environmentVariables![diagnosticPorts] === undefined) { loaderConfig.environmentVariables![diagnosticPorts] = "js://ready"; } initializeJsClient(); 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 index 0aec4d498521c7..56b30b6a39efd0 100644 --- a/src/native/libs/System.Native.Browser/diagnostics/dotnet-cpu-profiler.ts +++ b/src/native/libs/System.Native.Browser/diagnostics/dotnet-cpu-profiler.ts @@ -23,7 +23,7 @@ export function collectCpuSamples(options?: DiagnosticCommandOptions, startup?: // stop tracing after period of monitoring Module.safeSetTimeout(() => { session.sendCommand(commandStopTracing(session.sessionId)); - }, 1000 * (options?.durationSeconds ?? 10)); + }, 1000 * (options?.durationSeconds ?? 60)); } setupJsClient({ From f144fb4174adfa5ac33876e6562deff7a81335f1 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 27 May 2026 18:25:27 +0200 Subject: [PATCH 5/6] revert EventPipeThreadSamplingRate --- src/coreclr/inc/clrconfigvalues.h | 2 +- src/coreclr/nativeaot/Runtime/eventpipe/ep-rt-aot.h | 2 +- src/mono/mono/eventpipe/ep-rt-mono.h | 2 +- src/native/eventpipe/ep.c | 4 +--- src/native/libs/System.Native.Browser/diagnostics/common.ts | 6 +++++- .../diagnostics/diagnostic-server-js.ts | 3 +++ 6 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/coreclr/inc/clrconfigvalues.h b/src/coreclr/inc/clrconfigvalues.h index 5339fa20f97b3c..3d457c8fd5460b 100644 --- a/src/coreclr/inc/clrconfigvalues.h +++ b/src/coreclr/inc/clrconfigvalues.h @@ -609,7 +609,7 @@ RETAIL_CONFIG_DWORD_INFO(INTERNAL_EventPipeCircularMB, W("EventPipeCircularMB"), RETAIL_CONFIG_DWORD_INFO(INTERNAL_EventPipeProcNumbers, W("EventPipeProcNumbers"), 0, "Enable/disable capturing processor numbers in EventPipe event headers") RETAIL_CONFIG_DWORD_INFO(INTERNAL_EventPipeOutputStreaming, W("EventPipeOutputStreaming"), 1, "Enable/disable streaming for trace file set in DOTNET_EventPipeOutputPath. Non-zero values enable streaming.") RETAIL_CONFIG_DWORD_INFO(INTERNAL_EventPipeEnableStackwalk, W("EventPipeEnableStackwalk"), 1, "Set to 0 to disable collecting stacks for EventPipe events.") -RETAIL_CONFIG_DWORD_INFO(INTERNAL_EventPipeThreadSamplingRate, W("EventPipeThreadSamplingRate"), UINT32_MAX, "Desired sample interval in milliseconds for EventPipe thread time sampling profiler. 0 means maximum frequency (sample every opportunity). Unset uses the default.") +RETAIL_CONFIG_DWORD_INFO(INTERNAL_EventPipeThreadSamplingRate, W("EventPipeThreadSamplingRate"), 0, "Desired sample interval in milliseconds for EventPipe thread time sampling profiler. 0 means use the default.") // // Generational Aware Analysis diff --git a/src/coreclr/nativeaot/Runtime/eventpipe/ep-rt-aot.h b/src/coreclr/nativeaot/Runtime/eventpipe/ep-rt-aot.h index de0c84b98aac37..c9ae8dcbd80c8e 100644 --- a/src/coreclr/nativeaot/Runtime/eventpipe/ep-rt-aot.h +++ b/src/coreclr/nativeaot/Runtime/eventpipe/ep-rt-aot.h @@ -521,7 +521,7 @@ ep_rt_config_value_get_sampling_rate (void) return static_cast(value); } - return UINT32_MAX; + return 0; } /* diff --git a/src/mono/mono/eventpipe/ep-rt-mono.h b/src/mono/mono/eventpipe/ep-rt-mono.h index b54b649c36b48b..3f8bc5fa51893f 100644 --- a/src/mono/mono/eventpipe/ep-rt-mono.h +++ b/src/mono/mono/eventpipe/ep-rt-mono.h @@ -642,7 +642,7 @@ inline uint32_t ep_rt_config_value_get_sampling_rate (void) { - uint32_t value_uint32_t = G_MAXUINT32; + uint32_t value_uint32_t = 0; gchar *value = g_getenv ("DOTNET_EventPipeThreadSamplingRate"); if (!value) value = g_getenv ("COMPlus_EventPipeThreadSamplingRate"); diff --git a/src/native/eventpipe/ep.c b/src/native/eventpipe/ep.c index cb7549f9c0e49b..9cf4acff6bdfed 100644 --- a/src/native/eventpipe/ep.c +++ b/src/native/eventpipe/ep.c @@ -1498,10 +1498,8 @@ ep_init (void) #endif // PERFTRACING_DISABLE_THREADS // Allow overriding the sampling rate via DOTNET_EventPipeThreadSamplingRate (in milliseconds). - // UINT32_MAX is the sentinel for "unset" (use default). 0 means maximum frequency - // (sample at every opportunity — 0 nanosecond interval). uint32_t configured_rate_ms = ep_rt_config_value_get_sampling_rate (); - if (configured_rate_ms != UINT32_MAX) + if (configured_rate_ms > 0) ep_sample_profiler_set_sampling_rate ((uint64_t)configured_rate_ms * 1000000); else ep_sample_profiler_set_sampling_rate (default_profiler_sample_rate_in_nanoseconds); diff --git a/src/native/libs/System.Native.Browser/diagnostics/common.ts b/src/native/libs/System.Native.Browser/diagnostics/common.ts index de90bc34ce86ec..46acae8e2e7484 100644 --- a/src/native/libs/System.Native.Browser/diagnostics/common.ts +++ b/src/native/libs/System.Native.Browser/diagnostics/common.ts @@ -4,6 +4,9 @@ import type { VoidPtr } from "../../Common/JavaScript/types/emscripten"; import { dotnetApi, dotnetLogger } from "./cross-module"; +// Minimum number of consumed entries before compacting the receive queue +const RECV_QUEUE_COMPACT_THRESHOLD = 128; + export class DiagnosticConnectionBase { protected messagesToSend: Uint8Array[] = []; protected messagesReceived: Uint8Array[] = []; @@ -30,7 +33,8 @@ export class DiagnosticConnectionBase { view.set(message.subarray(0, bytesRead), buffer as any >>> 0); if (bytesRead === message.length) { this.messagesReceivedHead++; - if (this.messagesReceivedHead > 128 && this.messagesReceivedHead >= (this.messagesReceived.length >>> 1)) { + // Compact when enough dead slots accumulate (>128) and they represent ≥50% of the array + if (this.messagesReceivedHead > RECV_QUEUE_COMPACT_THRESHOLD && this.messagesReceivedHead >= (this.messagesReceived.length >>> 1)) { this.messagesReceived = this.messagesReceived.slice(this.messagesReceivedHead); this.messagesReceivedHead = 0; } 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 index 3b952fca07b5a6..22384640be5328 100644 --- a/src/native/libs/System.Native.Browser/diagnostics/diagnostic-server-js.ts +++ b/src/native/libs/System.Native.Browser/diagnostics/diagnostic-server-js.ts @@ -150,6 +150,9 @@ export function setupJsClient(client: IDiagnosticClient, startup?: boolean) { throw new Error("Runtime is not running"); } if (startup) { + if (startupJsClient) { + throw new Error("startup diagnostic client already registered"); + } startupJsClient = client; } else { if (nextJsClient.isDone) { From 6af74d2bfe25f40749175f26fd720688e3e733f8 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Thu, 28 May 2026 13:10:27 +0200 Subject: [PATCH 6/6] nodeJS feedback --- src/native/libs/System.Native.Browser/diagnostics/index.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/native/libs/System.Native.Browser/diagnostics/index.ts b/src/native/libs/System.Native.Browser/diagnostics/index.ts index c90784df6d60d9..d4e0a52338de21 100644 --- a/src/native/libs/System.Native.Browser/diagnostics/index.ts +++ b/src/native/libs/System.Native.Browser/diagnostics/index.ts @@ -6,6 +6,7 @@ import { InternalExchangeIndex } from "../types"; import GitHash from "consts:gitHash"; +import { ENVIRONMENT_IS_WEB } from "./per-module"; import { dotnetApi, dotnetUpdateInternals, dotnetUpdateInternalsSubscriber, Module } from "./cross-module"; import { registerExit } from "./exit"; import { installNativeSymbols, symbolicateStackTrace } from "./symbolicate"; @@ -28,7 +29,10 @@ export function dotnetInitializeModule(internals: InternalExchange): void { globalThis.performance && typeof globalThis.performance.measure === "function" ? (namePtr: CharPtr, start: number) => { try { - globalThis.performance.measure(Module.UTF8ToString(namePtr), { start: start }); + const fnName = Module.UTF8ToString(namePtr); + // NodeJs accepts startTime, browsers accepts start + const options = ENVIRONMENT_IS_WEB ? { start: start } : { startTime: start }; + globalThis.performance.measure(fnName, options); } catch { // Ignore }