diff --git a/src/mono/mono/eventpipe/ep-rt-mono.h b/src/mono/mono/eventpipe/ep-rt-mono.h index a5f3c2626509ed..3f8bc5fa51893f 100644 --- a/src/mono/mono/eventpipe/ep-rt-mono.h +++ b/src/mono/mono/eventpipe/ep-rt-mono.h @@ -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/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..46acae8e2e7484 100644 --- a/src/native/libs/System.Native.Browser/diagnostics/common.ts +++ b/src/native/libs/System.Native.Browser/diagnostics/common.ts @@ -4,9 +4,13 @@ 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[] = []; + private messagesReceivedHead = 0; constructor(public clientSocket: number) { } @@ -16,21 +20,26 @@ 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++; + // 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; + } } 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..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 @@ -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,12 @@ class DiagnosticSession extends DiagnosticConnectionBase implements IDiagnosticC } async connectNewClient() { - this.diagClient = await nextJsClient.promise; + if (startupJsClient) { + this.diagClient = startupJsClient; + startupJsClient = undefined; + } else { + this.diagClient = await nextJsClient.promise; + } initializeJsClient(); const firstCommand = this.diagClient.commandOnAdvertise(); this.respond(firstCommand); @@ -136,29 +142,37 @@ 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) { + if (startupJsClient) { + throw new Error("startup diagnostic client already registered"); + } + 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..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 @@ -50,7 +50,12 @@ class DiagnosticConnectionWS extends DiagnosticConnectionBase implements IDiagno return super.store(message); } - this.ws!.send(message as any); + try { + this.ws!.send(message as any); + } catch { + dotnetLogger.warn("Diagnostic server WebSocket connection failed unexpectedly."); + 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..4a59c891e53381 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,11 @@ 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 ??= {}; + if (loaderConfig.environmentVariables![diagnosticPorts] === undefined) { 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..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 @@ -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,6 +19,7 @@ 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)); @@ -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..d4e0a52338de21 100644 --- a/src/native/libs/System.Native.Browser/diagnostics/index.ts +++ b/src/native/libs/System.Native.Browser/diagnostics/index.ts @@ -1,12 +1,13 @@ // 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 { ENVIRONMENT_IS_WEB } from "./per-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 +25,19 @@ 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) => { + try { + 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 + } + } + : () => { }; internals[InternalExchangeIndex.DiagnosticsExportsTable] = diagnosticsExportsToTable({ symbolicateStackTrace, @@ -33,6 +47,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 +71,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;