From e73537f2b4c3c98fbb975c919dcb834ad5eed8df Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Fri, 17 Jun 2022 13:25:59 -0400 Subject: [PATCH 01/84] [wasm] Enable the tracing component if threading is supported --- src/mono/wasm/build/WasmApp.InTree.props | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/mono/wasm/build/WasmApp.InTree.props b/src/mono/wasm/build/WasmApp.InTree.props index 125c4fb81f5926..4949caa40e6eda 100644 --- a/src/mono/wasm/build/WasmApp.InTree.props +++ b/src/mono/wasm/build/WasmApp.InTree.props @@ -15,7 +15,8 @@ <_MonoRuntimeComponentDontLink Include="libmono-component-debugger-stub-static.a" /> - <_MonoRuntimeComponentDontLink Include="libmono-component-diagnostics_tracing-static.a" Condition="'$(FeatureWasmPerfTracing)' != 'true'"/> + + <_MonoRuntimeComponentDontLink Include="libmono-component-diagnostics_tracing-static.a" Condition="'$(FeatureWasmPerfTracing)' != 'true' and $(FeatureWasmThreads) != 'true'"/> <_MonoRuntimeComponentDontLink Include="libmono-component-hot_reload-stub-static.a" /> From 8c3360a7f5ebb14b014021dd8b484fba023d2aac Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Mon, 23 May 2022 16:28:07 -0400 Subject: [PATCH 02/84] WIP: add a way to specify EP sessions in the MonoConfig Currently not wired up to the runtime --- src/mono/wasm/runtime/diagnostics.ts | 43 ++++++++++++++++++---------- src/mono/wasm/runtime/startup.ts | 4 +++ src/mono/wasm/runtime/types.ts | 32 +++++++++++++++++++++ 3 files changed, 64 insertions(+), 15 deletions(-) diff --git a/src/mono/wasm/runtime/diagnostics.ts b/src/mono/wasm/runtime/diagnostics.ts index d346a9865468e0..050687db191760 100644 --- a/src/mono/wasm/runtime/diagnostics.ts +++ b/src/mono/wasm/runtime/diagnostics.ts @@ -3,27 +3,14 @@ import { Module } from "./imports"; import cwraps from "./cwraps"; -import type { EventPipeSessionOptions } from "./types"; +import type { EventPipeSessionID, DiagnosticOptions, EventPipeSession, EventPipeSessionOptions } from "./types"; import type { VoidPtr } from "./types/emscripten"; import * as memory from "./memory"; const sizeOfInt32 = 4; -export type EventPipeSessionID = bigint; type EventPipeSessionIDImpl = number; -/// An EventPipe session object represents a single diagnostic tracing session that is collecting -/// events from the runtime and managed libraries. There may be multiple active sessions at the same time. -/// Each session subscribes to a number of providers and will collect events from the time that start() is called, until stop() is called. -/// Upon completion the session saves the events to a file on the VFS. -/// The data can then be retrieved as Blob. -export interface EventPipeSession { - // session ID for debugging logging only - get sessionID(): EventPipeSessionID; - start(): void; - stop(): void; - getTraceBlob(): Blob; -} // internal session state of the JS instance enum State { @@ -42,7 +29,7 @@ function stop_streaming(sessionID: EventPipeSessionIDImpl): void { /// An EventPipe session that saves the event data to a file in the VFS. class EventPipeFileSession implements EventPipeSession { - private _state: State; + protected _state: State; private _sessionID: EventPipeSessionIDImpl; private _tracePath: string; // VFS file path to the trace file @@ -82,6 +69,25 @@ class EventPipeFileSession implements EventPipeSession { } } +// an EventPipeSession that starts at runtime startup +class StartupEventPipeFileSession extends EventPipeFileSession implements EventPipeSession { + readonly _on_stop_callback: null | ((session: EventPipeSession) => void); + constructor(sessionID: EventPipeSessionIDImpl, tracePath: string, on_stop_callback?: (session: EventPipeSession) => void) { + super(sessionID, tracePath); + // By the time we create the JS object, it's already running + this._state = State.Started; + this._on_stop_callback = on_stop_callback ?? null; + } + + stop = () => { + super.stop(); + if (this._on_stop_callback !== null) { + const cb = this._on_stop_callback; + setTimeout(cb, 0, this); + } + } +} + const eventLevel = { LogAlways: 0, Critical: 1, @@ -257,4 +263,11 @@ export const diagnostics: Diagnostics = { }, }; +export function mono_wasm_init_diagnostics(config?: DiagnosticOptions): void { + const sessions = config?.sessions ?? []; + sessions.forEach(session => { + console.log("Starting session ", session); + }); +} + export default diagnostics; diff --git a/src/mono/wasm/runtime/startup.ts b/src/mono/wasm/runtime/startup.ts index 3b3ec8f14869fd..c2d91222d1d252 100644 --- a/src/mono/wasm/runtime/startup.ts +++ b/src/mono/wasm/runtime/startup.ts @@ -10,6 +10,7 @@ import GuardedPromise from "./guarded-promise"; import { mono_wasm_globalization_init, mono_wasm_load_icu_data } from "./icu"; import { toBase64StringImpl } from "./base64"; import { mono_wasm_init_aot_profiler, mono_wasm_init_coverage_profiler } from "./profiler"; +import { mono_wasm_init_diagnostics } from "./diagnostics"; import { mono_wasm_load_bytes_into_heap } from "./buffers"; import { bind_runtime_method, get_method, _create_primitive_converters } from "./method-binding"; import { find_corlib_class } from "./class-loader"; @@ -314,6 +315,9 @@ function _apply_configuration_from_args(config: MonoConfig) { if (config.coverage_profiler_options) mono_wasm_init_coverage_profiler(config.coverage_profiler_options); + + if (config.diagnostic_options) + mono_wasm_init_diagnostics(config.diagnostic_options); } function finalize_startup(config: MonoConfig | MonoConfigError | undefined): void { diff --git a/src/mono/wasm/runtime/types.ts b/src/mono/wasm/runtime/types.ts index 8e3970722aa92b..4960b3120c6f9c 100644 --- a/src/mono/wasm/runtime/types.ts +++ b/src/mono/wasm/runtime/types.ts @@ -79,6 +79,7 @@ export type MonoConfig = { runtime_options?: string[], // array of runtime options as strings aot_profiler_options?: AOTProfilerOptions, // dictionary-style Object. If omitted, aot profiler will not be initialized. coverage_profiler_options?: CoverageProfilerOptions, // dictionary-style Object. If omitted, coverage profiler will not be initialized. + diagnostic_options?: DiagnosticOptions, // dictionary-style Object. If omitted, diagnostics will not be initialized. ignore_pdb_load_errors?: boolean, wait_for_debugger?: number }; @@ -180,6 +181,20 @@ export type CoverageProfilerOptions = { send_to?: string // should be in the format ::, default: 'WebAssembly.Runtime::DumpCoverageProfileData' (DumpCoverageProfileData stores the data into INTERNAL.coverage_profile_data.) } +/// Options to configure EventPipe sessions that will be created and started at runtime startup +export type DiagnosticOptions = { + sessions?: (EventPipeSessionOptions & EventPipeSessionAutoStopOptions)[] +} + +/// For EventPipe sessions that will be created and started at runtime startup +export interface EventPipeSessionAutoStopOptions { + /// Should be in the format ::, default: 'WebAssembly.Runtime::StopProfile' + /// The session will be stopped when the jit_done event is fired for this method. + stop_at?: string; + /// Called after the session has been stopped. + on_session_stopped?: (session: EventPipeSession) => void; +} + /// Options to configure the event pipe session /// The recommended method is to MONO.diagnostics.SesisonOptionsBuilder to create an instance of this type export interface EventPipeSessionOptions { @@ -286,3 +301,20 @@ export const enum MarshalError { export function is_nullish(value: T | null | undefined): value is null | undefined { return (value === undefined) || (value === null); } + +/// An identifier for an EventPipe session. The id is unique during the lifetime of the runtime. +/// Primarily intended for debugging purposes. +export type EventPipeSessionID = bigint; + +/// An EventPipe session object represents a single diagnostic tracing session that is collecting +/// events from the runtime and managed libraries. There may be multiple active sessions at the same time. +/// Each session subscribes to a number of providers and will collect events from the time that start() is called, until stop() is called. +/// Upon completion the session saves the events to a file on the VFS. +/// The data can then be retrieved as Blob. +export interface EventPipeSession { + // session ID for debugging logging only + get sessionID(): EventPipeSessionID; + start(): void; + stop(): void; + getTraceBlob(): Blob; +} From 6334ee7b68bb8c4d6c4ae38e0468a45760acb63e Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Mon, 23 May 2022 16:48:27 -0400 Subject: [PATCH 03/84] Add a mechanism to copy startup configs into the runtime and session IDs out --- src/mono/wasm/runtime/cwraps.ts | 4 ++++ src/mono/wasm/runtime/diagnostics.ts | 11 ++++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/mono/wasm/runtime/cwraps.ts b/src/mono/wasm/runtime/cwraps.ts index b2963790bd20ff..206c16a6cdf9ac 100644 --- a/src/mono/wasm/runtime/cwraps.ts +++ b/src/mono/wasm/runtime/cwraps.ts @@ -71,6 +71,8 @@ const fn_signatures: [ident: string, returnType: string | null, argTypes?: strin ["mono_wasm_event_pipe_enable", "bool", ["string", "number", "string", "bool", "number"]], ["mono_wasm_event_pipe_session_start_streaming", "bool", ["number"]], ["mono_wasm_event_pipe_session_disable", "bool", ["number"]], + ["mono_wasm_event_pipe_session_set_startup_sessions", null, ["number", "number"]], + ["mono_wasm_event_pipe_session_get_startup_session_ids", null, ["number", "number"]], //DOTNET ["mono_wasm_string_from_js", "number", ["string"]], @@ -172,6 +174,8 @@ export interface t_Cwraps { mono_wasm_event_pipe_enable(outputPath: string, bufferSizeInMB: number, providers: string, rundownRequested: boolean, outSessionId: VoidPtr): boolean; mono_wasm_event_pipe_session_start_streaming(sessionId: number): boolean; mono_wasm_event_pipe_session_disable(sessionId: number): boolean; + mono_wasm_event_pipe_session_set_startup_sessions(count: number, sessionStrings: VoidPtr): void; + mono_wasm_event_pipe_session_get_startup_settion_ids(count: number, sessionIdOutArray: VoidPtr): void; //DOTNET /** diff --git a/src/mono/wasm/runtime/diagnostics.ts b/src/mono/wasm/runtime/diagnostics.ts index 050687db191760..b8c58b4d073bf1 100644 --- a/src/mono/wasm/runtime/diagnostics.ts +++ b/src/mono/wasm/runtime/diagnostics.ts @@ -265,9 +265,14 @@ export const diagnostics: Diagnostics = { export function mono_wasm_init_diagnostics(config?: DiagnosticOptions): void { const sessions = config?.sessions ?? []; - sessions.forEach(session => { - console.log("Starting session ", session); - }); + const count = sessions.length; + const sessionConfigs = Module._malloc(sizeOfInt32 * count); + for (let i = 0; i < count; ++i) { + const session = sessions[i]; + const sessionPtr = (sessionConfigs + i * sizeOfInt32); + memory.setI32(sessionPtr, cwraps.mono_wasm_strdup(session.providers)); + } + cwraps.mono_wasm_event_pipe_session_set_startup_sessions(count, sessionConfigs); } export default diagnostics; From 48972413e372d6ac270628697dce92570310bde0 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Tue, 31 May 2022 09:54:08 -0400 Subject: [PATCH 04/84] WIP: C side startup provider copying --- src/mono/mono/component/event_pipe-stub.c | 11 ++++++++ src/mono/mono/component/event_pipe-wasm.h | 6 ++++ src/mono/mono/component/event_pipe.c | 34 +++++++++++++++++++++++ src/mono/wasm/runtime/dotnet.d.ts | 10 ++++++- 4 files changed, 60 insertions(+), 1 deletion(-) diff --git a/src/mono/mono/component/event_pipe-stub.c b/src/mono/mono/component/event_pipe-stub.c index 3f01b911cee7c3..caa7695a77c7d4 100644 --- a/src/mono/mono/component/event_pipe-stub.c +++ b/src/mono/mono/component/event_pipe-stub.c @@ -552,4 +552,15 @@ mono_wasm_event_pipe_session_disable (MonoWasmEventPipeSessionID session_id) g_assert_not_reached (); } +void +mono_wasm_event_pipe_session_set_startup_sessions (uint32_t count, const char **provider_configs) +{ +} + +void +mono_wasm_event_pipe_session_get_startup_session_ids (uint32_t count, uint32_t *session_id_dest) +{ +} + + #endif /* HOST_WASM */ diff --git a/src/mono/mono/component/event_pipe-wasm.h b/src/mono/mono/component/event_pipe-wasm.h index 90e4a4d8b99482..314814aec18ff3 100644 --- a/src/mono/mono/component/event_pipe-wasm.h +++ b/src/mono/mono/component/event_pipe-wasm.h @@ -43,6 +43,12 @@ mono_wasm_event_pipe_session_start_streaming (MonoWasmEventPipeSessionID session EMSCRIPTEN_KEEPALIVE gboolean mono_wasm_event_pipe_session_disable (MonoWasmEventPipeSessionID session_id); +EMSCRIPTEN_KEEPALIVE void +mono_wasm_event_pipe_session_set_startup_sessions (uint32_t count, const char **provider_configs); + +EMSCRIPTEN_KEEPALIVE void +mono_wasm_event_pipe_session_get_startup_session_ids (uint32_t count, uint32_t *session_id_dest); + G_END_DECLS #endif /* HOST_WASM */ diff --git a/src/mono/mono/component/event_pipe.c b/src/mono/mono/component/event_pipe.c index f0c4b24583ba20..da1aa7e962915a 100644 --- a/src/mono/mono/component/event_pipe.c +++ b/src/mono/mono/component/event_pipe.c @@ -98,6 +98,9 @@ event_pipe_wait_for_session_signal ( EventPipeSessionID session_id, uint32_t timeout); +static void +event_pipe_create_and_start_startup_sessions (void); + static MonoComponentEventPipe fn_table = { { MONO_COMPONENT_ITF_VERSION, &event_pipe_available }, &ep_init, @@ -228,6 +231,8 @@ event_pipe_add_rundown_execution_checkpoint_2 ( const ep_char8_t *name, ep_timestamp_t timestamp) { + // If WASM installed any startup session provider configs, start those sessions and record the session IDs + event_pipe_create_and_start_startup_sessions(); return ep_add_rundown_execution_checkpoint (name, timestamp); } @@ -403,4 +408,33 @@ mono_wasm_event_pipe_session_disable (MonoWasmEventPipeSessionID session_id) return TRUE; } +static uint32_t startup_session_provider_configs_count; +static const char **startup_session_provider_configs; +static uint32_t *startup_session_ids; + +void +event_pipe_create_and_start_startup_sessions (void) +{ + uint32_t count = startup_session_provider_configs_count; + const char **configs = startup_session_provider_configs; + uint32_t *ids = g_new0 ( +} + +void +mono_wasm_event_pipe_session_set_startup_sessions (uint32_t count, const char **provider_configs) +{ + g_assert (count == 0 || provider_configs != NULL); + startup_session_provider_configs_counts = count; + startup_session_provider_configs = provider_configs; +} + +void +mono_wasm_event_pipe_session_get_startup_session_ids (uint32_t count, uint32_t *session_id_dest) +{ + g_assert (session_id_dest); + for (uint32_t i = 0; i < count; ++i) { + session_id_dest [i] = NULL; /* FIXME: create the sessions and install them */ + } +} + #endif /* HOST_WASM */ diff --git a/src/mono/wasm/runtime/dotnet.d.ts b/src/mono/wasm/runtime/dotnet.d.ts index 4b0d7826fde5e3..71728e392bfa77 100644 --- a/src/mono/wasm/runtime/dotnet.d.ts +++ b/src/mono/wasm/runtime/dotnet.d.ts @@ -155,6 +155,7 @@ declare type MonoConfig = { runtime_options?: string[]; aot_profiler_options?: AOTProfilerOptions; coverage_profiler_options?: CoverageProfilerOptions; + diagnostic_options?: DiagnosticOptions; ignore_pdb_load_errors?: boolean; wait_for_debugger?: number; }; @@ -208,6 +209,13 @@ declare type CoverageProfilerOptions = { write_at?: string; send_to?: string; }; +declare type DiagnosticOptions = { + sessions?: (EventPipeSessionOptions & EventPipeSessionAutoStopOptions)[]; +}; +interface EventPipeSessionAutoStopOptions { + stop_at?: string; + on_session_stopped?: (session: EventPipeSession) => void; +} interface EventPipeSessionOptions { collectRundownEvents?: boolean; providers: string; @@ -242,7 +250,6 @@ declare type DotnetModuleConfigImports = { }; url?: any; }; - declare type EventPipeSessionID = bigint; interface EventPipeSession { get sessionID(): EventPipeSessionID; @@ -250,6 +257,7 @@ interface EventPipeSession { stop(): void; getTraceBlob(): Blob; } + declare const eventLevel: { readonly LogAlways: 0; readonly Critical: 1; From 43d6ec8a31d610343728d57bd9c9bf5079ed753f Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Thu, 2 Jun 2022 16:34:41 -0400 Subject: [PATCH 05/84] WIP checkpoint. Do more from JS The issue is that once we're setting up streaming sessions, we will need to send back a DS IPC reply with the session id before we start streaming. So it's better to just call back to JS when we start up and setup all the EP sessions from JS so that when we return to C everything is all ready. --- src/mono/mono/component/event_pipe-stub.c | 2 +- src/mono/mono/component/event_pipe-wasm.h | 9 +++- src/mono/mono/component/event_pipe.c | 44 ++++++++++++++--- src/mono/wasm/runtime/cjs/dotnet.cjs.lib.js | 1 + src/mono/wasm/runtime/cwraps.ts | 6 +-- src/mono/wasm/runtime/diagnostics.ts | 53 ++++++++++++--------- src/mono/wasm/runtime/driver.c | 11 ++++- src/mono/wasm/runtime/es6/dotnet.es6.lib.js | 1 + src/mono/wasm/runtime/exports.ts | 4 ++ 9 files changed, 94 insertions(+), 37 deletions(-) diff --git a/src/mono/mono/component/event_pipe-stub.c b/src/mono/mono/component/event_pipe-stub.c index caa7695a77c7d4..358aa9ac181a32 100644 --- a/src/mono/mono/component/event_pipe-stub.c +++ b/src/mono/mono/component/event_pipe-stub.c @@ -553,7 +553,7 @@ mono_wasm_event_pipe_session_disable (MonoWasmEventPipeSessionID session_id) } void -mono_wasm_event_pipe_session_set_startup_sessions (uint32_t count, const char **provider_configs) +mono_wasm_event_pipe_session_set_startup_sessions (uint32_t count, const char **provider_configs, mono_wasm_event_pipe_startup_session_prestreaming_cb callback) { } diff --git a/src/mono/mono/component/event_pipe-wasm.h b/src/mono/mono/component/event_pipe-wasm.h index 314814aec18ff3..6bba1e4c822fb8 100644 --- a/src/mono/mono/component/event_pipe-wasm.h +++ b/src/mono/mono/component/event_pipe-wasm.h @@ -25,6 +25,10 @@ typedef uint32_t MonoWasmEventPipeSessionID; #error "EventPipeSessionID is 64-bits, update the JS side to work with it" #endif +typedef void (*mono_wasm_event_pipe_early_startup_cb)(void); + +typedef void (*mono_wasm_event_pipe_startup_session_prestreaming_cb)(uint32_t i, MonoWasmEventPipeSessionID session); + EMSCRIPTEN_KEEPALIVE gboolean mono_wasm_event_pipe_enable (const ep_char8_t *output_path, uint32_t circular_buffer_size_in_mb, @@ -43,8 +47,11 @@ mono_wasm_event_pipe_session_start_streaming (MonoWasmEventPipeSessionID session EMSCRIPTEN_KEEPALIVE gboolean mono_wasm_event_pipe_session_disable (MonoWasmEventPipeSessionID session_id); +void +mono_wasm_event_pipe_set_early_startup_callback (mono_wasm_event_pipe_early_startup_cb callback); + EMSCRIPTEN_KEEPALIVE void -mono_wasm_event_pipe_session_set_startup_sessions (uint32_t count, const char **provider_configs); +mono_wasm_event_pipe_session_set_startup_sessions (uint32_t count, const char **provider_configs, mono_wasm_event_pipe_startup_session_prestreaming_cb callback); EMSCRIPTEN_KEEPALIVE void mono_wasm_event_pipe_session_get_startup_session_ids (uint32_t count, uint32_t *session_id_dest); diff --git a/src/mono/mono/component/event_pipe.c b/src/mono/mono/component/event_pipe.c index da1aa7e962915a..ec073aa33238f6 100644 --- a/src/mono/mono/component/event_pipe.c +++ b/src/mono/mono/component/event_pipe.c @@ -410,31 +410,61 @@ mono_wasm_event_pipe_session_disable (MonoWasmEventPipeSessionID session_id) static uint32_t startup_session_provider_configs_count; static const char **startup_session_provider_configs; -static uint32_t *startup_session_ids; +static MonoWasmEventPipeSessionID *startup_session_ids; +static mono_wasm_event_pipe_startup_session_prestreaming_cb startup_session_prestreaming_callback; void event_pipe_create_and_start_startup_sessions (void) { + /* FIXME: what if we don't do this in C? */ + uint32_t count = startup_session_provider_configs_count; const char **configs = startup_session_provider_configs; - uint32_t *ids = g_new0 ( + if (!count) + return; + uint32_t *ids = g_new0 (MonoWasmEventPipeSessionID, count); + for (uint32_t i = 0; i < count; i++) { + EventPipeSessionID session; + EventPipeSerializationFormat format = EP_SERIALIZATION_FORMAT_NETTRACE_V4; + EventPipeSessionType session_type = EP_SESSION_TYPE_FILE; + char *output_path = NULL; /* FIXME for file sessions, set it to something */ + + session = ep_enable_2 (output_path, + circular_buffer_size_in_mb, + providers, + session_type, + format, + !!rundown_requested, + /* stream */NULL, + /* callback*/ NULL, + /* callback_data*/ NULL); + ids[i] = ep_to_wasm_session_id (session); + + /* FIXME: for streaming we need to send back a DS IPC reply before we start sending + * the data, so enabling here is too early. */ + g_assert (session_type != EP_SESSION_TYPE_IPCSTREAM); + if (startup_session_prestreaming_callback != NULL) + startup_session_prestreaming_callback (i, ids[i]); + ep_start_streaming (session); + } + startup_session_ids = ids; } void -mono_wasm_event_pipe_session_set_startup_sessions (uint32_t count, const char **provider_configs) +mono_wasm_event_pipe_session_set_startup_sessions (uint32_t count, const char **provider_configs, mono_wasm_event_pipe_startup_session_prestreaming_cb callback) { g_assert (count == 0 || provider_configs != NULL); startup_session_provider_configs_counts = count; startup_session_provider_configs = provider_configs; + startup_session_prestreaming_callback = callback; } void -mono_wasm_event_pipe_session_get_startup_session_ids (uint32_t count, uint32_t *session_id_dest) +mono_wasm_event_pipe_session_get_startup_session_ids (uint32_t count, MonoWasmEventPipeSessionID *session_id_dest) { g_assert (session_id_dest); - for (uint32_t i = 0; i < count; ++i) { - session_id_dest [i] = NULL; /* FIXME: create the sessions and install them */ - } + g_assert (count <= startup_session_provider_configs_count); + memcpy (session_id_dest, startup_session_ids, count * sizeof (MonoWasmEventPipeSessionID)); } #endif /* HOST_WASM */ diff --git a/src/mono/wasm/runtime/cjs/dotnet.cjs.lib.js b/src/mono/wasm/runtime/cjs/dotnet.cjs.lib.js index 4aca26762484d2..7bdb1d25418077 100644 --- a/src/mono/wasm/runtime/cjs/dotnet.cjs.lib.js +++ b/src/mono/wasm/runtime/cjs/dotnet.cjs.lib.js @@ -59,6 +59,7 @@ const linked_functions = [ "mono_wasm_invoke_js_blazor", "mono_wasm_trace_logger", "mono_wasm_set_entrypoint_breakpoint", + "mono_wasm_event_pipe_early_startup_callback", // corebindings.c "mono_wasm_invoke_js_with_args_ref", diff --git a/src/mono/wasm/runtime/cwraps.ts b/src/mono/wasm/runtime/cwraps.ts index 206c16a6cdf9ac..7fe0280b1e5842 100644 --- a/src/mono/wasm/runtime/cwraps.ts +++ b/src/mono/wasm/runtime/cwraps.ts @@ -71,8 +71,6 @@ const fn_signatures: [ident: string, returnType: string | null, argTypes?: strin ["mono_wasm_event_pipe_enable", "bool", ["string", "number", "string", "bool", "number"]], ["mono_wasm_event_pipe_session_start_streaming", "bool", ["number"]], ["mono_wasm_event_pipe_session_disable", "bool", ["number"]], - ["mono_wasm_event_pipe_session_set_startup_sessions", null, ["number", "number"]], - ["mono_wasm_event_pipe_session_get_startup_session_ids", null, ["number", "number"]], //DOTNET ["mono_wasm_string_from_js", "number", ["string"]], @@ -174,8 +172,6 @@ export interface t_Cwraps { mono_wasm_event_pipe_enable(outputPath: string, bufferSizeInMB: number, providers: string, rundownRequested: boolean, outSessionId: VoidPtr): boolean; mono_wasm_event_pipe_session_start_streaming(sessionId: number): boolean; mono_wasm_event_pipe_session_disable(sessionId: number): boolean; - mono_wasm_event_pipe_session_set_startup_sessions(count: number, sessionStrings: VoidPtr): void; - mono_wasm_event_pipe_session_get_startup_settion_ids(count: number, sessionIdOutArray: VoidPtr): void; //DOTNET /** @@ -225,4 +221,4 @@ export const enum I52Error { NONE = 0, NON_INTEGRAL = 1, OUT_OF_RANGE = 2, -} \ No newline at end of file +} diff --git a/src/mono/wasm/runtime/diagnostics.ts b/src/mono/wasm/runtime/diagnostics.ts index b8c58b4d073bf1..682e8071f84dc1 100644 --- a/src/mono/wasm/runtime/diagnostics.ts +++ b/src/mono/wasm/runtime/diagnostics.ts @@ -3,7 +3,7 @@ import { Module } from "./imports"; import cwraps from "./cwraps"; -import type { EventPipeSessionID, DiagnosticOptions, EventPipeSession, EventPipeSessionOptions } from "./types"; +import type { EventPipeSessionID, DiagnosticOptions, EventPipeSession, EventPipeSessionOptions, EventPipeSessionAutoStopOptions } from "./types"; import type { VoidPtr } from "./types/emscripten"; import * as memory from "./memory"; @@ -229,8 +229,34 @@ export interface Diagnostics { SessionOptionsBuilder: typeof SessionOptionsBuilder; createEventPipeSession(options?: EventPipeSessionOptions): EventPipeSession | null; + getStartupSessions(): (EventPipeSession | null)[]; } +let startup_session_configs: (EventPipeSessionOptions & EventPipeSessionAutoStopOptions)[] | null = null; +let startup_sessions: (EventPipeSession | null)[] | null = null; + +export function mono_wasm_event_pipe_early_startup_callback(): void { + if (startup_session_configs === null || startup_session_configs.length == 0) { + return; + } + startup_sessions = startup_session_configs.map(config => createEventPipeSession(config)); + startup_session_configs = null; +} + +function createEventPipeSession(options?: EventPipeSessionOptions): EventPipeSession | null { + // The session trace is saved to a file in the VFS. The file name doesn't matter, + // but we'd like it to be distinct from other traces. + const tracePath = `/trace-${totalSessions++}.nettrace`; + + const success = memory.withStackAlloc(sizeOfInt32, createSessionWithPtrCB, options, tracePath); + + if (success === false) + return null; + const sessionID = success; + + const session = new EventPipeFileSession(sessionID, tracePath); + return session; +} /// APIs for working with .NET diagnostics from JavaScript. export const diagnostics: Diagnostics = { @@ -247,32 +273,15 @@ export const diagnostics: Diagnostics = { /// Creates a new EventPipe session that will collect trace events from the runtime and managed libraries. /// Use the options to control the kinds of events to be collected. /// Multiple sessions may be created and started at the same time. - createEventPipeSession(options?: EventPipeSessionOptions): EventPipeSession | null { - // The session trace is saved to a file in the VFS. The file name doesn't matter, - // but we'd like it to be distinct from other traces. - const tracePath = `/trace-${totalSessions++}.nettrace`; - - const success = memory.withStackAlloc(sizeOfInt32, createSessionWithPtrCB, options, tracePath); - - if (success === false) - return null; - const sessionID = success; - - const session = new EventPipeFileSession(sessionID, tracePath); - return session; + createEventPipeSession: createEventPipeSession, + getStartupSessions(): (EventPipeSession | null)[] { + return Array.from(startup_sessions || []); }, }; export function mono_wasm_init_diagnostics(config?: DiagnosticOptions): void { const sessions = config?.sessions ?? []; - const count = sessions.length; - const sessionConfigs = Module._malloc(sizeOfInt32 * count); - for (let i = 0; i < count; ++i) { - const session = sessions[i]; - const sessionPtr = (sessionConfigs + i * sizeOfInt32); - memory.setI32(sessionPtr, cwraps.mono_wasm_strdup(session.providers)); - } - cwraps.mono_wasm_event_pipe_session_set_startup_sessions(count, sessionConfigs); + startup_session_configs = sessions; } export default diagnostics; diff --git a/src/mono/wasm/runtime/driver.c b/src/mono/wasm/runtime/driver.c index e0981a5c55d15a..a14c0277e1e7e3 100644 --- a/src/mono/wasm/runtime/driver.c +++ b/src/mono/wasm/runtime/driver.c @@ -47,6 +47,9 @@ extern void mono_wasm_set_entrypoint_breakpoint (const char* assembly_name, int // Blazor specific custom routines - see dotnet_support.js for backing code extern void* mono_wasm_invoke_js_blazor (MonoString **exceptionMessage, void *callInfo, void* arg0, void* arg1, void* arg2); +// JS callback to invoke early during runtime initialization once eventpipe is functional but before too much of the rest of the runtime is loaded. +extern void mono_wasm_event_pipe_early_startup_callback (void); + void mono_wasm_enable_debugging (int); static int _marshal_type_from_mono_type (int mono_type, MonoClass *klass, MonoType *type); @@ -68,6 +71,10 @@ void mono_free (void*); int32_t mini_parse_debug_option (const char *option); char *mono_method_get_full_name (MonoMethod *method); +typedef void (*mono_wasm_event_pipe_early_startup_cb)(void); + +void mono_wasm_event_pipe_set_early_startup_callback (mono_wasm_event_pipe_early_startup_cb callback); + static void mono_wasm_init_finalizer_thread (void); #define MARSHAL_TYPE_NULL 0 @@ -530,6 +537,8 @@ mono_wasm_load_runtime (const char *unused, int debug_level) free (file_path); } + mono_wasm_event_pipe_set_early_startup_callback (mono_wasm_event_pipe_early_startup_callback); + monovm_initialize (2, appctx_keys, appctx_values); mini_parse_debug_option ("top-runtime-invoke-unhandled"); @@ -1467,4 +1476,4 @@ EMSCRIPTEN_KEEPALIVE int mono_wasm_f64_to_i52 (int64_t *destination, double valu *destination = (int64_t)value; return I52_ERROR_NONE; -} \ No newline at end of file +} diff --git a/src/mono/wasm/runtime/es6/dotnet.es6.lib.js b/src/mono/wasm/runtime/es6/dotnet.es6.lib.js index fe431d319e1f7a..d88b21341f7c1c 100644 --- a/src/mono/wasm/runtime/es6/dotnet.es6.lib.js +++ b/src/mono/wasm/runtime/es6/dotnet.es6.lib.js @@ -96,6 +96,7 @@ const linked_functions = [ // driver.c "mono_wasm_invoke_js_blazor", "mono_wasm_trace_logger", + "mono_wasm_event_pipe_early_startup_callback", // corebindings.c "mono_wasm_invoke_js_with_args_ref", diff --git a/src/mono/wasm/runtime/exports.ts b/src/mono/wasm/runtime/exports.ts index f12c35dd9d3191..96f41f27ca689a 100644 --- a/src/mono/wasm/runtime/exports.ts +++ b/src/mono/wasm/runtime/exports.ts @@ -53,6 +53,9 @@ import { mono_wasm_invoke_js_blazor, mono_wasm_invoke_js_with_args_ref, mono_wasm_set_by_index_ref, mono_wasm_set_object_property_ref } from "./method-calls"; +import { + mono_wasm_event_pipe_early_startup_callback +} from "./diagnostics"; import { mono_wasm_typed_array_copy_to_ref, mono_wasm_typed_array_from_ref, mono_wasm_typed_array_copy_from_ref, mono_wasm_load_bytes_into_heap } from "./buffers"; import { mono_wasm_release_cs_owned_object } from "./gc-handles"; import cwraps from "./cwraps"; @@ -382,6 +385,7 @@ export const __linker_exports: any = { mono_wasm_invoke_js_blazor, mono_wasm_trace_logger, mono_wasm_set_entrypoint_breakpoint, + mono_wasm_event_pipe_early_startup_callback, // also keep in sync with corebindings.c mono_wasm_invoke_js_with_args_ref, From b232adb6aeed0da4cc47cb85e2b1e744730e71ba Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Fri, 3 Jun 2022 12:33:30 -0400 Subject: [PATCH 06/84] checkpoint: starting a session at startup works --- src/mono/mono/component/event_pipe-stub.c | 8 +-- src/mono/mono/component/event_pipe-wasm.h | 8 --- src/mono/mono/component/event_pipe.c | 64 ++++--------------- .../Wasm.Browser.EventPipe.Sample.csproj | 4 ++ .../sample/wasm/browser-eventpipe/main.js | 31 ++++++--- src/mono/wasm/runtime/diagnostics.ts | 22 ++++++- src/mono/wasm/runtime/dotnet.d.ts | 2 + src/mono/wasm/runtime/types.ts | 1 + 8 files changed, 63 insertions(+), 77 deletions(-) diff --git a/src/mono/mono/component/event_pipe-stub.c b/src/mono/mono/component/event_pipe-stub.c index 358aa9ac181a32..0d2371b0119823 100644 --- a/src/mono/mono/component/event_pipe-stub.c +++ b/src/mono/mono/component/event_pipe-stub.c @@ -553,14 +553,8 @@ mono_wasm_event_pipe_session_disable (MonoWasmEventPipeSessionID session_id) } void -mono_wasm_event_pipe_session_set_startup_sessions (uint32_t count, const char **provider_configs, mono_wasm_event_pipe_startup_session_prestreaming_cb callback) +mono_wasm_event_pipe_set_early_startup_callback (mono_wasm_event_pipe_early_startup_cb callback) { } -void -mono_wasm_event_pipe_session_get_startup_session_ids (uint32_t count, uint32_t *session_id_dest) -{ -} - - #endif /* HOST_WASM */ diff --git a/src/mono/mono/component/event_pipe-wasm.h b/src/mono/mono/component/event_pipe-wasm.h index 6bba1e4c822fb8..bc9ac163cf3a92 100644 --- a/src/mono/mono/component/event_pipe-wasm.h +++ b/src/mono/mono/component/event_pipe-wasm.h @@ -27,8 +27,6 @@ typedef uint32_t MonoWasmEventPipeSessionID; typedef void (*mono_wasm_event_pipe_early_startup_cb)(void); -typedef void (*mono_wasm_event_pipe_startup_session_prestreaming_cb)(uint32_t i, MonoWasmEventPipeSessionID session); - EMSCRIPTEN_KEEPALIVE gboolean mono_wasm_event_pipe_enable (const ep_char8_t *output_path, uint32_t circular_buffer_size_in_mb, @@ -50,12 +48,6 @@ mono_wasm_event_pipe_session_disable (MonoWasmEventPipeSessionID session_id); void mono_wasm_event_pipe_set_early_startup_callback (mono_wasm_event_pipe_early_startup_cb callback); -EMSCRIPTEN_KEEPALIVE void -mono_wasm_event_pipe_session_set_startup_sessions (uint32_t count, const char **provider_configs, mono_wasm_event_pipe_startup_session_prestreaming_cb callback); - -EMSCRIPTEN_KEEPALIVE void -mono_wasm_event_pipe_session_get_startup_session_ids (uint32_t count, uint32_t *session_id_dest); - G_END_DECLS #endif /* HOST_WASM */ diff --git a/src/mono/mono/component/event_pipe.c b/src/mono/mono/component/event_pipe.c index ec073aa33238f6..01f0560b4bf503 100644 --- a/src/mono/mono/component/event_pipe.c +++ b/src/mono/mono/component/event_pipe.c @@ -98,8 +98,10 @@ event_pipe_wait_for_session_signal ( EventPipeSessionID session_id, uint32_t timeout); +#ifdef HOST_WASM static void -event_pipe_create_and_start_startup_sessions (void); +invoke_wasm_early_startup_callback (void); +#endif static MonoComponentEventPipe fn_table = { { MONO_COMPONENT_ITF_VERSION, &event_pipe_available }, @@ -231,8 +233,10 @@ event_pipe_add_rundown_execution_checkpoint_2 ( const ep_char8_t *name, ep_timestamp_t timestamp) { +#ifdef HOST_WASM // If WASM installed any startup session provider configs, start those sessions and record the session IDs - event_pipe_create_and_start_startup_sessions(); + invoke_wasm_early_startup_callback (); +#endif return ep_add_rundown_execution_checkpoint (name, timestamp); } @@ -408,63 +412,19 @@ mono_wasm_event_pipe_session_disable (MonoWasmEventPipeSessionID session_id) return TRUE; } -static uint32_t startup_session_provider_configs_count; -static const char **startup_session_provider_configs; -static MonoWasmEventPipeSessionID *startup_session_ids; -static mono_wasm_event_pipe_startup_session_prestreaming_cb startup_session_prestreaming_callback; - -void -event_pipe_create_and_start_startup_sessions (void) -{ - /* FIXME: what if we don't do this in C? */ - - uint32_t count = startup_session_provider_configs_count; - const char **configs = startup_session_provider_configs; - if (!count) - return; - uint32_t *ids = g_new0 (MonoWasmEventPipeSessionID, count); - for (uint32_t i = 0; i < count; i++) { - EventPipeSessionID session; - EventPipeSerializationFormat format = EP_SERIALIZATION_FORMAT_NETTRACE_V4; - EventPipeSessionType session_type = EP_SESSION_TYPE_FILE; - char *output_path = NULL; /* FIXME for file sessions, set it to something */ - - session = ep_enable_2 (output_path, - circular_buffer_size_in_mb, - providers, - session_type, - format, - !!rundown_requested, - /* stream */NULL, - /* callback*/ NULL, - /* callback_data*/ NULL); - ids[i] = ep_to_wasm_session_id (session); - - /* FIXME: for streaming we need to send back a DS IPC reply before we start sending - * the data, so enabling here is too early. */ - g_assert (session_type != EP_SESSION_TYPE_IPCSTREAM); - if (startup_session_prestreaming_callback != NULL) - startup_session_prestreaming_callback (i, ids[i]); - ep_start_streaming (session); - } - startup_session_ids = ids; -} +static mono_wasm_event_pipe_early_startup_cb wasm_early_startup_callback; void -mono_wasm_event_pipe_session_set_startup_sessions (uint32_t count, const char **provider_configs, mono_wasm_event_pipe_startup_session_prestreaming_cb callback) +mono_wasm_event_pipe_set_early_startup_callback (mono_wasm_event_pipe_early_startup_cb callback) { - g_assert (count == 0 || provider_configs != NULL); - startup_session_provider_configs_counts = count; - startup_session_provider_configs = provider_configs; - startup_session_prestreaming_callback = callback; + wasm_early_startup_callback = callback; } void -mono_wasm_event_pipe_session_get_startup_session_ids (uint32_t count, MonoWasmEventPipeSessionID *session_id_dest) +invoke_wasm_early_startup_callback (void) { - g_assert (session_id_dest); - g_assert (count <= startup_session_provider_configs_count); - memcpy (session_id_dest, startup_session_ids, count * sizeof (MonoWasmEventPipeSessionID)); + if (wasm_early_startup_callback) + wasm_early_startup_callback (); } #endif /* HOST_WASM */ diff --git a/src/mono/sample/wasm/browser-eventpipe/Wasm.Browser.EventPipe.Sample.csproj b/src/mono/sample/wasm/browser-eventpipe/Wasm.Browser.EventPipe.Sample.csproj index 8bcabdb2ea181c..acbe9e3be6c45a 100644 --- a/src/mono/sample/wasm/browser-eventpipe/Wasm.Browser.EventPipe.Sample.csproj +++ b/src/mono/sample/wasm/browser-eventpipe/Wasm.Browser.EventPipe.Sample.csproj @@ -19,6 +19,10 @@ { "MONO_LOG_LEVEL": "debug", "MONO_LOG_MASK": "diagnostics" +}' /> + diff --git a/src/mono/sample/wasm/browser-eventpipe/main.js b/src/mono/sample/wasm/browser-eventpipe/main.js index 5a45de0248bd72..13adb1a9dd2349 100644 --- a/src/mono/sample/wasm/browser-eventpipe/main.js +++ b/src/mono/sample/wasm/browser-eventpipe/main.js @@ -56,19 +56,32 @@ async function doWork(startWork, stopWork, getIterationsDone) { function getOnClickHandler(startWork, stopWork, getIterationsDone) { return async function () { - const options = MONO.diagnostics.SessionOptionsBuilder - .Empty - .setRundownEnabled(false) - .addProvider({ name: 'WasmHello', level: MONO.diagnostics.EventLevel.Verbose, args: 'EventCounterIntervalSec=1' }) - .build(); - console.log('starting providers', options.providers); + // const options = MONO.diagnostics.SessionOptionsBuilder + // .Empty + // .setRundownEnabled(false) + // .addProvider({ name: 'WasmHello', level: MONO.diagnostics.EventLevel.Verbose, args: 'EventCounterIntervalSec=1' }) + // .build(); + // console.log('starting providers', options.providers); - const eventSession = MONO.diagnostics.createEventPipeSession(options); + let sessions = MONO.diagnostics.getStartupSessions(); - eventSession.start(); + if (typeof(sessions) !== "object" || sessions.length === "undefined" || sessions.length == 0) + console.error ("expected an array of sessions, got ", sessions); + if (sessions.length != 1) + console.error ("expected one startup session, got ", sessions); + let eventSession = sessions[0]; + + console.debug ("eventSession state is ", eventSession._state); // ooh protected member access + + // const eventSession = MONO.diagnostics.createEventPipeSession(options); + + // eventSession.start(); const ret = await doWork(startWork, stopWork, getIterationsDone); - eventSession.stop(); + + eventSession.stop(); + + const filename = "dotnet-wasm-" + makeTimestamp() + ".nettrace"; const blob = eventSession.getTraceBlob(); diff --git a/src/mono/wasm/runtime/diagnostics.ts b/src/mono/wasm/runtime/diagnostics.ts index 682e8071f84dc1..03f4d86e3e90da 100644 --- a/src/mono/wasm/runtime/diagnostics.ts +++ b/src/mono/wasm/runtime/diagnostics.ts @@ -34,6 +34,9 @@ class EventPipeFileSession implements EventPipeSession { private _tracePath: string; // VFS file path to the trace file get sessionID(): bigint { return BigInt(this._sessionID); } + get isIPCStreamingSession(): boolean { + return false; + } constructor(sessionID: EventPipeSessionIDImpl, tracePath: string) { this._state = State.Initialized; @@ -239,10 +242,27 @@ export function mono_wasm_event_pipe_early_startup_callback(): void { if (startup_session_configs === null || startup_session_configs.length == 0) { return; } - startup_sessions = startup_session_configs.map(config => createEventPipeSession(config)); + startup_sessions = startup_session_configs.map(config => createAndStartEventPipeSession(config)); startup_session_configs = null; } +function postIPCStreamingSessionStarted(sessionID: EventPipeSessionID): void { + // TODO: For IPC streaming sessions this is the place to send back an acknowledgement with the session ID +} + +function createAndStartEventPipeSession(options: (EventPipeSessionOptions & EventPipeSessionAutoStopOptions)): EventPipeSession | null { + const session = createEventPipeSession(options); + if (session === null) { + return null; + } + + if (session.isIPCStreamingSession) { + postIPCStreamingSessionStarted(session.sessionID); + } + session.start(); + return session; +} + function createEventPipeSession(options?: EventPipeSessionOptions): EventPipeSession | null { // The session trace is saved to a file in the VFS. The file name doesn't matter, // but we'd like it to be distinct from other traces. diff --git a/src/mono/wasm/runtime/dotnet.d.ts b/src/mono/wasm/runtime/dotnet.d.ts index 71728e392bfa77..3f7420697f6464 100644 --- a/src/mono/wasm/runtime/dotnet.d.ts +++ b/src/mono/wasm/runtime/dotnet.d.ts @@ -253,6 +253,7 @@ declare type DotnetModuleConfigImports = { declare type EventPipeSessionID = bigint; interface EventPipeSession { get sessionID(): EventPipeSessionID; + get isIPCStreamingSession(): boolean; start(): void; stop(): void; getTraceBlob(): Blob; @@ -292,6 +293,7 @@ interface Diagnostics { EventLevel: EventLevel; SessionOptionsBuilder: typeof SessionOptionsBuilder; createEventPipeSession(options?: EventPipeSessionOptions): EventPipeSession | null; + getStartupSessions(): (EventPipeSession | null)[]; } declare function mono_wasm_runtime_ready(): void; diff --git a/src/mono/wasm/runtime/types.ts b/src/mono/wasm/runtime/types.ts index 4960b3120c6f9c..64a1a0d8682415 100644 --- a/src/mono/wasm/runtime/types.ts +++ b/src/mono/wasm/runtime/types.ts @@ -314,6 +314,7 @@ export type EventPipeSessionID = bigint; export interface EventPipeSession { // session ID for debugging logging only get sessionID(): EventPipeSessionID; + get isIPCStreamingSession(): boolean; start(): void; stop(): void; getTraceBlob(): Blob; From 3d22fe1d7e2da45db99054fa8aa75bbc6e0b904d Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Mon, 6 Jun 2022 13:58:47 -0400 Subject: [PATCH 07/84] WIP: checkpoint add a controller and a webworker for DS --- .../runtime/diagnostic-server-controller.ts | 30 +++++++++++++++++++ src/mono/wasm/runtime/diagnostics.ts | 27 +++++++++++++---- src/mono/wasm/runtime/startup.ts | 10 ++++++- src/mono/wasm/runtime/types.ts | 6 +++- .../dotnet-diagnostic_server-worker.ts | 9 ++++++ 5 files changed, 75 insertions(+), 7 deletions(-) create mode 100644 src/mono/wasm/runtime/diagnostic-server-controller.ts create mode 100644 src/mono/wasm/runtime/workers/dotnet-diagnostic_server-worker.ts diff --git a/src/mono/wasm/runtime/diagnostic-server-controller.ts b/src/mono/wasm/runtime/diagnostic-server-controller.ts new file mode 100644 index 00000000000000..f289452368dce9 --- /dev/null +++ b/src/mono/wasm/runtime/diagnostic-server-controller.ts @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import type { DiagnosticOptions, EventPipeSessionID } from "./types"; + +interface ServerConfigureResult { + serverStarted: boolean; + serverReady?: Promise; +} + +async function configureServer(options: DiagnosticOptions): Promise { + if (options.waitForConnection) { + // TODO: start the server and wait for a connection + return { serverStarted: false, serverReady: Promise.resolve() }; + } else { + // TODO: maybe still start the server if there's an option specified + return { serverStarted: false }; + } +} + +function postIPCStreamingSessionStarted(sessionID: EventPipeSessionID): void { + // TODO: For IPC streaming sessions this is the place to send back an acknowledgement with the session ID +} + +const serverController = { + configureServer, + postIPCStreamingSessionStarted, +}; + +export default serverController; diff --git a/src/mono/wasm/runtime/diagnostics.ts b/src/mono/wasm/runtime/diagnostics.ts index 03f4d86e3e90da..84865400f196ee 100644 --- a/src/mono/wasm/runtime/diagnostics.ts +++ b/src/mono/wasm/runtime/diagnostics.ts @@ -3,8 +3,9 @@ import { Module } from "./imports"; import cwraps from "./cwraps"; -import type { EventPipeSessionID, DiagnosticOptions, EventPipeSession, EventPipeSessionOptions, EventPipeSessionAutoStopOptions } from "./types"; +import type { DiagnosticOptions, EventPipeSession, EventPipeSessionOptions, EventPipeSessionAutoStopOptions } from "./types"; import type { VoidPtr } from "./types/emscripten"; +import serverController from "./diagnostic-server-controller"; import * as memory from "./memory"; const sizeOfInt32 = 4; @@ -246,9 +247,7 @@ export function mono_wasm_event_pipe_early_startup_callback(): void { startup_session_configs = null; } -function postIPCStreamingSessionStarted(sessionID: EventPipeSessionID): void { - // TODO: For IPC streaming sessions this is the place to send back an acknowledgement with the session ID -} + function createAndStartEventPipeSession(options: (EventPipeSessionOptions & EventPipeSessionAutoStopOptions)): EventPipeSession | null { const session = createEventPipeSession(options); @@ -257,7 +256,7 @@ function createAndStartEventPipeSession(options: (EventPipeSessionOptions & Even } if (session.isIPCStreamingSession) { - postIPCStreamingSessionStarted(session.sessionID); + serverController.postIPCStreamingSessionStarted(session.sessionID); } session.start(); return session; @@ -299,6 +298,24 @@ export const diagnostics: Diagnostics = { }, }; +export async function conifigureDiagnostics(options: DiagnosticOptions): Promise { + if (!options.server) + return options; + mono_assert(options.server !== undefined && (options.server === true || options.server === "wait")); + // serverController.startServer + // if diagnostic_options.await is true, wait for the server to get a connection + const q = await serverController.configureServer(options); + const wait = options.server == "wait"; + if (q.serverStarted) { + if (wait && q.serverReady) { + // wait for the server to get a connection + await q.serverReady; + // TODO: get sessions from the server controller and add them to the list of startup sessions + } + } + return options; +} + export function mono_wasm_init_diagnostics(config?: DiagnosticOptions): void { const sessions = config?.sessions ?? []; startup_session_configs = sessions; diff --git a/src/mono/wasm/runtime/startup.ts b/src/mono/wasm/runtime/startup.ts index c2d91222d1d252..27db3db2c84d01 100644 --- a/src/mono/wasm/runtime/startup.ts +++ b/src/mono/wasm/runtime/startup.ts @@ -2,7 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. import MonoWasmThreads from "consts:monoWasmThreads"; -import { AllAssetEntryTypes, mono_assert, AssetEntry, CharPtrNull, DotnetModule, GlobalizationMode, MonoConfig, MonoConfigError, wasm_type_symbol, MonoObject } from "./types"; +import { AllAssetEntryTypes, mono_assert, AssetEntry, CharPtrNull, DotnetModule, GlobalizationMode, MonoConfig, MonoConfigError, wasm_type_symbol, MonoObject, is_nullish } from "./types"; import { ENVIRONMENT_IS_ESM, ENVIRONMENT_IS_NODE, ENVIRONMENT_IS_PTHREAD, ENVIRONMENT_IS_SHELL, INTERNAL, locateFile, Module, MONO, requirePromise, runtimeHelpers } from "./imports"; import cwraps from "./cwraps"; import { mono_wasm_raise_debug_event, mono_wasm_runtime_ready } from "./debug"; @@ -23,6 +23,7 @@ import { mono_wasm_new_root } from "./roots"; import { init_crypto } from "./crypto-worker"; import { init_polyfills } from "./polyfills"; import * as pthreads_worker from "./pthreads/worker"; +import { configure_diagnostics } from "./diagnostics"; export let runtime_is_initialized_resolve: () => void; export let runtime_is_initialized_reject: (reason?: any) => void; @@ -171,6 +172,13 @@ async function mono_wasm_pre_init(): Promise { Module.print("MONO_WASM: configSrc nor config was specified"); } + if (Module.config !== undefined && !Module.config.isError) { + const diagnostic_options = Module.config.diagnostic_options; + if (!is_nullish(diagnostic_options)) { + Module.config.diagnostic_options = await configure_diagnostics(diagnostic_options); + } + } + Module.removeRunDependency("mono_wasm_pre_init"); } diff --git a/src/mono/wasm/runtime/types.ts b/src/mono/wasm/runtime/types.ts index 64a1a0d8682415..ab962333b3fe79 100644 --- a/src/mono/wasm/runtime/types.ts +++ b/src/mono/wasm/runtime/types.ts @@ -183,7 +183,9 @@ export type CoverageProfilerOptions = { /// Options to configure EventPipe sessions that will be created and started at runtime startup export type DiagnosticOptions = { - sessions?: (EventPipeSessionOptions & EventPipeSessionAutoStopOptions)[] + sessions?: (EventPipeSessionOptions & EventPipeSessionAutoStopOptions)[], + /// If true, the diagnostic server will be started. If "wait", the runtime will wait at startup until a diagnsotic session connects to the server + server?: boolean | "wait", } /// For EventPipe sessions that will be created and started at runtime startup @@ -302,6 +304,8 @@ export function is_nullish(value: T | null | undefined): value is null | unde return (value === undefined) || (value === null); } +export type DiagnosticServerControlCommand = {}; + /// An identifier for an EventPipe session. The id is unique during the lifetime of the runtime. /// Primarily intended for debugging purposes. export type EventPipeSessionID = bigint; diff --git a/src/mono/wasm/runtime/workers/dotnet-diagnostic_server-worker.ts b/src/mono/wasm/runtime/workers/dotnet-diagnostic_server-worker.ts new file mode 100644 index 00000000000000..d451b6f3420483 --- /dev/null +++ b/src/mono/wasm/runtime/workers/dotnet-diagnostic_server-worker.ts @@ -0,0 +1,9 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import type { DiagnosticServerControlCommand } from "../types"; + +function messageReceived(event: MessageEvent): void { + console.debug("get in loser, we're going to vegas", event.data); +} +addEventListener("message", messageReceived); From ee53f056230cbab946012bda6e111caa34172a77 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Wed, 8 Jun 2022 15:38:23 -0400 Subject: [PATCH 08/84] WIP checkpoint EventPipeIPCSession class skeleton --- src/mono/wasm/runtime/diagnostics.ts | 82 +++++++++++++++++++++++----- src/mono/wasm/runtime/types.ts | 4 ++ 2 files changed, 72 insertions(+), 14 deletions(-) diff --git a/src/mono/wasm/runtime/diagnostics.ts b/src/mono/wasm/runtime/diagnostics.ts index 84865400f196ee..9f9ac552ccd581 100644 --- a/src/mono/wasm/runtime/diagnostics.ts +++ b/src/mono/wasm/runtime/diagnostics.ts @@ -3,7 +3,8 @@ import { Module } from "./imports"; import cwraps from "./cwraps"; -import type { DiagnosticOptions, EventPipeSession, EventPipeSessionOptions, EventPipeSessionAutoStopOptions } from "./types"; +import type { DiagnosticOptions, EventPipeSession, EventPipeStreamingSession, EventPipeSessionOptions, EventPipeSessionAutoStopOptions } from "./types"; +import { mono_assert } from "./types"; import type { VoidPtr } from "./types/emscripten"; import serverController from "./diagnostic-server-controller"; import * as memory from "./memory"; @@ -28,6 +29,38 @@ function stop_streaming(sessionID: EventPipeSessionIDImpl): void { cwraps.mono_wasm_event_pipe_session_disable(sessionID); } +class EventPipeIPCSession implements EventPipeStreamingSession { + private _sessionID: EventPipeSessionIDImpl; + private _messagePort: MessagePort; + + constructor(messagePort: MessagePort, sessionID: EventPipeSessionIDImpl) { + this._messagePort = messagePort; + this._sessionID = sessionID; + } + + get sessionID(): bigint { + return BigInt(this._sessionID); + } + get isIPCStreamingSession(): true { + return true; + } + start() { + throw new Error("implement me"); + } + stop() { + throw new Error("implement me"); + } + getTraceBlob(): Blob { + throw new Error("implement me"); + } + postIPCStreamingSessionStarted() { + this._messagePort.postMessage({ + "type": "started", + "sessionID": this._sessionID, + }); + } +} + /// An EventPipe session that saves the event data to a file in the VFS. class EventPipeFileSession implements EventPipeSession { protected _state: State; @@ -236,7 +269,11 @@ export interface Diagnostics { getStartupSessions(): (EventPipeSession | null)[]; } -let startup_session_configs: (EventPipeSessionOptions & EventPipeSessionAutoStopOptions)[] | null = null; +interface EventPipeSessionIPCOptions { + message_port: MessagePort; +} + +let startup_session_configs: (EventPipeSessionOptions & EventPipeSessionAutoStopOptions & EventPipeSessionIPCOptions)[] = []; let startup_sessions: (EventPipeSession | null)[] | null = null; export function mono_wasm_event_pipe_early_startup_callback(): void { @@ -244,25 +281,24 @@ export function mono_wasm_event_pipe_early_startup_callback(): void { return; } startup_sessions = startup_session_configs.map(config => createAndStartEventPipeSession(config)); - startup_session_configs = null; + startup_session_configs = []; } - -function createAndStartEventPipeSession(options: (EventPipeSessionOptions & EventPipeSessionAutoStopOptions)): EventPipeSession | null { +function createAndStartEventPipeSession(options: (EventPipeSessionOptions & EventPipeSessionAutoStopOptions & EventPipeSessionIPCOptions)): EventPipeSession | null { const session = createEventPipeSession(options); if (session === null) { return null; } - if (session.isIPCStreamingSession) { - serverController.postIPCStreamingSessionStarted(session.sessionID); + if (session instanceof EventPipeIPCSession) { + session.postIPCStreamingSessionStarted(); } session.start(); return session; } -function createEventPipeSession(options?: EventPipeSessionOptions): EventPipeSession | null { +function createEventPipeSession(options?: EventPipeSessionOptions & EventPipeSessionIPCOptions): EventPipeSession | null { // The session trace is saved to a file in the VFS. The file name doesn't matter, // but we'd like it to be distinct from other traces. const tracePath = `/trace-${totalSessions++}.nettrace`; @@ -273,8 +309,12 @@ function createEventPipeSession(options?: EventPipeSessionOptions): EventPipeSes return null; const sessionID = success; - const session = new EventPipeFileSession(sessionID, tracePath); - return session; + if (options?.message_port !== undefined) { + return new EventPipeIPCSession(sessionID, options.message_port); + } else { + const session = new EventPipeFileSession(sessionID, tracePath); + return session; + } } /// APIs for working with .NET diagnostics from JavaScript. @@ -298,10 +338,21 @@ export const diagnostics: Diagnostics = { }, }; -export async function conifigureDiagnostics(options: DiagnosticOptions): Promise { +// Initialization flow +/// * The runtime calls configure_diagnostics with options from MonoConfig +/// * We start the diagnostic server which connects to the host and waits for some configurations (an IPC CollectTracing command) +/// * The host sends us the configurations and we push them onto the startup_session_configs array and let the startup resume +/// * The runtime calls mono_wasm_initA_diagnostics with any options from MonoConfig +/// * The runtime C layer calls mono_wasm_event_pipe_early_startup_callback during startup once native EventPipe code is initialized +/// * We start all the sessiosn in startup_session_configs and allow them to start streaming +/// * The IPC sessions first send an IPC message with the session ID and then they start streaming +//// * If the diagnostic server gets more commands it will send us a message through the serverController and we will start additional sessions + + +export async function configure_diagnostics(options: DiagnosticOptions): Promise { if (!options.server) return options; - mono_assert(options.server !== undefined && (options.server === true || options.server === "wait")); + mono_assert(options.server !== undefined && (options.server === true || options.server === "wait"), "options.server must be a boolean or 'wait'"); // serverController.startServer // if diagnostic_options.await is true, wait for the server to get a connection const q = await serverController.configureServer(options); @@ -309,8 +360,11 @@ export async function conifigureDiagnostics(options: DiagnosticOptions): Promise if (q.serverStarted) { if (wait && q.serverReady) { // wait for the server to get a connection - await q.serverReady; + const serverReadyResponse = await q.serverReady; // TODO: get sessions from the server controller and add them to the list of startup sessions + if (serverReadyResponse.sessions) { + startup_sesison_configs.push(...serverReadyResponse.sessions); + } } } return options; @@ -318,7 +372,7 @@ export async function conifigureDiagnostics(options: DiagnosticOptions): Promise export function mono_wasm_init_diagnostics(config?: DiagnosticOptions): void { const sessions = config?.sessions ?? []; - startup_session_configs = sessions; + startup_session_configs.push(...sessions); } export default diagnostics; diff --git a/src/mono/wasm/runtime/types.ts b/src/mono/wasm/runtime/types.ts index ab962333b3fe79..451894de2aa812 100644 --- a/src/mono/wasm/runtime/types.ts +++ b/src/mono/wasm/runtime/types.ts @@ -323,3 +323,7 @@ export interface EventPipeSession { stop(): void; getTraceBlob(): Blob; } + +export interface EventPipeStreamingSession extends EventPipeSession { + get isIPCStreamingSession(): true; +} From 6ddb82907ab669251dc6c04311ffff001e1f38f1 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Wed, 8 Jun 2022 16:51:54 -0400 Subject: [PATCH 09/84] WIP checkpoint: runtime crashes; but WS from JS works --- .../Directory.Build.props | 1 + .../Wasm.Browser.EventPipe.Sample.csproj | 2 +- .../sample/wasm/browser-eventpipe/index.html | 4 ++- .../sample/wasm/browser-eventpipe/main.js | 36 ++++++++++++++----- .../runtime/diagnostic-server-controller.ts | 27 +++++++++----- src/mono/wasm/runtime/diagnostics.ts | 22 ++++++------ src/mono/wasm/runtime/dotnet.d.ts | 1 + src/mono/wasm/runtime/types.ts | 5 +++ 8 files changed, 67 insertions(+), 31 deletions(-) diff --git a/src/installer/pkg/sfx/Microsoft.NETCore.App/Directory.Build.props b/src/installer/pkg/sfx/Microsoft.NETCore.App/Directory.Build.props index f8194381ae1d8b..d0b682432cfc2c 100644 --- a/src/installer/pkg/sfx/Microsoft.NETCore.App/Directory.Build.props +++ b/src/installer/pkg/sfx/Microsoft.NETCore.App/Directory.Build.props @@ -214,6 +214,7 @@ + diff --git a/src/mono/sample/wasm/browser-eventpipe/Wasm.Browser.EventPipe.Sample.csproj b/src/mono/sample/wasm/browser-eventpipe/Wasm.Browser.EventPipe.Sample.csproj index acbe9e3be6c45a..91d1708c3362a9 100644 --- a/src/mono/sample/wasm/browser-eventpipe/Wasm.Browser.EventPipe.Sample.csproj +++ b/src/mono/sample/wasm/browser-eventpipe/Wasm.Browser.EventPipe.Sample.csproj @@ -20,7 +20,7 @@ "MONO_LOG_LEVEL": "debug", "MONO_LOG_MASK": "diagnostics" }' /> - diff --git a/src/mono/sample/wasm/browser-eventpipe/index.html b/src/mono/sample/wasm/browser-eventpipe/index.html index 9d97103c7de467..607e16650c3dc5 100644 --- a/src/mono/sample/wasm/browser-eventpipe/index.html +++ b/src/mono/sample/wasm/browser-eventpipe/index.html @@ -18,7 +18,9 @@
Computing Fib(N) repeatedly: - +
+ +
diff --git a/src/mono/sample/wasm/browser-eventpipe/main.js b/src/mono/sample/wasm/browser-eventpipe/main.js index 13adb1a9dd2349..7500c7ac57a2bd 100644 --- a/src/mono/sample/wasm/browser-eventpipe/main.js +++ b/src/mono/sample/wasm/browser-eventpipe/main.js @@ -63,15 +63,15 @@ function getOnClickHandler(startWork, stopWork, getIterationsDone) { // .build(); // console.log('starting providers', options.providers); - let sessions = MONO.diagnostics.getStartupSessions(); + let sessions = MONO.diagnostics.getStartupSessions(); - if (typeof(sessions) !== "object" || sessions.length === "undefined" || sessions.length == 0) - console.error ("expected an array of sessions, got ", sessions); - if (sessions.length != 1) - console.error ("expected one startup session, got ", sessions); - let eventSession = sessions[0]; + if (typeof (sessions) !== "object" || sessions.length === "undefined" || sessions.length == 0) + console.error("expected an array of sessions, got ", sessions); + if (sessions.length != 1) + console.error("expected one startup session, got ", sessions); + let eventSession = sessions[0]; - console.debug ("eventSession state is ", eventSession._state); // ooh protected member access + console.debug("eventSession state is ", eventSession._state); // ooh protected member access // const eventSession = MONO.diagnostics.createEventPipeSession(options); @@ -79,9 +79,9 @@ function getOnClickHandler(startWork, stopWork, getIterationsDone) { const ret = await doWork(startWork, stopWork, getIterationsDone); - eventSession.stop(); + eventSession.stop(); + - const filename = "dotnet-wasm-" + makeTimestamp() + ".nettrace"; const blob = eventSession.getTraceBlob(); @@ -90,7 +90,25 @@ function getOnClickHandler(startWork, stopWork, getIterationsDone) { } } +function websocketTestThing() { + console.log("websocketTestThing opening a connection"); + const ws = new WebSocket("ws://localhost:9090/diagnostics"); + ws.onopen = function () { + ws.send("hello from browser"); + ws.onmessage = function (event) { + console.log("got message from server: ", event.data); + ws.close(); + } + } + ws.onerror = function (event) { + console.log("error from server: ", event); + } +} + async function main() { + const wsbtn = document.getElementById("openWS"); + wsbtn.onclick = websocketTestThing; + const { MONO, BINDING, Module, RuntimeBuildInfo } = await createDotnetRuntime(() => { return { disableDotnet6Compatibility: true, diff --git a/src/mono/wasm/runtime/diagnostic-server-controller.ts b/src/mono/wasm/runtime/diagnostic-server-controller.ts index f289452368dce9..709324e56290b2 100644 --- a/src/mono/wasm/runtime/diagnostic-server-controller.ts +++ b/src/mono/wasm/runtime/diagnostic-server-controller.ts @@ -1,24 +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 { DiagnosticOptions, EventPipeSessionID } from "./types"; +import type { DiagnosticOptions, EventPipeSessionOptions, EventPipeSessionIPCOptions } from "./types"; + +interface ServerReadyResult { + sessions?: (EventPipeSessionOptions & EventPipeSessionIPCOptions)[]; // provider configs +} interface ServerConfigureResult { serverStarted: boolean; - serverReady?: Promise; + serverReady?: Promise; } async function configureServer(options: DiagnosticOptions): Promise { - if (options.waitForConnection) { + if (options.server !== undefined && options.server) { + // TODO start the server + let serverReady: Promise; + if (options.server == "wait") { + //TODO: make a promise to wait for the connection + serverReady = Promise.resolve({}); + } else { + // server is ready now, no need to wait + serverReady = Promise.resolve({}); + } // TODO: start the server and wait for a connection - return { serverStarted: false, serverReady: Promise.resolve() }; - } else { - // TODO: maybe still start the server if there's an option specified + return { serverStarted: false, serverReady: serverReady }; + } else return { serverStarted: false }; - } } -function postIPCStreamingSessionStarted(sessionID: EventPipeSessionID): void { +function postIPCStreamingSessionStarted(/*sessionID: EventPipeSessionID*/): void { // TODO: For IPC streaming sessions this is the place to send back an acknowledgement with the session ID } diff --git a/src/mono/wasm/runtime/diagnostics.ts b/src/mono/wasm/runtime/diagnostics.ts index 9f9ac552ccd581..09aa5d7b712803 100644 --- a/src/mono/wasm/runtime/diagnostics.ts +++ b/src/mono/wasm/runtime/diagnostics.ts @@ -3,7 +3,7 @@ import { Module } from "./imports"; import cwraps from "./cwraps"; -import type { DiagnosticOptions, EventPipeSession, EventPipeStreamingSession, EventPipeSessionOptions, EventPipeSessionAutoStopOptions } from "./types"; +import type { DiagnosticOptions, EventPipeSession, EventPipeStreamingSession, EventPipeSessionOptions, EventPipeSessionIPCOptions, EventPipeSessionAutoStopOptions } from "./types"; import { mono_assert } from "./types"; import type { VoidPtr } from "./types/emscripten"; import serverController from "./diagnostic-server-controller"; @@ -245,7 +245,7 @@ export class SessionOptionsBuilder { // a conter for the number of sessions created let totalSessions = 0; -function createSessionWithPtrCB(sessionIdOutPtr: VoidPtr, options: EventPipeSessionOptions | undefined, tracePath: string): false | number { +function createSessionWithPtrCB(sessionIdOutPtr: VoidPtr, options: EventPipeSessionOptions & Partial | undefined, tracePath: string): false | number { const defaultRundownRequested = true; const defaultProviders = ""; // empty string means use the default providers const defaultBufferSizeInMB = 1; @@ -253,6 +253,8 @@ function createSessionWithPtrCB(sessionIdOutPtr: VoidPtr, options: EventPipeSess const rundown = options?.collectRundownEvents ?? defaultRundownRequested; const providers = options?.providers ?? defaultProviders; + // TODO: if options.message_port, create a streaming session instead of a file session + memory.setI32(sessionIdOutPtr, 0); if (!cwraps.mono_wasm_event_pipe_enable(tracePath, defaultBufferSizeInMB, providers, rundown, sessionIdOutPtr)) { return false; @@ -269,11 +271,8 @@ export interface Diagnostics { getStartupSessions(): (EventPipeSession | null)[]; } -interface EventPipeSessionIPCOptions { - message_port: MessagePort; -} -let startup_session_configs: (EventPipeSessionOptions & EventPipeSessionAutoStopOptions & EventPipeSessionIPCOptions)[] = []; +let startup_session_configs: (EventPipeSessionOptions & EventPipeSessionAutoStopOptions & Partial)[] = []; let startup_sessions: (EventPipeSession | null)[] | null = null; export function mono_wasm_event_pipe_early_startup_callback(): void { @@ -285,7 +284,7 @@ export function mono_wasm_event_pipe_early_startup_callback(): void { } -function createAndStartEventPipeSession(options: (EventPipeSessionOptions & EventPipeSessionAutoStopOptions & EventPipeSessionIPCOptions)): EventPipeSession | null { +function createAndStartEventPipeSession(options: (EventPipeSessionOptions & EventPipeSessionAutoStopOptions & Partial)): EventPipeSession | null { const session = createEventPipeSession(options); if (session === null) { return null; @@ -298,7 +297,7 @@ function createAndStartEventPipeSession(options: (EventPipeSessionOptions & Even return session; } -function createEventPipeSession(options?: EventPipeSessionOptions & EventPipeSessionIPCOptions): EventPipeSession | null { +function createEventPipeSession(options?: EventPipeSessionOptions & Partial): EventPipeSession | null { // The session trace is saved to a file in the VFS. The file name doesn't matter, // but we'd like it to be distinct from other traces. const tracePath = `/trace-${totalSessions++}.nettrace`; @@ -310,7 +309,7 @@ function createEventPipeSession(options?: EventPipeSessionOptions & EventPipeSes const sessionID = success; if (options?.message_port !== undefined) { - return new EventPipeIPCSession(sessionID, options.message_port); + return new EventPipeIPCSession(options.message_port, sessionID); } else { const session = new EventPipeFileSession(sessionID, tracePath); return session; @@ -361,9 +360,8 @@ export async function configure_diagnostics(options: DiagnosticOptions): Promise if (wait && q.serverReady) { // wait for the server to get a connection const serverReadyResponse = await q.serverReady; - // TODO: get sessions from the server controller and add them to the list of startup sessions - if (serverReadyResponse.sessions) { - startup_sesison_configs.push(...serverReadyResponse.sessions); + if (serverReadyResponse?.sessions) { + startup_session_configs.push(...serverReadyResponse.sessions); } } } diff --git a/src/mono/wasm/runtime/dotnet.d.ts b/src/mono/wasm/runtime/dotnet.d.ts index 3f7420697f6464..c3d3bbc5a75168 100644 --- a/src/mono/wasm/runtime/dotnet.d.ts +++ b/src/mono/wasm/runtime/dotnet.d.ts @@ -211,6 +211,7 @@ declare type CoverageProfilerOptions = { }; declare type DiagnosticOptions = { sessions?: (EventPipeSessionOptions & EventPipeSessionAutoStopOptions)[]; + server?: boolean | "wait"; }; interface EventPipeSessionAutoStopOptions { stop_at?: string; diff --git a/src/mono/wasm/runtime/types.ts b/src/mono/wasm/runtime/types.ts index 451894de2aa812..dc7a5a2ef1ae01 100644 --- a/src/mono/wasm/runtime/types.ts +++ b/src/mono/wasm/runtime/types.ts @@ -196,6 +196,11 @@ export interface EventPipeSessionAutoStopOptions { /// Called after the session has been stopped. on_session_stopped?: (session: EventPipeSession) => void; } +/// For EventPipe sessions started by the diagnostic server, a message port to send back the session data +export interface EventPipeSessionIPCOptions { + message_port: MessagePort; +} + /// Options to configure the event pipe session /// The recommended method is to MONO.diagnostics.SesisonOptionsBuilder to create an instance of this type From 5c4aa81829d9eb1012e1c95e80c23a0ecbc43fe1 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Tue, 14 Jun 2022 12:49:01 -0400 Subject: [PATCH 10/84] WIP: diagnostic server --- .../diagnostic-server-controller-commands.ts | 18 ++++++ .../runtime/diagnostic-server-controller.ts | 29 +++++++--- src/mono/wasm/runtime/diagnostics.ts | 21 +++---- src/mono/wasm/runtime/types.ts | 13 +++-- .../dotnet-diagnostic_server-worker.ts | 57 ++++++++++++++++++- 5 files changed, 110 insertions(+), 28 deletions(-) create mode 100644 src/mono/wasm/runtime/diagnostic-server-controller-commands.ts diff --git a/src/mono/wasm/runtime/diagnostic-server-controller-commands.ts b/src/mono/wasm/runtime/diagnostic-server-controller-commands.ts new file mode 100644 index 00000000000000..b8a52aa4b19c93 --- /dev/null +++ b/src/mono/wasm/runtime/diagnostic-server-controller-commands.ts @@ -0,0 +1,18 @@ + +import type { EventPipeSessionDiagnosticServerID } from "./types"; + +export type { EventPipeSessionDiagnosticServerID } from "./types"; + + +export type EventPipeSessionIDImpl = number; + +export type DiagnosticServerControlCommand = DiagnosticServerControlCommandStart | DiagnosticServerControlCommandSetSessionID; +export type DiagnosticServerControlCommandStart = { + type: "start"; +}; + +export type DiagnosticServerControlCommandSetSessionID = { + type: "set_session_id"; + diagnostic_server_id: EventPipeSessionDiagnosticServerID; + session_id: EventPipeSessionIDImpl; +} diff --git a/src/mono/wasm/runtime/diagnostic-server-controller.ts b/src/mono/wasm/runtime/diagnostic-server-controller.ts index 709324e56290b2..d808b47cc46e3a 100644 --- a/src/mono/wasm/runtime/diagnostic-server-controller.ts +++ b/src/mono/wasm/runtime/diagnostic-server-controller.ts @@ -1,7 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -import type { DiagnosticOptions, EventPipeSessionOptions, EventPipeSessionIPCOptions } from "./types"; +import type { DiagnosticOptions, EventPipeSessionOptions, EventPipeSessionIPCOptions, EventPipeSessionDiagnosticServerID } from "./types"; +import type { EventPipeSessionIDImpl } from "./diagnostic-server-controller-commands"; + + interface ServerReadyResult { sessions?: (EventPipeSessionOptions & EventPipeSessionIPCOptions)[]; // provider configs @@ -29,13 +32,25 @@ async function configureServer(options: DiagnosticOptions): Promise; + postIPCStreamingSessionStarted(diagnosticSessionID: EventPipeSessionDiagnosticServerID, sessionID: EventPipeSessionIDImpl): void; +} + +let serverController: ServerController | null = null; + +export function getController(): ServerController { + if (serverController) + return serverController; + serverController = { + configureServer, + postIPCStreamingSessionStarted, + }; + return serverController; +} -export default serverController; diff --git a/src/mono/wasm/runtime/diagnostics.ts b/src/mono/wasm/runtime/diagnostics.ts index 09aa5d7b712803..ebcc6483a99528 100644 --- a/src/mono/wasm/runtime/diagnostics.ts +++ b/src/mono/wasm/runtime/diagnostics.ts @@ -3,10 +3,10 @@ import { Module } from "./imports"; import cwraps from "./cwraps"; -import type { DiagnosticOptions, EventPipeSession, EventPipeStreamingSession, EventPipeSessionOptions, EventPipeSessionIPCOptions, EventPipeSessionAutoStopOptions } from "./types"; +import type { DiagnosticOptions, EventPipeSession, EventPipeStreamingSession, EventPipeSessionOptions, EventPipeSessionIPCOptions, EventPipeSessionDiagnosticServerID, EventPipeSessionAutoStopOptions } from "./types"; import { mono_assert } from "./types"; import type { VoidPtr } from "./types/emscripten"; -import serverController from "./diagnostic-server-controller"; +import { getController } from "./diagnostic-server-controller"; import * as memory from "./memory"; const sizeOfInt32 = 4; @@ -31,11 +31,11 @@ function stop_streaming(sessionID: EventPipeSessionIDImpl): void { class EventPipeIPCSession implements EventPipeStreamingSession { private _sessionID: EventPipeSessionIDImpl; - private _messagePort: MessagePort; + private _diagnosticServerID: EventPipeSessionDiagnosticServerID; - constructor(messagePort: MessagePort, sessionID: EventPipeSessionIDImpl) { - this._messagePort = messagePort; + constructor(diagnosticServerID: EventPipeSessionDiagnosticServerID, sessionID: EventPipeSessionIDImpl) { this._sessionID = sessionID; + this._diagnosticServerID = diagnosticServerID; } get sessionID(): bigint { @@ -54,10 +54,7 @@ class EventPipeIPCSession implements EventPipeStreamingSession { throw new Error("implement me"); } postIPCStreamingSessionStarted() { - this._messagePort.postMessage({ - "type": "started", - "sessionID": this._sessionID, - }); + getController().postIPCStreamingSessionStarted(this._diagnosticServerID, this._sessionID); } } @@ -308,8 +305,8 @@ function createEventPipeSession(options?: EventPipeSessionOptions & Partial void; } -/// For EventPipe sessions started by the diagnostic server, a message port to send back the session data + +export type EventPipeSessionDiagnosticServerID = number; +/// For EventPipe sessions started by the diagnostic server, an id is assigned to each session before it is associated with an eventpipe session in the runtime. export interface EventPipeSessionIPCOptions { - message_port: MessagePort; + diagnostic_server_id: EventPipeSessionDiagnosticServerID; } - /// Options to configure the event pipe session /// The recommended method is to MONO.diagnostics.SesisonOptionsBuilder to create an instance of this type export interface EventPipeSessionOptions { @@ -309,12 +310,12 @@ export function is_nullish(value: T | null | undefined): value is null | unde return (value === undefined) || (value === null); } -export type DiagnosticServerControlCommand = {}; /// An identifier for an EventPipe session. The id is unique during the lifetime of the runtime. /// Primarily intended for debugging purposes. export type EventPipeSessionID = bigint; + /// An EventPipe session object represents a single diagnostic tracing session that is collecting /// events from the runtime and managed libraries. There may be multiple active sessions at the same time. /// Each session subscribes to a number of providers and will collect events from the time that start() is called, until stop() is called. @@ -323,12 +324,12 @@ export type EventPipeSessionID = bigint; export interface EventPipeSession { // session ID for debugging logging only get sessionID(): EventPipeSessionID; - get isIPCStreamingSession(): boolean; + readonly isIPCStreamingSession: boolean; start(): void; stop(): void; getTraceBlob(): Blob; } export interface EventPipeStreamingSession extends EventPipeSession { - get isIPCStreamingSession(): true; + readonly isIPCStreamingSession: true; } diff --git a/src/mono/wasm/runtime/workers/dotnet-diagnostic_server-worker.ts b/src/mono/wasm/runtime/workers/dotnet-diagnostic_server-worker.ts index d451b6f3420483..7f05b5c0e09e5b 100644 --- a/src/mono/wasm/runtime/workers/dotnet-diagnostic_server-worker.ts +++ b/src/mono/wasm/runtime/workers/dotnet-diagnostic_server-worker.ts @@ -1,9 +1,60 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -import type { DiagnosticServerControlCommand } from "../types"; +import type { EventPipeSessionIDImpl, EventPipeSessionDiagnosticServerID, DiagnosticServerControlCommand, DiagnosticServerControlCommandStart, DiagnosticServerControlCommandSetSessionID } from "../diagnostic-server-controller-commands"; -function messageReceived(event: MessageEvent): void { +/// Everything the diagnostic server knows about a connection. +/// The connection has a server ID and a websocket. If it's an eventpipe session, it will also have an eventpipe ID assigned when the runtime starts an EventPipe session. + +interface DiagnosticServerConnection { + readonly type: string; + get diagnosticSessionID(): EventPipeSessionDiagnosticServerID; + get socket(): WebSocket; +} + +interface DiagnosticServerEventPipeConnection extends DiagnosticServerConnection { + type: "eventpipe"; + get sessionID(): EventPipeSessionIDImpl; + set sessionID(sessionID: EventPipeSessionIDImpl); +} + +interface ServerSessionManager { + createSession(): DiagnosticServerConnection; + assignEventPipeSessionID(diagnosticServerID: EventPipeSessionDiagnosticServerID, sessionID: EventPipeSessionIDImpl): void; + getSession(sessionID: EventPipeSessionDiagnosticServerID): DiagnosticServerEventPipeConnection | null; +} + +function startServer(): void { + // TODO + console.debug("TODO: startServer"); +} + +function setSessionID(diagnosticServerID: EventPipeSessionDiagnosticServerID, sessionID: EventPipeSessionIDImpl): void { + // TODO + console.debug("TODO: setSessionID"); +} +function controlCommandReceived(event: MessageEvent): void { console.debug("get in loser, we're going to vegas", event.data); + const cmd = event.data; + if (cmd.type === undefined) { + console.error("control command has no type property"); + return; + } + switch (cmd.type) { + case "start": + startServer(); + break; + case "set_session_id": + setSessionID(cmd.diagnostic_server_id, cmd.session_id); + break; + default: + console.warn("Unknown control command: " + (cmd).type); + break; + } } -addEventListener("message", messageReceived); + +function workerMain() { + self.addEventListener("message", controlCommandReceived); +} + +workerMain(); From dfcc613efd8a76e1bcb3a2f29653b104b621e4d0 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Mon, 13 Jun 2022 09:52:59 -0400 Subject: [PATCH 11/84] XXX PrintfDebuggingHacks --- src/mono/mono/metadata/icall.c | 11 ++++++++++- src/mono/mono/metadata/object.c | 6 ++++++ .../Wasm.Browser.EventPipe.Sample.csproj | 4 ++-- src/mono/wasm/runtime/diagnostics.ts | 3 +++ src/mono/wasm/runtime/driver.c | 9 +++++++++ src/mono/wasm/runtime/startup.ts | 15 ++++++++++++++- 6 files changed, 44 insertions(+), 4 deletions(-) diff --git a/src/mono/mono/metadata/icall.c b/src/mono/mono/metadata/icall.c index bae99d743f7db4..1e52a93e4add28 100644 --- a/src/mono/mono/metadata/icall.c +++ b/src/mono/mono/metadata/icall.c @@ -6093,13 +6093,22 @@ ves_icall_System_Diagnostics_Debugger_IsAttached_internal (void) MonoBoolean ves_icall_System_Diagnostics_Debugger_IsLogging (void) { - return mono_get_runtime_callbacks ()->debug_log_is_enabled + printf ("in Debugger.IsLogging\n"); + gboolean res = mono_get_runtime_callbacks ()->debug_log_is_enabled && mono_get_runtime_callbacks ()->debug_log_is_enabled (); + printf ("in Debugger.IsLogging returning %s\n", res ? "True" : "False"); + return res; } void ves_icall_System_Diagnostics_Debugger_Log (int level, MonoString *volatile* category, MonoString *volatile* message) { + ERROR_DECL (error); + printf ("in Debugger.Log\n"); + gchar *msg = mono_string_to_utf8_checked_internal (*message, error); + mono_error_assert_ok (error); + printf ("in Debugger.Log with message '%s'\n", msg); + g_free (msg); if (mono_get_runtime_callbacks ()->debug_log) mono_get_runtime_callbacks ()->debug_log (level, *category, *message); } diff --git a/src/mono/mono/metadata/object.c b/src/mono/mono/metadata/object.c index df63130a75f500..d502537a15cd61 100644 --- a/src/mono/mono/metadata/object.c +++ b/src/mono/mono/metadata/object.c @@ -534,6 +534,12 @@ mono_runtime_class_init_full (MonoVTable *vtable, MonoError *error) MonoExceptionHandle exch = MONO_HANDLE_NEW (MonoException, exc); mono_threads_end_abort_protected_block (); + if (mono_trace_is_traced (G_LOG_LEVEL_DEBUG, MONO_TRACE_TYPE)) { + char* type_name = mono_type_full_name (m_class_get_byval_arg (klass)); + mono_trace (G_LOG_LEVEL_DEBUG, MONO_TRACE_TYPE, "Returned from running class .cctor for %s from '%s'", type_name, m_class_get_image (klass)->name); + g_free (type_name); + } + //exception extracted, error will be set to the right value later if (exc == NULL && !is_ok (error)) { // invoking failed but exc was not set exc = mono_error_convert_to_exception (error); diff --git a/src/mono/sample/wasm/browser-eventpipe/Wasm.Browser.EventPipe.Sample.csproj b/src/mono/sample/wasm/browser-eventpipe/Wasm.Browser.EventPipe.Sample.csproj index 91d1708c3362a9..4385ed0537eaeb 100644 --- a/src/mono/sample/wasm/browser-eventpipe/Wasm.Browser.EventPipe.Sample.csproj +++ b/src/mono/sample/wasm/browser-eventpipe/Wasm.Browser.EventPipe.Sample.csproj @@ -15,10 +15,10 @@ - cmd).type); From 377a50a7b6e8d86847739a1deaab17dbe6c5e2f4 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Wed, 15 Jun 2022 16:01:45 -0400 Subject: [PATCH 13/84] WIP some notes on diagnostics and JS workers --- src/mono/wasm/runtime/diagnostic-server.md | 60 ++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 src/mono/wasm/runtime/diagnostic-server.md diff --git a/src/mono/wasm/runtime/diagnostic-server.md b/src/mono/wasm/runtime/diagnostic-server.md new file mode 100644 index 00000000000000..e3e937af026a27 --- /dev/null +++ b/src/mono/wasm/runtime/diagnostic-server.md @@ -0,0 +1,60 @@ +# Diagnostic Server for .NET WebAssembly + +The diagnostic server [IPC protocol](https://github.com/dotnet/diagnostics/blob/main/documentation/design-docs/ipc-protocol.md) can support "connect" mode and "listen" mode. In listen mode the .NET runtime opens a socket and waits for connections. This doesn't make a ton of sense in a websocket scenario (except maybe if the debugger is driving things?) + +We will initially only support "connect" mode. In connect mode the runtime must do the following in a loop: + + 1. Open a socket to the server URL. + 2. Send an "advertise" request. + 3. Idle until the server responds with a command. + 4. Do two things: + - Open a new socket send an advertise and go back to step 3. + - Respond to the command on the existing socket and begin some kind of server action. + 5. If the remote end closes the socket, notify the runtime to stop the session. + 6. If the remote end closes the socket before the server sends a command, stop the server (?) + +We will need a dedicated thread to handle the WebSocket connections. This thread will need to be able to notify the runtime (or directly execute commands?) + +## Implementation constraints + +- The diagnostic worker needs to start before the runtime starts - we have to be able to accept connections in "wait to start" mode to do startup profiling. + +- The diagnostic worker needs to be able to send commands to the runtime. Or to directlly start sessions. +- The runtime needs to be able to send events from (some? all?) thread to the server. +- The diagnostic worker needs to be able to notify the runtime when the connection is closed. Or to directly stop sessions. + +## Make the diagnostic Worker a pthread + +ok, so if we make the diagnostic server a pthread, what would that look like: + +Early during runtime startup if the appropriate bits are set, we will call into the runtime to make us a diagnostic pthread (which will use `emscripten_exit_with_live_runtime` to immediately return to JS and do everything else in an event-based way). + +The problem is if the diagnostic URL has a "suspend" option, the Worker should wait in JS for a resume command and then post a message back to the main thread to resume. + +So we need to create a promise in the main thread that becomes resolved when we receive some kind of notification from the worker. We could use the `Atomics.waitAsync` to make a promise on the main thread that will resolve when the memory changes. + +## DS IPC stream + +the native code for an EP session uses an `IpcStream` object to do the actual reading and writing which has a vtable of callbacks to provide the implementation. +We can use the `ds-ipc-pal-socket.c` implementation as a guide - essentially there's an `IpcStream` subclass that +has a custom vtable that has pointers to the functions to call. + +Once we're sending the actual payloads, we can wrap up the bytes in a JS buffer and pass it over to websocket. + +For this to work well we probably need the diagnostic Worker to be a pthread? + +It would be nice if we didn't have to do our own message queue. + +## Make our own MessagePort + +the emscripten onmessage handler in the worker errors on unknown messages. the emscripten onmessage handler in the main thread ignores unknown messages. + +So when mono starts a thread it can send over a port to the main thread. + +then the main thread can talk to the worker thread. + +There's a complication here that we need to be careful because emscripten reuses workers for pthreads. but if we only ever talk over the dedicated channel, it's probably good. + +Emscripten `pthread_t` is a pointer. hope...fully... they're not reused. + +Basically make `mono_thread_create_internal` run some JS that From 4a17d5dff0caccf739d2ee51f58462139cbc60d5 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Fri, 17 Jun 2022 11:12:31 -0400 Subject: [PATCH 14/84] fix eslint --- src/mono/wasm/runtime/dotnet.d.ts | 2 +- .../runtime/workers/dotnet-diagnostic_server-worker.ts | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/mono/wasm/runtime/dotnet.d.ts b/src/mono/wasm/runtime/dotnet.d.ts index c3d3bbc5a75168..6b63dc8be81725 100644 --- a/src/mono/wasm/runtime/dotnet.d.ts +++ b/src/mono/wasm/runtime/dotnet.d.ts @@ -254,7 +254,7 @@ declare type DotnetModuleConfigImports = { declare type EventPipeSessionID = bigint; interface EventPipeSession { get sessionID(): EventPipeSessionID; - get isIPCStreamingSession(): boolean; + readonly isIPCStreamingSession: boolean; start(): void; stop(): void; getTraceBlob(): Blob; diff --git a/src/mono/wasm/runtime/workers/dotnet-diagnostic_server-worker.ts b/src/mono/wasm/runtime/workers/dotnet-diagnostic_server-worker.ts index da684e7096ee18..c6a4e989a34279 100644 --- a/src/mono/wasm/runtime/workers/dotnet-diagnostic_server-worker.ts +++ b/src/mono/wasm/runtime/workers/dotnet-diagnostic_server-worker.ts @@ -1,7 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -import type { EventPipeSessionIDImpl, EventPipeSessionDiagnosticServerID, DiagnosticServerControlCommand, DiagnosticServerControlCommandStart, DiagnosticServerControlCommandSetSessionID } from "../diagnostic-server-controller-commands"; +import type { + EventPipeSessionIDImpl, EventPipeSessionDiagnosticServerID, DiagnosticServerControlCommand, + /*DiagnosticServerControlCommandStart, DiagnosticServerControlCommandSetSessionID*/ +} from "../diagnostic-server-controller-commands"; /// Everything the diagnostic server knows about a connection. /// The connection has a server ID and a websocket. If it's an eventpipe session, it will also have an eventpipe ID assigned when the runtime starts an EventPipe session. @@ -96,7 +99,7 @@ class EventPipeServerConnection implements DiagnosticServerEventPipeConnection { } } - private _onMessage(event: MessageEvent) { + private _onMessage(/*event: MessageEvent*/) { switch (this._state) { case ListenerState.AwaitingCommand: /* TODO process command */ From fd8df9875feef12c0eb34d5062f25a87a497a5ae Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Fri, 17 Jun 2022 11:13:30 -0400 Subject: [PATCH 15/84] debug printfs etc more printfs --- src/mono/mono/component/event_pipe-stub.c | 5 +++++ src/mono/mono/component/event_pipe.c | 3 +++ .../browser-eventpipe/Wasm.Browser.EventPipe.Sample.csproj | 4 ++-- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/mono/mono/component/event_pipe-stub.c b/src/mono/mono/component/event_pipe-stub.c index 0d2371b0119823..e74e93eb3902cd 100644 --- a/src/mono/mono/component/event_pipe-stub.c +++ b/src/mono/mono/component/event_pipe-stub.c @@ -555,6 +555,11 @@ mono_wasm_event_pipe_session_disable (MonoWasmEventPipeSessionID session_id) void mono_wasm_event_pipe_set_early_startup_callback (mono_wasm_event_pipe_early_startup_cb callback) { +#ifdef HOST_BROWSER + EM_ASM({ + console.log ('in stub early callback\n'); + }); +#endif } #endif /* HOST_WASM */ diff --git a/src/mono/mono/component/event_pipe.c b/src/mono/mono/component/event_pipe.c index 01f0560b4bf503..a61e0fe875af76 100644 --- a/src/mono/mono/component/event_pipe.c +++ b/src/mono/mono/component/event_pipe.c @@ -423,6 +423,9 @@ mono_wasm_event_pipe_set_early_startup_callback (mono_wasm_event_pipe_early_star void invoke_wasm_early_startup_callback (void) { + EM_ASM({ + console.log ('in real invoke early callback\n'); + }); if (wasm_early_startup_callback) wasm_early_startup_callback (); } diff --git a/src/mono/sample/wasm/browser-eventpipe/Wasm.Browser.EventPipe.Sample.csproj b/src/mono/sample/wasm/browser-eventpipe/Wasm.Browser.EventPipe.Sample.csproj index 4385ed0537eaeb..5ec84a7d455195 100644 --- a/src/mono/sample/wasm/browser-eventpipe/Wasm.Browser.EventPipe.Sample.csproj +++ b/src/mono/sample/wasm/browser-eventpipe/Wasm.Browser.EventPipe.Sample.csproj @@ -17,10 +17,10 @@ - From f235f559d803210102b4098c0902e496100a2a27 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Tue, 21 Jun 2022 16:48:57 -0400 Subject: [PATCH 16/84] WIP: start moving the diagnostic server to a JS pthread --- .../wasm/runtime/diagnostic_server/README.md | 10 ++++ .../browser/controller.ts} | 4 +- .../diagnostic-server.md | 0 .../server_pthread/controller-commands.ts} | 4 +- .../server_pthread/event_pipe.ts} | 10 +--- .../diagnostic_server/server_pthread/index.ts | 53 +++++++++++++++++++ .../server_pthread/tsconfig.json | 9 ++++ src/mono/wasm/runtime/diagnostics.ts | 2 +- .../wasm/runtime/pthreads/worker/index.ts | 24 +++++++++ src/mono/wasm/runtime/workers/README.md | 11 ++++ 10 files changed, 114 insertions(+), 13 deletions(-) create mode 100644 src/mono/wasm/runtime/diagnostic_server/README.md rename src/mono/wasm/runtime/{diagnostic-server-controller.ts => diagnostic_server/browser/controller.ts} (95%) rename src/mono/wasm/runtime/{ => diagnostic_server}/diagnostic-server.md (100%) rename src/mono/wasm/runtime/{diagnostic-server-controller-commands.ts => diagnostic_server/server_pthread/controller-commands.ts} (77%) rename src/mono/wasm/runtime/{workers/dotnet-diagnostic_server-worker.ts => diagnostic_server/server_pthread/event_pipe.ts} (96%) create mode 100644 src/mono/wasm/runtime/diagnostic_server/server_pthread/index.ts create mode 100644 src/mono/wasm/runtime/diagnostic_server/server_pthread/tsconfig.json diff --git a/src/mono/wasm/runtime/diagnostic_server/README.md b/src/mono/wasm/runtime/diagnostic_server/README.md new file mode 100644 index 00000000000000..fe73aa17104d80 --- /dev/null +++ b/src/mono/wasm/runtime/diagnostic_server/README.md @@ -0,0 +1,10 @@ +# Diagnostic Server and EventPipe + +What's in here: + +- `browser/` APIs for the main thread. The main thread has 2 responsibilities: + - control the overall diagnostic server `browser/controller.ts` + - establish communication channels between EventPipe session streaming threads and the diagnostic server pthread +- `server_pthread/` A long-running worker that owns the WebSocket connections out of the browser and that receives the session payloads from the streaming threads. +- `pthread/` (**TODO* decide if this is necessary) APIs for normal pthreads that need to do things to diagnostics +- `shared/` type definitions to be shared between the worker and browser main thread diff --git a/src/mono/wasm/runtime/diagnostic-server-controller.ts b/src/mono/wasm/runtime/diagnostic_server/browser/controller.ts similarity index 95% rename from src/mono/wasm/runtime/diagnostic-server-controller.ts rename to src/mono/wasm/runtime/diagnostic_server/browser/controller.ts index d808b47cc46e3a..2d27d8605a8bb7 100644 --- a/src/mono/wasm/runtime/diagnostic-server-controller.ts +++ b/src/mono/wasm/runtime/diagnostic_server/browser/controller.ts @@ -1,8 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -import type { DiagnosticOptions, EventPipeSessionOptions, EventPipeSessionIPCOptions, EventPipeSessionDiagnosticServerID } from "./types"; -import type { EventPipeSessionIDImpl } from "./diagnostic-server-controller-commands"; +import type { DiagnosticOptions, EventPipeSessionOptions, EventPipeSessionIPCOptions, EventPipeSessionDiagnosticServerID } from "../../types"; +import type { EventPipeSessionIDImpl } from "../server_pthread/controller-commands"; diff --git a/src/mono/wasm/runtime/diagnostic-server.md b/src/mono/wasm/runtime/diagnostic_server/diagnostic-server.md similarity index 100% rename from src/mono/wasm/runtime/diagnostic-server.md rename to src/mono/wasm/runtime/diagnostic_server/diagnostic-server.md diff --git a/src/mono/wasm/runtime/diagnostic-server-controller-commands.ts b/src/mono/wasm/runtime/diagnostic_server/server_pthread/controller-commands.ts similarity index 77% rename from src/mono/wasm/runtime/diagnostic-server-controller-commands.ts rename to src/mono/wasm/runtime/diagnostic_server/server_pthread/controller-commands.ts index 472103e0a3e0b0..8cf48b6a2bb99b 100644 --- a/src/mono/wasm/runtime/diagnostic-server-controller-commands.ts +++ b/src/mono/wasm/runtime/diagnostic_server/server_pthread/controller-commands.ts @@ -1,7 +1,7 @@ -import type { EventPipeSessionDiagnosticServerID } from "./types"; +import type { EventPipeSessionDiagnosticServerID } from "../../types"; -export type { EventPipeSessionDiagnosticServerID } from "./types"; +export type { EventPipeSessionDiagnosticServerID } from "../../types"; export type EventPipeSessionIDImpl = number; diff --git a/src/mono/wasm/runtime/workers/dotnet-diagnostic_server-worker.ts b/src/mono/wasm/runtime/diagnostic_server/server_pthread/event_pipe.ts similarity index 96% rename from src/mono/wasm/runtime/workers/dotnet-diagnostic_server-worker.ts rename to src/mono/wasm/runtime/diagnostic_server/server_pthread/event_pipe.ts index c6a4e989a34279..37836cf1c3344e 100644 --- a/src/mono/wasm/runtime/workers/dotnet-diagnostic_server-worker.ts +++ b/src/mono/wasm/runtime/diagnostic_server/server_pthread/event_pipe.ts @@ -4,7 +4,7 @@ import type { EventPipeSessionIDImpl, EventPipeSessionDiagnosticServerID, DiagnosticServerControlCommand, /*DiagnosticServerControlCommandStart, DiagnosticServerControlCommandSetSessionID*/ -} from "../diagnostic-server-controller-commands"; +} from "./controller-commands"; /// Everything the diagnostic server knows about a connection. /// The connection has a server ID and a websocket. If it's an eventpipe session, it will also have an eventpipe ID assigned when the runtime starts an EventPipe session. @@ -190,7 +190,7 @@ function startServer(url: string): SessionManager { let sessionManager: SessionManager | null = null; -function controlCommandReceived(event: MessageEvent): void { +export function controlCommandReceived(event: MessageEvent): void { console.debug("get in loser, we're going to vegas", event.data); const cmd = event.data; if (cmd.type === undefined) { @@ -214,9 +214,3 @@ function controlCommandReceived(event: MessageEvent + +import { pthread_self } from "../../pthreads/worker"; +import { controlCommandReceived } from "./event_pipe"; + +interface DiagnosticMessage { + type: "diagnostic_server"; + cmd: string; +} + +function isDiagnosticMessage(x: any): x is DiagnosticMessage { + return typeof (x) === "object" && x.type === "diagnostic_server" && typeof (x.cmd) === "string"; +} + +class DiagnosticServer { + readonly websocketUrl: string; + readonly ws: WebSocket; + constructor(websocketUrl: string) { + this.websocketUrl = websocketUrl; + this.ws = new WebSocket(this.websocketUrl); + } + + start(): void { + console.log("starting diagnostic server"); + + if (pthread_self) { + pthread_self.addEventListener(this.onMessage.bind(this)); + pthread_self.postMessage({ + "type": "diagnostic_server", + "cmd": "started", + "thread_id": pthread_self.pthread_id + }); + } + } + + onMessage(event: MessageEvent): void { + const d = event.data; + if (d && isDiagnosticMessage(d)) { + controlCommandReceived(d); + } + } +} + + +/// Called by the runtime to initialize the diagnostic server workers +export function mono_wasm_diagnostic_server_start(websocketUrl: string): void { + console.debug("mono_wasm_diagnostic_server_start"); + const server = new DiagnosticServer(websocketUrl); + server.start(); +} diff --git a/src/mono/wasm/runtime/diagnostic_server/server_pthread/tsconfig.json b/src/mono/wasm/runtime/diagnostic_server/server_pthread/tsconfig.json new file mode 100644 index 00000000000000..62dfb20e168049 --- /dev/null +++ b/src/mono/wasm/runtime/diagnostic_server/server_pthread/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "lib": [ + "esnext", + "webworker" + ], + ] +} +} diff --git a/src/mono/wasm/runtime/diagnostics.ts b/src/mono/wasm/runtime/diagnostics.ts index 22f977befe72af..89832b9741f169 100644 --- a/src/mono/wasm/runtime/diagnostics.ts +++ b/src/mono/wasm/runtime/diagnostics.ts @@ -6,7 +6,7 @@ import cwraps from "./cwraps"; import type { DiagnosticOptions, EventPipeSession, EventPipeStreamingSession, EventPipeSessionOptions, EventPipeSessionIPCOptions, EventPipeSessionDiagnosticServerID, EventPipeSessionAutoStopOptions } from "./types"; import { mono_assert } from "./types"; import type { VoidPtr } from "./types/emscripten"; -import { getController } from "./diagnostic-server-controller"; +import { getController } from "./diagnostic_server/browser/controller"; import * as memory from "./memory"; const sizeOfInt32 = 4; diff --git a/src/mono/wasm/runtime/pthreads/worker/index.ts b/src/mono/wasm/runtime/pthreads/worker/index.ts index 13bd4afcac18a8..685d9e96555b22 100644 --- a/src/mono/wasm/runtime/pthreads/worker/index.ts +++ b/src/mono/wasm/runtime/pthreads/worker/index.ts @@ -23,6 +23,29 @@ export { WorkerThreadEventTarget, } from "./events"; +export interface PThreadSelf { + readonly pthread_id: pthread_ptr; + postMessage: (message: T, transfer?: Transferable[]) => void; + addEventListener: (listener: (event: MessageEvent) => void) => void; +} + +class WorkerSelf implements PThreadSelf { + readonly port: MessagePort; + readonly pthread_id: pthread_ptr; + constructor(pthread_id: pthread_ptr, port: MessagePort) { + this.port = port; + this.pthread_id = pthread_id; + } + postMessage(message: T, transfer?: Transferable[]) { + this.port.postMessage(message, transfer); + } + addEventListener(listener: (event: MessageEvent) => void) { + this.port.addEventListener("message", listener); + } +} + +export let pthread_self: PThreadSelf | null = null; + /// This is the "public internal" API for runtime subsystems that wish to be notified about /// pthreads that are running on the current worker. /// Example: @@ -46,6 +69,7 @@ function setupChannelToMainThread(pthread_ptr: pthread_ptr): MessagePort { workerPort.addEventListener("message", monoDedicatedChannelMessageFromMainToWorker); workerPort.start(); portToMain = workerPort; + pthread_self = new WorkerSelf(pthread_ptr, workerPort); self.postMessage(makeChannelCreatedMonoMessage(pthread_ptr, mainPort), [mainPort]); return workerPort; } diff --git a/src/mono/wasm/runtime/workers/README.md b/src/mono/wasm/runtime/workers/README.md index 762a1d301d0ad6..5b120f1832760a 100644 --- a/src/mono/wasm/runtime/workers/README.md +++ b/src/mono/wasm/runtime/workers/README.md @@ -11,6 +11,17 @@ file (or `.js`) and a `xyz/` subdirectory with additional sources. To add a new web worker, add a definition here and modify the `rollup.config.js` to add a new configuration. +## Caveats: a note about pthreads + +The workers in this directory are completely standalone from the Emscripten pthreads! they do not have access to the shared instance memory, and do not load the Emscripten `dotnet.js`. As a result, the workers in this directory cannot use any of pthread APIs or otherwise interact with the runtime in any way, except through message passing, or by having something in the runtime set up their own shared array buffer (which would be inaccessible from wasm). + +On the other hand, the workers in this directory also do not depend on a .NET runtime compiled with `-s USE_PTHREADS` and are thus usable on sufficiently new browser using the single-threaded builds of .NET for WebAssembly. + +For workers that need to interact with native code, follow the model of `../pthreads/` or `../diagnostic_server/`: + +- create `xyz/shared/`, `xyz/browser/` and `xyz/worker/` directories that have `index.ts` and `tsconfig.json` files that are set up for common ES, ES with DOM APIs and ES with WebWorker APIs, respectively +- call the apropriate functions (browser or worker) from the C code or from JS. + ## Typescript modules Typescript workers can use the modules from [`..`](..) but bear in From 431e2c6ab8f9fdf8deded8d5b3def6f823a31259 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Wed, 22 Jun 2022 15:52:12 -0400 Subject: [PATCH 17/84] WIP: move things around --- .../diagnostic_server/browser/controller.ts | 5 +-- .../server_pthread/controller-commands.ts | 19 --------- .../server_pthread/event_pipe.ts | 22 +++++----- .../diagnostic_server/server_pthread/index.ts | 13 ++---- .../server_pthread/tsconfig.json | 8 +--- .../shared/controller-commands.ts | 21 ++++++++++ .../diagnostic_server/shared/tsconfig.json | 3 ++ .../runtime/diagnostic_server/shared/types.ts | 20 +++++++++ src/mono/wasm/runtime/diagnostics.ts | 19 +++++---- .../wasm/runtime/pthreads/shared/index.ts | 42 +++++++++++++++++++ .../wasm/runtime/pthreads/worker/index.ts | 19 +++++---- src/mono/wasm/runtime/types.ts | 6 +-- src/mono/wasm/runtime/workers/tsconfig.json | 12 +----- 13 files changed, 128 insertions(+), 81 deletions(-) delete mode 100644 src/mono/wasm/runtime/diagnostic_server/server_pthread/controller-commands.ts create mode 100644 src/mono/wasm/runtime/diagnostic_server/shared/controller-commands.ts create mode 100644 src/mono/wasm/runtime/diagnostic_server/shared/tsconfig.json create mode 100644 src/mono/wasm/runtime/diagnostic_server/shared/types.ts diff --git a/src/mono/wasm/runtime/diagnostic_server/browser/controller.ts b/src/mono/wasm/runtime/diagnostic_server/browser/controller.ts index 2d27d8605a8bb7..b7d35c2c00d76e 100644 --- a/src/mono/wasm/runtime/diagnostic_server/browser/controller.ts +++ b/src/mono/wasm/runtime/diagnostic_server/browser/controller.ts @@ -2,8 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. import type { DiagnosticOptions, EventPipeSessionOptions, EventPipeSessionIPCOptions, EventPipeSessionDiagnosticServerID } from "../../types"; -import type { EventPipeSessionIDImpl } from "../server_pthread/controller-commands"; - +import type { EventPipeSessionIDImpl } from "../shared/types"; interface ServerReadyResult { @@ -32,7 +31,7 @@ async function configureServer(options: DiagnosticOptions): Promise): void { - console.debug("get in loser, we're going to vegas", event.data); - const cmd = event.data; - if (cmd.type === undefined) { - console.error("control command has no type property"); - return; - } - - switch (cmd.type) { +export function controlCommandReceived(msg: DiagnosticMessage): void { + const cmd = msg as DiagnosticServerControlCommand; + switch (cmd.cmd) { case "start": if (sessionManager !== null) throw new Error("server already started"); @@ -210,7 +208,7 @@ export function controlCommandReceived(event: MessageEventcmd).type); + console.warn("Unknown control command: ", cmd); break; } } diff --git a/src/mono/wasm/runtime/diagnostic_server/server_pthread/index.ts b/src/mono/wasm/runtime/diagnostic_server/server_pthread/index.ts index 9ac74eb110bac4..e4978daa7941cd 100644 --- a/src/mono/wasm/runtime/diagnostic_server/server_pthread/index.ts +++ b/src/mono/wasm/runtime/diagnostic_server/server_pthread/index.ts @@ -5,15 +5,8 @@ import { pthread_self } from "../../pthreads/worker"; import { controlCommandReceived } from "./event_pipe"; +import { isDiagnosticMessage } from "../shared/types"; -interface DiagnosticMessage { - type: "diagnostic_server"; - cmd: string; -} - -function isDiagnosticMessage(x: any): x is DiagnosticMessage { - return typeof (x) === "object" && x.type === "diagnostic_server" && typeof (x.cmd) === "string"; -} class DiagnosticServer { readonly websocketUrl: string; @@ -27,8 +20,8 @@ class DiagnosticServer { console.log("starting diagnostic server"); if (pthread_self) { - pthread_self.addEventListener(this.onMessage.bind(this)); - pthread_self.postMessage({ + pthread_self.addEventListenerFromBrowser(this.onMessage.bind(this)); + pthread_self.postMessageToBrowser({ "type": "diagnostic_server", "cmd": "started", "thread_id": pthread_self.pthread_id diff --git a/src/mono/wasm/runtime/diagnostic_server/server_pthread/tsconfig.json b/src/mono/wasm/runtime/diagnostic_server/server_pthread/tsconfig.json index 62dfb20e168049..ea592d1a092a80 100644 --- a/src/mono/wasm/runtime/diagnostic_server/server_pthread/tsconfig.json +++ b/src/mono/wasm/runtime/diagnostic_server/server_pthread/tsconfig.json @@ -1,9 +1,3 @@ { - "compilerOptions": { - "lib": [ - "esnext", - "webworker" - ], - ] -} + "extends": "../../tsconfig.worker.json" } diff --git a/src/mono/wasm/runtime/diagnostic_server/shared/controller-commands.ts b/src/mono/wasm/runtime/diagnostic_server/shared/controller-commands.ts new file mode 100644 index 00000000000000..293b29571519b9 --- /dev/null +++ b/src/mono/wasm/runtime/diagnostic_server/shared/controller-commands.ts @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import { EventPipeSessionDiagnosticServerID, EventPipeSessionIDImpl, DiagnosticMessage } from "./types"; + + +export type DiagnosticServerControlCommand = + DiagnosticServerControlCommandStart + | DiagnosticServerControlCommandSetSessionID + ; + +export interface DiagnosticServerControlCommandStart extends DiagnosticMessage { + cmd: "start", + url: string, // websocket url to connect to +} + +export interface DiagnosticServerControlCommandSetSessionID extends DiagnosticMessage { + cmd: "set_session_id"; + diagnostic_server_id: EventPipeSessionDiagnosticServerID; + session_id: EventPipeSessionIDImpl; +} diff --git a/src/mono/wasm/runtime/diagnostic_server/shared/tsconfig.json b/src/mono/wasm/runtime/diagnostic_server/shared/tsconfig.json new file mode 100644 index 00000000000000..7b8ecd91fcc4f1 --- /dev/null +++ b/src/mono/wasm/runtime/diagnostic_server/shared/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../tsconfig.shared.json" +} diff --git a/src/mono/wasm/runtime/diagnostic_server/shared/types.ts b/src/mono/wasm/runtime/diagnostic_server/shared/types.ts new file mode 100644 index 00000000000000..ceffa5329ca3e9 --- /dev/null +++ b/src/mono/wasm/runtime/diagnostic_server/shared/types.ts @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import type { MonoThreadMessage } from "../../pthreads/shared"; +import { isMonoThreadMessage } from "../../pthreads/shared"; + +export type { EventPipeSessionDiagnosticServerID } from "../../types"; + +export type EventPipeSessionIDImpl = number; + +export interface DiagnosticMessage extends MonoThreadMessage { + type: "diagnostic_server"; + cmd: string; +} + +export function isDiagnosticMessage(x: unknown): x is DiagnosticMessage { + return isMonoThreadMessage(x) && x.type === "diagnostic_server"; +} + + diff --git a/src/mono/wasm/runtime/diagnostics.ts b/src/mono/wasm/runtime/diagnostics.ts index 89832b9741f169..482bb855259456 100644 --- a/src/mono/wasm/runtime/diagnostics.ts +++ b/src/mono/wasm/runtime/diagnostics.ts @@ -29,11 +29,18 @@ function stop_streaming(sessionID: EventPipeSessionIDImpl): void { cwraps.mono_wasm_event_pipe_session_disable(sessionID); } -class EventPipeIPCSession implements EventPipeStreamingSession { +abstract class EventPipeSessionBase { + isIPCStreamingSession(): this is EventPipeStreamingSession { + return this instanceof EventPipeIPCSession; + } +} + +class EventPipeIPCSession extends EventPipeSessionBase implements EventPipeStreamingSession { private _sessionID: EventPipeSessionIDImpl; private _diagnosticServerID: EventPipeSessionDiagnosticServerID; constructor(diagnosticServerID: EventPipeSessionDiagnosticServerID, sessionID: EventPipeSessionIDImpl) { + super(); this._sessionID = sessionID; this._diagnosticServerID = diagnosticServerID; } @@ -41,9 +48,7 @@ class EventPipeIPCSession implements EventPipeStreamingSession { get sessionID(): bigint { return BigInt(this._sessionID); } - get isIPCStreamingSession(): true { - return true; - } + start() { throw new Error("implement me"); } @@ -59,17 +64,15 @@ class EventPipeIPCSession implements EventPipeStreamingSession { } /// An EventPipe session that saves the event data to a file in the VFS. -class EventPipeFileSession implements EventPipeSession { +class EventPipeFileSession extends EventPipeSessionBase implements EventPipeSession { protected _state: State; private _sessionID: EventPipeSessionIDImpl; private _tracePath: string; // VFS file path to the trace file get sessionID(): bigint { return BigInt(this._sessionID); } - get isIPCStreamingSession(): boolean { - return false; - } constructor(sessionID: EventPipeSessionIDImpl, tracePath: string) { + super(); this._state = State.Initialized; this._sessionID = sessionID; this._tracePath = tracePath; diff --git a/src/mono/wasm/runtime/pthreads/shared/index.ts b/src/mono/wasm/runtime/pthreads/shared/index.ts index d14013e04bf67b..1b53b1799a0bd8 100644 --- a/src/mono/wasm/runtime/pthreads/shared/index.ts +++ b/src/mono/wasm/runtime/pthreads/shared/index.ts @@ -1,9 +1,51 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +import { Module } from "../../imports"; + /// pthread_t in C export type pthread_ptr = number; +export interface PThreadInfo { + readonly pthread_id: pthread_ptr; + readonly is_main_thread: boolean; +} + +export const MainThread: PThreadInfo = { + get pthread_id(): pthread_ptr { + return getBrowserThreadID(); + }, + is_main_thread: true +}; + +let browser_thread_id_lazy: pthread_ptr | undefined; +export function getBrowserThreadID(): pthread_ptr { + if (browser_thread_id_lazy === undefined) { + browser_thread_id_lazy = (Module)["_emscripten_main_browser_thread_id"]() as pthread_ptr; + } + return browser_thread_id_lazy; +} + +/// Messages sent on the dedicated mono channel between a pthread and the browser thread + +// We use a namespacing scheme to avoid collisions: type/command should be unique. +export interface MonoThreadMessage { + // Type of message. Generally a subsystem like "diagnostic_server", or "event_pipe", "debugger", etc. + type: string; + // A particular kind of message. For example, "started", "stopped", "stopped_with_error", etc. + cmd: string; +} + +export function isMonoThreadMessage(x: unknown): x is MonoThreadMessage { + if (typeof (x) !== "object" || x === null) { + return false; + } + const xmsg = x as MonoThreadMessage; + return typeof (xmsg.type) === "string" && typeof (xmsg.cmd) === "string"; +} + +/// Messages sent using the worker object's postMessage() method /// + /// a symbol that we use as a key on messages on the global worker-to-main channel to identify our own messages /// we can't use an actual JS Symbol because those don't transfer between workers. export const monoSymbol = "__mono_message_please_dont_collide__"; //Symbol("mono"); diff --git a/src/mono/wasm/runtime/pthreads/worker/index.ts b/src/mono/wasm/runtime/pthreads/worker/index.ts index 685d9e96555b22..5172dc59a7cdd8 100644 --- a/src/mono/wasm/runtime/pthreads/worker/index.ts +++ b/src/mono/wasm/runtime/pthreads/worker/index.ts @@ -7,6 +7,7 @@ import MonoWasmThreads from "consts:monoWasmThreads"; import { Module, ENVIRONMENT_IS_PTHREAD } from "../../imports"; import { makeChannelCreatedMonoMessage, pthread_ptr } from "../shared"; import { mono_assert, is_nullish } from "../../types"; +import type { MonoThreadMessage, PThreadInfo } from "../shared"; import { makeWorkerThreadEvent, dotnetPthreadCreated, @@ -23,23 +24,27 @@ export { WorkerThreadEventTarget, } from "./events"; -export interface PThreadSelf { - readonly pthread_id: pthread_ptr; - postMessage: (message: T, transfer?: Transferable[]) => void; - addEventListener: (listener: (event: MessageEvent) => void) => void; +export interface PThreadSelf extends PThreadInfo { + postMessageToBrowser: (message: T, transfer?: Transferable[]) => void; + addEventListenerFromBrowser: (listener: (event: MessageEvent) => void) => void; } class WorkerSelf implements PThreadSelf { readonly port: MessagePort; readonly pthread_id: pthread_ptr; + readonly is_main_thread = false; constructor(pthread_id: pthread_ptr, port: MessagePort) { this.port = port; this.pthread_id = pthread_id; } - postMessage(message: T, transfer?: Transferable[]) { - this.port.postMessage(message, transfer); + postMessageToBrowser(message: MonoThreadMessage, transfer?: Transferable[]) { + if (transfer) { + this.port.postMessage(message, transfer); + } else { + this.port.postMessage(message); + } } - addEventListener(listener: (event: MessageEvent) => void) { + addEventListenerFromBrowser(listener: (event: MessageEvent) => void) { this.port.addEventListener("message", listener); } } diff --git a/src/mono/wasm/runtime/types.ts b/src/mono/wasm/runtime/types.ts index f9fd94339957a2..162bd3976a8b96 100644 --- a/src/mono/wasm/runtime/types.ts +++ b/src/mono/wasm/runtime/types.ts @@ -324,12 +324,10 @@ export type EventPipeSessionID = bigint; export interface EventPipeSession { // session ID for debugging logging only get sessionID(): EventPipeSessionID; - readonly isIPCStreamingSession: boolean; + isIPCStreamingSession(): this is EventPipeStreamingSession; start(): void; stop(): void; getTraceBlob(): Blob; } -export interface EventPipeStreamingSession extends EventPipeSession { - readonly isIPCStreamingSession: true; -} +export type EventPipeStreamingSession = EventPipeSession diff --git a/src/mono/wasm/runtime/workers/tsconfig.json b/src/mono/wasm/runtime/workers/tsconfig.json index 4df100fcb1d86f..5b141d29cf09c8 100644 --- a/src/mono/wasm/runtime/workers/tsconfig.json +++ b/src/mono/wasm/runtime/workers/tsconfig.json @@ -1,13 +1,3 @@ { - "extends": "../tsconfig", - "compilerOptions": { - "lib": [ - "esnext", - "webworker" - ], - }, - "exclude": [ - "../dotnet.d.ts", - "../bin" - ] + "extends": "../tsconfig.worker.json" } From 563b31a035c0c7511dd3084bcc69462a76fc136c Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Thu, 23 Jun 2022 14:03:02 -0400 Subject: [PATCH 18/84] cleanup --- .../pkg/sfx/Microsoft.NETCore.App/Directory.Build.props | 1 - src/mono/mono/component/event_pipe-stub.c | 9 +-------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/src/installer/pkg/sfx/Microsoft.NETCore.App/Directory.Build.props b/src/installer/pkg/sfx/Microsoft.NETCore.App/Directory.Build.props index d0b682432cfc2c..f8194381ae1d8b 100644 --- a/src/installer/pkg/sfx/Microsoft.NETCore.App/Directory.Build.props +++ b/src/installer/pkg/sfx/Microsoft.NETCore.App/Directory.Build.props @@ -214,7 +214,6 @@ - diff --git a/src/mono/mono/component/event_pipe-stub.c b/src/mono/mono/component/event_pipe-stub.c index e74e93eb3902cd..08b718011d286e 100644 --- a/src/mono/mono/component/event_pipe-stub.c +++ b/src/mono/mono/component/event_pipe-stub.c @@ -6,9 +6,6 @@ #include "mono/component/event_pipe.h" #include "mono/component/event_pipe-wasm.h" #include "mono/metadata/components.h" -#ifdef HOST_WASM -#include -#endif static EventPipeSessionID _dummy_session_id; @@ -555,11 +552,7 @@ mono_wasm_event_pipe_session_disable (MonoWasmEventPipeSessionID session_id) void mono_wasm_event_pipe_set_early_startup_callback (mono_wasm_event_pipe_early_startup_cb callback) { -#ifdef HOST_BROWSER - EM_ASM({ - console.log ('in stub early callback\n'); - }); -#endif + g_assert_not_reached (); } #endif /* HOST_WASM */ From 5e999a3387aeb2bd729f93b3d66dac8afacc7d88 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Wed, 22 Jun 2022 16:26:46 -0400 Subject: [PATCH 19/84] notes --- .../diagnostic_server/diagnostic-server.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/mono/wasm/runtime/diagnostic_server/diagnostic-server.md b/src/mono/wasm/runtime/diagnostic_server/diagnostic-server.md index e3e937af026a27..081967a362df07 100644 --- a/src/mono/wasm/runtime/diagnostic_server/diagnostic-server.md +++ b/src/mono/wasm/runtime/diagnostic_server/diagnostic-server.md @@ -58,3 +58,22 @@ There's a complication here that we need to be careful because emscripten reuses Emscripten `pthread_t` is a pointer. hope...fully... they're not reused. Basically make `mono_thread_create_internal` run some JS that + +## TODO + +- [browser] in dotnet preInit read the config options. extract websocket URL and whether to suspend. +- [browser] call down to C to start diagnostics +- [browser] create a pthread and have it call the JS diagnostic server start and then set it to not die after thread main returns. return pthread id to JS. +- [server_worker] start listening on the URL. +- [browser] if suspending, listen for a continue event from the JS and await for that to resolve. +- [server_worker] when there's a session start event, if we were suspended, add a pending session, and send a continue event. +- [server_worker] wait for a "runtime started" event? +- [server_worker] when there's a new session start event, call down to C to start and EP session. **FIXME** need a port at this point - +- [browser] in early startup callback, start any pending EP sessions (and create ports for them to the diagnostic server) +- [session_streamer] call out to JS to post messages to the diagnostic server. +- [browser] in C, fire events, which will wake up the session streamer +- [session_streamer] post more messages + +So the tricky bit is that for startup sessions and for "course of running" sessions, we need the browser thread to originate the message port transfer. (hopefully queuing work on the main thread is good enough?) + +Also the streamer thread probably needs to do a bunch of preliminary work in asynchronous JS before it can begin serving sessions. From 2906e1e886499aeb08683e4dc29970b3a4e5c772 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Thu, 23 Jun 2022 15:39:39 -0400 Subject: [PATCH 20/84] [diagnostic_server] wasm-specific fn_table We won't be using the native C version --- src/mono/mono/component/diagnostics_server.c | 70 ++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/src/mono/mono/component/diagnostics_server.c b/src/mono/mono/component/diagnostics_server.c index a4a8b5ba08baa9..80703bbfb92ff3 100644 --- a/src/mono/mono/component/diagnostics_server.c +++ b/src/mono/mono/component/diagnostics_server.c @@ -7,10 +7,14 @@ #include #include #include +#ifdef HOST_BROWSER +#include +#endif static bool diagnostics_server_available (void); +#ifndef HOST_BROWSER static MonoComponentDiagnosticsServer fn_table = { { MONO_COMPONENT_ITF_VERSION, &diagnostics_server_available }, &ds_server_init, @@ -18,15 +22,81 @@ static MonoComponentDiagnosticsServer fn_table = { &ds_server_pause_for_diagnostics_monitor, &ds_server_disable }; +#else +static bool +ds_server_wasm_init (void); + +static bool +ds_server_wasm_shutdown (void); + +static void +ds_server_wasm_pause_for_diagnostics_monitor (void); + +static void +ds_server_wasm_disable (void); + +static MonoComponentDiagnosticsServer fn_table = { + { MONO_COMPONENT_ITF_VERSION, &diagnostics_server_available }, + &ds_server_wasm_init, + &ds_server_wasm_shutdown, + &ds_server_wasm_pause_for_diagnostics_monitor, + &ds_server_wasm_disable, +}; +#endif static bool diagnostics_server_available (void) { + EM_ASM({ + console.log ("diagnostic server available"); + }); return true; } MonoComponentDiagnosticsServer * mono_component_diagnostics_server_init (void) { + EM_ASM({ + console.log ("diagnostic server component init"); + }); return &fn_table; } + +#ifdef HOST_BROWSER + +static bool +ds_server_wasm_init (void) +{ + EM_ASM({ + console.log ("ds_server_wasm_init"); + }); + return true; +} + + +static bool +ds_server_wasm_shutdown (void) +{ + EM_ASM({ + console.log ("ds_server_wasm_shutdown"); + }); + return true; +} + +static void +ds_server_wasm_pause_for_diagnostics_monitor (void) +{ + EM_ASM({ + console.log ("ds_server_wasm_pause_for_diagnostics_monitor"); + }); +} + +static void +ds_server_wasm_disable (void) +{ + EM_ASM({ + console.log ("ds_server_wasm_disable"); + }); +} + +#endif; From 6c249f0ef5a02a988cfbd301c4cde569d22a6c3e Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Fri, 24 Jun 2022 10:01:35 -0400 Subject: [PATCH 21/84] [wasm-ep] disable DS connect ports in C, too we will implement DS in JS --- src/mono/mono.proj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/mono/mono.proj b/src/mono/mono.proj index 3fdd8c41eb9a65..cf693d22fe546f 100644 --- a/src/mono/mono.proj +++ b/src/mono/mono.proj @@ -483,6 +483,7 @@ <_MonoCMakeArgs Include="-DFEATURE_PERFTRACING_DISABLE_PERFTRACING_LISTEN_PORTS=1"/> <_MonoCMakeArgs Include="-DFEATURE_PERFTRACING_DISABLE_DEFAULT_LISTEN_PORT=1"/> + <_MonoCMakeArgs Include="-DFEATURE_PERFTRACING_DISABLE_CONNECT_PORTS=1" /> From 567484c01543ba37d3a260e525a90224e762f0a3 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Fri, 24 Jun 2022 10:03:14 -0400 Subject: [PATCH 22/84] asyncify finalize_startup; make 1 diagnostics init function Combine mono_wasm_init_diagnostics and configure_diagnostics. Now that finalize_startup is async, we can pause to wait for the diagnostic server to startup at this point --- src/mono/wasm/runtime/diagnostics.ts | 37 +++++++++++++--------------- src/mono/wasm/runtime/startup.ts | 27 ++++++++------------ 2 files changed, 27 insertions(+), 37 deletions(-) diff --git a/src/mono/wasm/runtime/diagnostics.ts b/src/mono/wasm/runtime/diagnostics.ts index 482bb855259456..b5c3d42789a13c 100644 --- a/src/mono/wasm/runtime/diagnostics.ts +++ b/src/mono/wasm/runtime/diagnostics.ts @@ -4,7 +4,7 @@ import { Module } from "./imports"; import cwraps from "./cwraps"; import type { DiagnosticOptions, EventPipeSession, EventPipeStreamingSession, EventPipeSessionOptions, EventPipeSessionIPCOptions, EventPipeSessionDiagnosticServerID, EventPipeSessionAutoStopOptions } from "./types"; -import { mono_assert } from "./types"; +import { mono_assert, is_nullish } from "./types"; import type { VoidPtr } from "./types/emscripten"; import { getController } from "./diagnostic_server/browser/controller"; import * as memory from "./memory"; @@ -351,28 +351,25 @@ export const diagnostics: Diagnostics = { //// * If the diagnostic server gets more commands it will send us a message through the serverController and we will start additional sessions -export async function configure_diagnostics(options: DiagnosticOptions): Promise { - if (!options.server) - return options; - mono_assert(options.server !== undefined && (options.server === true || options.server === "wait"), "options.server must be a boolean or 'wait'"); - // serverController.startServer - // if diagnostic_options.await is true, wait for the server to get a connection - const q = await getController().configureServer(options); - const wait = options.server == "wait"; - if (q.serverStarted) { - if (wait && q.serverReady) { - // wait for the server to get a connection - const serverReadyResponse = await q.serverReady; - if (serverReadyResponse?.sessions) { - startup_session_configs.push(...serverReadyResponse.sessions); +export async function mono_wasm_init_diagnostics(options: DiagnosticOptions): Promise { + if (!is_nullish(options.server)) { + mono_assert(options.server !== undefined && (options.server === true || options.server === "wait"), "options.server must be a boolean or 'wait'"); + // serverController.startServer + // if diagnostic_options.await is true, wait for the server to get a connection + console.debug("in configure_diagnostics", options); + const q = await getController().configureServer(options); + const wait = options.server == "wait"; + if (q.serverStarted) { + if (wait && q.serverReady) { + // wait for the server to get a connection + const serverReadyResponse = await q.serverReady; + if (serverReadyResponse?.sessions) { + startup_session_configs.push(...serverReadyResponse.sessions); + } } } } - return options; -} - -export function mono_wasm_init_diagnostics(config?: DiagnosticOptions): void { - const sessions = config?.sessions ?? []; + const sessions = options?.sessions ?? []; startup_session_configs.push(...sessions); } diff --git a/src/mono/wasm/runtime/startup.ts b/src/mono/wasm/runtime/startup.ts index fb4f4f27e28c5e..04406ac75eda91 100644 --- a/src/mono/wasm/runtime/startup.ts +++ b/src/mono/wasm/runtime/startup.ts @@ -2,7 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. import MonoWasmThreads from "consts:monoWasmThreads"; -import { AllAssetEntryTypes, mono_assert, AssetEntry, CharPtrNull, DotnetModule, GlobalizationMode, MonoConfig, MonoConfigError, wasm_type_symbol, MonoObject, is_nullish } from "./types"; +import { AllAssetEntryTypes, mono_assert, AssetEntry, CharPtrNull, DotnetModule, GlobalizationMode, MonoConfig, MonoConfigError, wasm_type_symbol, MonoObject } from "./types"; import { ENVIRONMENT_IS_ESM, ENVIRONMENT_IS_NODE, ENVIRONMENT_IS_PTHREAD, ENVIRONMENT_IS_SHELL, INTERNAL, locateFile, Module, MONO, requirePromise, runtimeHelpers } from "./imports"; import cwraps from "./cwraps"; import { mono_wasm_raise_debug_event, mono_wasm_runtime_ready } from "./debug"; @@ -10,7 +10,7 @@ import GuardedPromise from "./guarded-promise"; import { mono_wasm_globalization_init, mono_wasm_load_icu_data } from "./icu"; import { toBase64StringImpl } from "./base64"; import { mono_wasm_init_aot_profiler, mono_wasm_init_coverage_profiler } from "./profiler"; -import { mono_wasm_init_diagnostics } from "./diagnostics"; +import { mono_wasm_init_diagnostics, } from "./diagnostics"; import { mono_wasm_load_bytes_into_heap } from "./buffers"; import { bind_runtime_method, get_method, _create_primitive_converters } from "./method-binding"; import { find_corlib_class } from "./class-loader"; @@ -23,7 +23,6 @@ import { mono_wasm_new_root } from "./roots"; import { init_crypto } from "./crypto-worker"; import { init_polyfills } from "./polyfills"; import * as pthreads_worker from "./pthreads/worker"; -import { configure_diagnostics } from "./diagnostics"; export let runtime_is_initialized_resolve: () => void; export let runtime_is_initialized_reject: (reason?: any) => void; @@ -179,22 +178,15 @@ async function mono_wasm_pre_init(): Promise { Module.print("MONO_WASM: configSrc nor config was specified"); } - if (Module.config !== undefined && !Module.config.isError) { - const diagnostic_options = Module.config.diagnostic_options; - if (!is_nullish(diagnostic_options)) { - Module.config.diagnostic_options = await configure_diagnostics(diagnostic_options); - } - } - Module.removeRunDependency("mono_wasm_pre_init"); } -function mono_wasm_after_runtime_initialized(): void { +async function mono_wasm_after_runtime_initialized(): Promise { if (!Module.config || Module.config.isError) { return; } finalize_assets(Module.config); - finalize_startup(Module.config); + await finalize_startup(Module.config); if (!ctx || !ctx.loaded_files || ctx.loaded_files.length == 0) { Module.print("MONO_WASM: no files were loaded into runtime"); } @@ -309,7 +301,7 @@ function _handle_fetched_asset(asset: AssetEntry, url?: string) { } } -function _apply_configuration_from_args(config: MonoConfig) { +async function _apply_configuration_from_args(config: MonoConfig): Promise { const envars = (config.environment_variables || {}); if (typeof (envars) !== "object") throw new Error("Expected config.environment_variables to be unset or a dictionary-style object"); @@ -331,11 +323,12 @@ function _apply_configuration_from_args(config: MonoConfig) { if (config.coverage_profiler_options) mono_wasm_init_coverage_profiler(config.coverage_profiler_options); - if (config.diagnostic_options) - mono_wasm_init_diagnostics(config.diagnostic_options); + if (config.diagnostic_options) { + await mono_wasm_init_diagnostics(config.diagnostic_options); + } } -function finalize_startup(config: MonoConfig | MonoConfigError | undefined): void { +async function finalize_startup(config: MonoConfig | MonoConfigError | undefined): Promise { const globalThisAny = globalThis as any; try { @@ -367,7 +360,7 @@ function finalize_startup(config: MonoConfig | MonoConfigError | undefined): voi try { console.debug("MONO_WASM: Initializing mono runtime"); - _apply_configuration_from_args(config); + await _apply_configuration_from_args(config); console.debug("MONO_WASM: applied config from args"); mono_wasm_globalization_init(config.globalization_mode!, config.diagnostic_tracing!); console.debug("MONO_WASM: globalization init"); From 199cc801b87f6811cd42c15010510c7620bec8ee Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Fri, 24 Jun 2022 10:04:43 -0400 Subject: [PATCH 23/84] (not implemented) set browser-eventpipe sample to start a DS server --- .../wasm/browser-eventpipe/Wasm.Browser.EventPipe.Sample.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/mono/sample/wasm/browser-eventpipe/Wasm.Browser.EventPipe.Sample.csproj b/src/mono/sample/wasm/browser-eventpipe/Wasm.Browser.EventPipe.Sample.csproj index 5ec84a7d455195..91bf7e8dc924e7 100644 --- a/src/mono/sample/wasm/browser-eventpipe/Wasm.Browser.EventPipe.Sample.csproj +++ b/src/mono/sample/wasm/browser-eventpipe/Wasm.Browser.EventPipe.Sample.csproj @@ -22,6 +22,7 @@ }' /> From c3e7fe1886278d280b6d37806d15143c60774f6d Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Fri, 24 Jun 2022 10:16:18 -0400 Subject: [PATCH 24/84] ping in the DS server (not functional yet) once we're able to start the DS server, make it log a ping to the console --- .../diagnostic_server/server_pthread/index.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/mono/wasm/runtime/diagnostic_server/server_pthread/index.ts b/src/mono/wasm/runtime/diagnostic_server/server_pthread/index.ts index e4978daa7941cd..abb481d05968c5 100644 --- a/src/mono/wasm/runtime/diagnostic_server/server_pthread/index.ts +++ b/src/mono/wasm/runtime/diagnostic_server/server_pthread/index.ts @@ -26,10 +26,20 @@ class DiagnosticServer { "cmd": "started", "thread_id": pthread_self.pthread_id }); + this.installTimeoutHandler(); } } - onMessage(event: MessageEvent): void { + private installTimeoutHandler(): void { + setTimeout(this.timeoutHandler.bind(this), 500); + } + + private timeoutHandler(this: DiagnosticServer): void { + console.debug("ping from diagnostic server"); + this.installTimeoutHandler(); + } + + onMessage(this: DiagnosticServer, event: MessageEvent): void { const d = event.data; if (d && isDiagnosticMessage(d)) { controlCommandReceived(d); From e64e539871906cc3496c2f8fe0e07371e3bf2795 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Fri, 24 Jun 2022 16:24:33 -0400 Subject: [PATCH 25/84] Start diagnostic server pthread Clean up some of the old WIP code - we will probably not send configuration strings from the diagnostic server back to the main thread. --- src/mono/mono/component/diagnostics_server.c | 42 ++++++- src/mono/mono/component/event_pipe-wasm.h | 4 + src/mono/wasm/runtime/cjs/dotnet.cjs.lib.js | 4 +- src/mono/wasm/runtime/cwraps.ts | 2 + .../diagnostic_server/browser/controller.ts | 110 ++++++++++++------ .../diagnostic_server/server_pthread/index.ts | 6 +- src/mono/wasm/runtime/diagnostics.ts | 55 +++++---- src/mono/wasm/runtime/es6/dotnet.es6.lib.js | 4 +- src/mono/wasm/runtime/exports.ts | 5 + src/mono/wasm/runtime/types.ts | 8 +- 10 files changed, 172 insertions(+), 68 deletions(-) diff --git a/src/mono/mono/component/diagnostics_server.c b/src/mono/mono/component/diagnostics_server.c index 80703bbfb92ff3..fa47935b1d4c0e 100644 --- a/src/mono/mono/component/diagnostics_server.c +++ b/src/mono/mono/component/diagnostics_server.c @@ -7,14 +7,15 @@ #include #include #include -#ifdef HOST_BROWSER +#ifdef HOST_WASM +#include #include #endif static bool diagnostics_server_available (void); -#ifndef HOST_BROWSER +#ifndef HOST_WASM static MonoComponentDiagnosticsServer fn_table = { { MONO_COMPONENT_ITF_VERSION, &diagnostics_server_available }, &ds_server_init, @@ -62,7 +63,7 @@ mono_component_diagnostics_server_init (void) return &fn_table; } -#ifdef HOST_BROWSER +#ifdef HOST_WASM static bool ds_server_wasm_init (void) @@ -99,4 +100,39 @@ ds_server_wasm_disable (void) }); } +/* Allocated by mono_wasm_diagnostic_server_create_thread, + * then ownership passed to server_thread. + */ +static char* +ds_websocket_url; + +extern void mono_wasm_diagnostic_server_on_server_thread_created (char *websocket_url); + +static void* +server_thread (void* unused_arg G_GNUC_UNUSED) +{ + char* ws_url = g_strdup (ds_websocket_url); + g_free (ds_websocket_url); + ds_websocket_url = NULL; + mono_wasm_diagnostic_server_on_server_thread_created (ws_url); + // "exit" from server_thread, but keep the pthread alive and responding to events + emscripten_exit_with_live_runtime (); +} + +gboolean +mono_wasm_diagnostic_server_create_thread (const char *websocket_url, pthread_t *out_thread_id) +{ + pthread_t thread; + + g_assert (!ds_websocket_url); + ds_websocket_url = g_strdup (websocket_url); + if (!pthread_create (&thread, NULL, server_thread, NULL)) { + *out_thread_id = thread; + return TRUE; + } + memset(out_thread_id, 0, sizeof(pthread_t)); + return FALSE; +} + + #endif; diff --git a/src/mono/mono/component/event_pipe-wasm.h b/src/mono/mono/component/event_pipe-wasm.h index bc9ac163cf3a92..e4f3841b726453 100644 --- a/src/mono/mono/component/event_pipe-wasm.h +++ b/src/mono/mono/component/event_pipe-wasm.h @@ -6,6 +6,7 @@ #define _MONO_COMPONENT_EVENT_PIPE_WASM_H #include +#include #include #include #include @@ -45,6 +46,9 @@ mono_wasm_event_pipe_session_start_streaming (MonoWasmEventPipeSessionID session EMSCRIPTEN_KEEPALIVE gboolean mono_wasm_event_pipe_session_disable (MonoWasmEventPipeSessionID session_id); +EMSCRIPTEN_KEEPALIVE gboolean +mono_wasm_diagnostic_server_create_thread (const char *websocket_url, pthread_t *out_thread_id); + void mono_wasm_event_pipe_set_early_startup_callback (mono_wasm_event_pipe_early_startup_cb callback); diff --git a/src/mono/wasm/runtime/cjs/dotnet.cjs.lib.js b/src/mono/wasm/runtime/cjs/dotnet.cjs.lib.js index 7bdb1d25418077..b92276bc36293b 100644 --- a/src/mono/wasm/runtime/cjs/dotnet.cjs.lib.js +++ b/src/mono/wasm/runtime/cjs/dotnet.cjs.lib.js @@ -91,9 +91,11 @@ const linked_functions = [ "dotnet_browser_encrypt_decrypt", "dotnet_browser_derive_bits", - /// mono-threads-wasm.c #if USE_PTHREADS + /// mono-threads-wasm.c "mono_wasm_pthread_on_pthread_attached", + /// diagnostics_server.c + "mono_wasm_diagnostic_server_on_server_thread_created", #endif ]; diff --git a/src/mono/wasm/runtime/cwraps.ts b/src/mono/wasm/runtime/cwraps.ts index 7fe0280b1e5842..3f88501acc7ff2 100644 --- a/src/mono/wasm/runtime/cwraps.ts +++ b/src/mono/wasm/runtime/cwraps.ts @@ -71,6 +71,7 @@ const fn_signatures: [ident: string, returnType: string | null, argTypes?: strin ["mono_wasm_event_pipe_enable", "bool", ["string", "number", "string", "bool", "number"]], ["mono_wasm_event_pipe_session_start_streaming", "bool", ["number"]], ["mono_wasm_event_pipe_session_disable", "bool", ["number"]], + ["mono_wasm_diagnostic_server_create_thread", "bool", ["string", "number"]], //DOTNET ["mono_wasm_string_from_js", "number", ["string"]], @@ -172,6 +173,7 @@ export interface t_Cwraps { mono_wasm_event_pipe_enable(outputPath: string, bufferSizeInMB: number, providers: string, rundownRequested: boolean, outSessionId: VoidPtr): boolean; mono_wasm_event_pipe_session_start_streaming(sessionId: number): boolean; mono_wasm_event_pipe_session_disable(sessionId: number): boolean; + mono_wasm_diagnostic_server_create_thread(websocketURL: string, threadIdOutPtr: VoidPtr): boolean; //DOTNET /** diff --git a/src/mono/wasm/runtime/diagnostic_server/browser/controller.ts b/src/mono/wasm/runtime/diagnostic_server/browser/controller.ts index b7d35c2c00d76e..7e96c174042106 100644 --- a/src/mono/wasm/runtime/diagnostic_server/browser/controller.ts +++ b/src/mono/wasm/runtime/diagnostic_server/browser/controller.ts @@ -1,55 +1,95 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -import type { DiagnosticOptions, EventPipeSessionOptions, EventPipeSessionIPCOptions, EventPipeSessionDiagnosticServerID } from "../../types"; -import type { EventPipeSessionIDImpl } from "../shared/types"; +import type { EventPipeSessionDiagnosticServerID } from "../../types"; +import cwraps from "../../cwraps"; +import { withStackAlloc, getI32 } from "../../memory"; +import { getThread, Thread } from "../../pthreads/browser"; +// interface ServerReadyResult { +// sessions?: (EventPipeSessionOptions & EventPipeSessionIPCOptions)[]; // provider configs +// } -interface ServerReadyResult { - sessions?: (EventPipeSessionOptions & EventPipeSessionIPCOptions)[]; // provider configs -} +// interface ServerConfigureResult { +// serverStarted: boolean; +// serverReady?: Promise; +// } -interface ServerConfigureResult { - serverStarted: boolean; - serverReady?: Promise; -} +// async function configureServer(options: DiagnosticOptions): Promise { +// if (options.server !== undefined && options.server) { +// // TODO start the server +// let serverReady: Promise; +// if (options.server == "wait") { +// //TODO: make a promise to wait for the connection +// serverReady = Promise.resolve({}); +// } else { +// // server is ready now, no need to wait +// serverReady = Promise.resolve({}); +// } +// // TODO: start the server and wait for a connection +// return { serverStarted: false, serverReady: serverReady }; +// } else +// return { serverStarted: false }; +// } -async function configureServer(options: DiagnosticOptions): Promise { - if (options.server !== undefined && options.server) { - // TODO start the server - let serverReady: Promise; - if (options.server == "wait") { - //TODO: make a promise to wait for the connection - serverReady = Promise.resolve({}); - } else { - // server is ready now, no need to wait - serverReady = Promise.resolve({}); - } - // TODO: start the server and wait for a connection - return { serverStarted: false, serverReady: serverReady }; - } else - return { serverStarted: false }; -} - -function postIPCStreamingSessionStarted(/*diagnosticSessionID: EventPipeSessionDiagnosticServerID, sessionID: EventPipeSessionIDImpl*/): void { - // TODO: For IPC streaming sessions this is the place to send back an acknowledgement with the session ID -} +// function postIPCStreamingSessionStarted(/*diagnosticSessionID: EventPipeSessionDiagnosticServerID, sessionID: EventPipeSessionIDImpl*/): void { +// // TODO: For IPC streaming sessions this is the place to send back an acknowledgement with the session ID +// } /// An object that can be used to control the diagnostic server. export interface ServerController { - configureServer(options: DiagnosticOptions): Promise; - postIPCStreamingSessionStarted(diagnosticSessionID: EventPipeSessionDiagnosticServerID, sessionID: EventPipeSessionIDImpl): void; + wait_for_resume(): Promise<{ sessions: EventPipeSessionDiagnosticServerID[] }>; + post_diagnostic_server_attach_to_runtime(): void; + // configureServer(options: DiagnosticOptions): Promise; + // postIPCStreamingSessionStarted(diagnosticSessionID: EventPipeSessionDiagnosticServerID, sessionID: EventPipeSessionIDImpl): void; } +class ServerControllerImpl implements ServerController { + constructor(private server: Thread) { } + async wait_for_resume(): Promise<{ sessions: EventPipeSessionDiagnosticServerID[] }> { + console.debug("waiting for the diagnostic server to allow us to resume"); + const promise = new Promise((resolve, /*reject*/) => { + setTimeout(() => { resolve(); }, 1000); + }); + await promise; + // let req = this.server.allocateRequest({ type: "diagnostic_server", cmd: "wait_for_resume" }); + // let respone = await this.server.sendAndWait(req); + // if (respone.type !== "diagnostic_server" || respone.cmd !== "wait_for_resume_response") { + // throw new Error("unexpected response"); + // } + return { sessions: [] }; + } + post_diagnostic_server_attach_to_runtime(): void { + console.debug("signal the diagnostic server to attach to the runtime"); + } +} + + let serverController: ServerController | null = null; export function getController(): ServerController { if (serverController) return serverController; - serverController = { - configureServer, - postIPCStreamingSessionStarted, - }; + throw new Error("unexpected no server controller"); +} + +export function startDiagnosticServer(websocket_url: string): ServerController | null { + const sizeOfPthreadT = 4; + const result: number | undefined = withStackAlloc(sizeOfPthreadT, (pthreadIdPtr) => { + if (!cwraps.mono_wasm_diagnostic_server_create_thread(websocket_url, pthreadIdPtr)) + return undefined; + const pthreadId = getI32(pthreadIdPtr); + return pthreadId; + }); + if (result === undefined) { + console.warn("diagnostic server failed to start"); + return null; + } + const thread = getThread(result); + if (thread === undefined) { + throw new Error("unexpected diagnostic server thread not found"); + } + serverController = new ServerControllerImpl(thread); return serverController; } diff --git a/src/mono/wasm/runtime/diagnostic_server/server_pthread/index.ts b/src/mono/wasm/runtime/diagnostic_server/server_pthread/index.ts index abb481d05968c5..832e072827c66b 100644 --- a/src/mono/wasm/runtime/diagnostic_server/server_pthread/index.ts +++ b/src/mono/wasm/runtime/diagnostic_server/server_pthread/index.ts @@ -17,7 +17,7 @@ class DiagnosticServer { } start(): void { - console.log("starting diagnostic server"); + console.log(`starting diagnostic server with url: ${this.websocketUrl}`); if (pthread_self) { pthread_self.addEventListenerFromBrowser(this.onMessage.bind(this)); @@ -49,8 +49,8 @@ class DiagnosticServer { /// Called by the runtime to initialize the diagnostic server workers -export function mono_wasm_diagnostic_server_start(websocketUrl: string): void { - console.debug("mono_wasm_diagnostic_server_start"); +export function mono_wasm_diagnostic_server_on_server_thread_created(websocketUrl: string): void { + console.debug("mono_wasm_diagnostic_server_on_server_thread_created"); const server = new DiagnosticServer(websocketUrl); server.start(); } diff --git a/src/mono/wasm/runtime/diagnostics.ts b/src/mono/wasm/runtime/diagnostics.ts index b5c3d42789a13c..09d2f8f08152d2 100644 --- a/src/mono/wasm/runtime/diagnostics.ts +++ b/src/mono/wasm/runtime/diagnostics.ts @@ -4,9 +4,9 @@ import { Module } from "./imports"; import cwraps from "./cwraps"; import type { DiagnosticOptions, EventPipeSession, EventPipeStreamingSession, EventPipeSessionOptions, EventPipeSessionIPCOptions, EventPipeSessionDiagnosticServerID, EventPipeSessionAutoStopOptions } from "./types"; -import { mono_assert, is_nullish } from "./types"; +import { is_nullish } from "./types"; import type { VoidPtr } from "./types/emscripten"; -import { getController } from "./diagnostic_server/browser/controller"; +import { getController, startDiagnosticServer } from "./diagnostic_server/browser/controller"; import * as memory from "./memory"; const sizeOfInt32 = 4; @@ -59,7 +59,7 @@ class EventPipeIPCSession extends EventPipeSessionBase implements EventPipeStrea throw new Error("implement me"); } postIPCStreamingSessionStarted() { - getController().postIPCStreamingSessionStarted(this._diagnosticServerID, this._sessionID); + // getController().postIPCStreamingSessionStarted(this._diagnosticServerID, this._sessionID); } } @@ -245,7 +245,7 @@ export class SessionOptionsBuilder { // a conter for the number of sessions created let totalSessions = 0; -function createSessionWithPtrCB(sessionIdOutPtr: VoidPtr, options: EventPipeSessionOptions & Partial | undefined, tracePath: string): false | number { +function createSessionWithPtrCB(sessionIdOutPtr: VoidPtr, options: EventPipeSessionOptions, tracePath: string): false | number { const defaultRundownRequested = true; const defaultProviders = ""; // empty string means use the default providers const defaultBufferSizeInMB = 1; @@ -272,7 +272,7 @@ export interface Diagnostics { } -let startup_session_configs: (EventPipeSessionOptions & EventPipeSessionAutoStopOptions & Partial)[] = []; +let startup_session_configs: ((EventPipeSessionOptions & EventPipeSessionAutoStopOptions) | EventPipeSessionIPCOptions)[] = []; let startup_sessions: (EventPipeSession | null)[] | null = null; export function mono_wasm_event_pipe_early_startup_callback(): void { @@ -287,32 +287,38 @@ export function mono_wasm_event_pipe_early_startup_callback(): void { } -function createAndStartEventPipeSession(options: (EventPipeSessionOptions & EventPipeSessionAutoStopOptions & Partial)): EventPipeSession | null { +function createAndStartEventPipeSession(options: ((EventPipeSessionOptions & EventPipeSessionAutoStopOptions) | EventPipeSessionIPCOptions)): EventPipeSession | null { const session = createEventPipeSession(options); if (session === null) { return null; } - if (session instanceof EventPipeIPCSession) { session.postIPCStreamingSessionStarted(); } session.start(); + return session; } -function createEventPipeSession(options?: EventPipeSessionOptions & Partial): EventPipeSession | null { +function createEventPipeSession(options?: EventPipeSessionOptions | EventPipeSessionIPCOptions): EventPipeSession | null { // The session trace is saved to a file in the VFS. The file name doesn't matter, // but we'd like it to be distinct from other traces. const tracePath = `/trace-${totalSessions++}.nettrace`; - const success = memory.withStackAlloc(sizeOfInt32, createSessionWithPtrCB, options, tracePath); + let success: number | false; + + if ((options)?.diagnostic_server_id !== undefined) { + success = false; + } else { + success = memory.withStackAlloc(sizeOfInt32, createSessionWithPtrCB, options, tracePath); + } if (success === false) return null; const sessionID = success; - if (options?.diagnostic_server_id !== undefined) { - return new EventPipeIPCSession(options.diagnostic_server_id, sessionID); + if ((options)?.diagnostic_server_id !== undefined) { + return new EventPipeIPCSession((options).diagnostic_server_id, sessionID); } else { const session = new EventPipeFileSession(sessionID, tracePath); return session; @@ -353,19 +359,15 @@ export const diagnostics: Diagnostics = { export async function mono_wasm_init_diagnostics(options: DiagnosticOptions): Promise { if (!is_nullish(options.server)) { - mono_assert(options.server !== undefined && (options.server === true || options.server === "wait"), "options.server must be a boolean or 'wait'"); - // serverController.startServer - // if diagnostic_options.await is true, wait for the server to get a connection - console.debug("in configure_diagnostics", options); - const q = await getController().configureServer(options); - const wait = options.server == "wait"; - if (q.serverStarted) { - if (wait && q.serverReady) { - // wait for the server to get a connection - const serverReadyResponse = await q.serverReady; - if (serverReadyResponse?.sessions) { - startup_session_configs.push(...serverReadyResponse.sessions); - } + const url = options.server.connect_url; + const suspend = options.server.suspend; + const controller = startDiagnosticServer(url); + if (controller) { + if (suspend) { + const response = await controller.wait_for_resume(); + const session_configs = response.sessions.map(session => { return { diagnostic_server_id: session }; }); + /// FIXME: decide if main thread or the diagnostic server will start the streaming sessions + startup_session_configs.push(...session_configs); } } } @@ -373,4 +375,9 @@ export async function mono_wasm_init_diagnostics(options: DiagnosticOptions): Pr startup_session_configs.push(...sessions); } +export function mono_wasm_diagnostic_server_attach(): void { + const controller = getController(); + controller.post_diagnostic_server_attach_to_runtime(); +} + export default diagnostics; diff --git a/src/mono/wasm/runtime/es6/dotnet.es6.lib.js b/src/mono/wasm/runtime/es6/dotnet.es6.lib.js index d88b21341f7c1c..938d8347b6531b 100644 --- a/src/mono/wasm/runtime/es6/dotnet.es6.lib.js +++ b/src/mono/wasm/runtime/es6/dotnet.es6.lib.js @@ -128,9 +128,11 @@ const linked_functions = [ "dotnet_browser_encrypt_decrypt", "dotnet_browser_derive_bits", - /// mono-threads-wasm.c #if USE_PTHREADS + /// mono-threads-wasm.c "mono_wasm_pthread_on_pthread_attached", + // diagnostics_server.c + "mono_wasm_diagnostic_server_on_server_thread_created", #endif ]; diff --git a/src/mono/wasm/runtime/exports.ts b/src/mono/wasm/runtime/exports.ts index 96f41f27ca689a..f058f363d1c535 100644 --- a/src/mono/wasm/runtime/exports.ts +++ b/src/mono/wasm/runtime/exports.ts @@ -56,6 +56,9 @@ import { import { mono_wasm_event_pipe_early_startup_callback } from "./diagnostics"; +import { + mono_wasm_diagnostic_server_on_server_thread_created, +} from "./diagnostic_server/server_pthread"; import { mono_wasm_typed_array_copy_to_ref, mono_wasm_typed_array_from_ref, mono_wasm_typed_array_copy_from_ref, mono_wasm_load_bytes_into_heap } from "./buffers"; import { mono_wasm_release_cs_owned_object } from "./gc-handles"; import cwraps from "./cwraps"; @@ -364,6 +367,8 @@ export const __initializeImportsAndExports: any = initializeImportsAndExports; / const mono_wasm_threads_exports = !MonoWasmThreads ? undefined : { // mono-threads-wasm.c mono_wasm_pthread_on_pthread_attached, + // diagnostics_server.c + mono_wasm_diagnostic_server_on_server_thread_created, }; // the methods would be visible to EMCC linker diff --git a/src/mono/wasm/runtime/types.ts b/src/mono/wasm/runtime/types.ts index 162bd3976a8b96..a5fdf99d24e379 100644 --- a/src/mono/wasm/runtime/types.ts +++ b/src/mono/wasm/runtime/types.ts @@ -181,11 +181,17 @@ export type CoverageProfilerOptions = { send_to?: string // should be in the format ::, default: 'WebAssembly.Runtime::DumpCoverageProfileData' (DumpCoverageProfileData stores the data into INTERNAL.coverage_profile_data.) } +/// Options to configure the diagnostic server +export type DiagnosticServerOptions = { + suspend: boolean, // if true, the server will suspend the app when it starts until a diagnostic tool tells the runtime to resume. + connect_url: string, // websocket URL to connect to. +} + /// Options to configure EventPipe sessions that will be created and started at runtime startup export type DiagnosticOptions = { sessions?: (EventPipeSessionOptions & EventPipeSessionAutoStopOptions)[], /// If true, the diagnostic server will be started. If "wait", the runtime will wait at startup until a diagnsotic session connects to the server - server?: boolean | "wait", + server?: DiagnosticServerOptions, } /// For EventPipe sessions that will be created and started at runtime startup From 00f73ba2c0ac651af801a1eab44d5e41c4f273d0 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Fri, 24 Jun 2022 21:33:55 -0400 Subject: [PATCH 26/84] WIP try to start the server It doesn't work right now because the MessagePort is not created until the server thread attaches to Mono, which doesn't happen because it's started before Mono. Also it doesn't yet send a resume event, so the main thread just blocks forever --- .../Wasm.Browser.EventPipe.Sample.csproj | 2 +- .../diagnostic_server/browser/controller.ts | 8 +++-- .../diagnostic_server/server_pthread/index.ts | 12 ++++--- src/mono/wasm/runtime/diagnostics.ts | 8 +++-- .../wasm/runtime/pthreads/browser/index.ts | 32 +++++++++++++++++++ src/mono/wasm/runtime/types.ts | 6 ++++ 6 files changed, 58 insertions(+), 10 deletions(-) diff --git a/src/mono/sample/wasm/browser-eventpipe/Wasm.Browser.EventPipe.Sample.csproj b/src/mono/sample/wasm/browser-eventpipe/Wasm.Browser.EventPipe.Sample.csproj index 91bf7e8dc924e7..f1689272f4907d 100644 --- a/src/mono/sample/wasm/browser-eventpipe/Wasm.Browser.EventPipe.Sample.csproj +++ b/src/mono/sample/wasm/browser-eventpipe/Wasm.Browser.EventPipe.Sample.csproj @@ -22,7 +22,7 @@ }' /> diff --git a/src/mono/wasm/runtime/diagnostic_server/browser/controller.ts b/src/mono/wasm/runtime/diagnostic_server/browser/controller.ts index 7e96c174042106..6afce0527cce7e 100644 --- a/src/mono/wasm/runtime/diagnostic_server/browser/controller.ts +++ b/src/mono/wasm/runtime/diagnostic_server/browser/controller.ts @@ -4,7 +4,7 @@ import type { EventPipeSessionDiagnosticServerID } from "../../types"; import cwraps from "../../cwraps"; import { withStackAlloc, getI32 } from "../../memory"; -import { getThread, Thread } from "../../pthreads/browser"; +import { Thread, waitForThread } from "../../pthreads/browser"; // interface ServerReadyResult { // sessions?: (EventPipeSessionOptions & EventPipeSessionIPCOptions)[]; // provider configs @@ -73,8 +73,9 @@ export function getController(): ServerController { throw new Error("unexpected no server controller"); } -export function startDiagnosticServer(websocket_url: string): ServerController | null { +export async function startDiagnosticServer(websocket_url: string): Promise { const sizeOfPthreadT = 4; + console.debug(`starting the diagnostic server url: ${websocket_url}`); const result: number | undefined = withStackAlloc(sizeOfPthreadT, (pthreadIdPtr) => { if (!cwraps.mono_wasm_diagnostic_server_create_thread(websocket_url, pthreadIdPtr)) return undefined; @@ -85,7 +86,8 @@ export function startDiagnosticServer(websocket_url: string): ServerController | console.warn("diagnostic server failed to start"); return null; } - const thread = getThread(result); + // have to wait until the message port is created + const thread = await waitForThread(result); if (thread === undefined) { throw new Error("unexpected diagnostic server thread not found"); } diff --git a/src/mono/wasm/runtime/diagnostic_server/server_pthread/index.ts b/src/mono/wasm/runtime/diagnostic_server/server_pthread/index.ts index 832e072827c66b..5276c1190b5441 100644 --- a/src/mono/wasm/runtime/diagnostic_server/server_pthread/index.ts +++ b/src/mono/wasm/runtime/diagnostic_server/server_pthread/index.ts @@ -4,20 +4,23 @@ /// import { pthread_self } from "../../pthreads/worker"; +import { Module } from "../../imports"; import { controlCommandReceived } from "./event_pipe"; import { isDiagnosticMessage } from "../shared/types"; +import { CharPtr } from "../../types/emscripten"; class DiagnosticServer { readonly websocketUrl: string; - readonly ws: WebSocket; + readonly ws: WebSocket | null; constructor(websocketUrl: string) { this.websocketUrl = websocketUrl; - this.ws = new WebSocket(this.websocketUrl); + this.ws = null; // new WebSocket(this.websocketUrl); } start(): void { console.log(`starting diagnostic server with url: ${this.websocketUrl}`); + // XXX FIXME: we started before the runtime is ready, so we don't get a port because it gets created on attach. if (pthread_self) { pthread_self.addEventListenerFromBrowser(this.onMessage.bind(this)); @@ -49,8 +52,9 @@ class DiagnosticServer { /// Called by the runtime to initialize the diagnostic server workers -export function mono_wasm_diagnostic_server_on_server_thread_created(websocketUrl: string): void { - console.debug("mono_wasm_diagnostic_server_on_server_thread_created"); +export function mono_wasm_diagnostic_server_on_server_thread_created(websocketUrlPtr: CharPtr): void { + const websocketUrl = Module.UTF8ToString(websocketUrlPtr); + console.debug(`mono_wasm_diagnostic_server_on_server_thread_created, url ${websocketUrl}`); const server = new DiagnosticServer(websocketUrl); server.start(); } diff --git a/src/mono/wasm/runtime/diagnostics.ts b/src/mono/wasm/runtime/diagnostics.ts index 09d2f8f08152d2..a1fb0a15d30455 100644 --- a/src/mono/wasm/runtime/diagnostics.ts +++ b/src/mono/wasm/runtime/diagnostics.ts @@ -359,11 +359,15 @@ export const diagnostics: Diagnostics = { export async function mono_wasm_init_diagnostics(options: DiagnosticOptions): Promise { if (!is_nullish(options.server)) { + if (options.server.connect_url === undefined || typeof (options.server.connect_url) !== "string") { + throw new Error("server.connect_url must be a string"); + } const url = options.server.connect_url; - const suspend = options.server.suspend; - const controller = startDiagnosticServer(url); + const suspend = options.server?.suspend ?? false; + const controller = await startDiagnosticServer(url); if (controller) { if (suspend) { + console.debug("waiting for the diagnostic server to resume us"); const response = await controller.wait_for_resume(); const session_configs = response.sessions.map(session => { return { diagnostic_server_id: session }; }); /// FIXME: decide if main thread or the diagnostic server will start the streaming sessions diff --git a/src/mono/wasm/runtime/pthreads/browser/index.ts b/src/mono/wasm/runtime/pthreads/browser/index.ts index cf2161cf236a1b..cb2e7970f1cbdb 100644 --- a/src/mono/wasm/runtime/pthreads/browser/index.ts +++ b/src/mono/wasm/runtime/pthreads/browser/index.ts @@ -12,6 +12,37 @@ export interface Thread { readonly port: MessagePort; } +interface ThreadCreateResolveReject { + resolve: (thread: Thread) => void, + reject: (error: Error) => void +} + +const thread_promises: Map = new Map(); + +/// wait until the thread with the given id has set up a message port to the runtime +export function waitForThread(pthread_ptr: pthread_ptr): Promise { + if (threads.has(pthread_ptr)) { + return Promise.resolve(threads.get(pthread_ptr)!); + } + const promise: Promise = new Promise((resolve, reject) => { + const arr = thread_promises.get(pthread_ptr); + if (arr === undefined) { + thread_promises.set(pthread_ptr, new Array({ resolve, reject })); + } else { + arr.push({ resolve, reject }); + } + }); + return promise; +} + +function resolvePromises(pthread_ptr: pthread_ptr, thread: Thread): void { + const arr = thread_promises.get(pthread_ptr); + if (arr !== undefined) { + arr.forEach(({ resolve }) => resolve(thread)); + thread_promises.delete(pthread_ptr); + } +} + function addThread(pthread_ptr: pthread_ptr, worker: Worker, port: MessagePort): Thread { const thread = { pthread_ptr, worker, port }; threads.set(pthread_ptr, thread); @@ -57,6 +88,7 @@ function monoWorkerMessageHandler(worker: Worker, ev: MessageEvent monoDedicatedChannelMessageFromWorkerToMain(ev, thread)); port.start(); + resolvePromises(pthread_id, thread); } } diff --git a/src/mono/wasm/runtime/types.ts b/src/mono/wasm/runtime/types.ts index a5fdf99d24e379..28a2d89769f4ce 100644 --- a/src/mono/wasm/runtime/types.ts +++ b/src/mono/wasm/runtime/types.ts @@ -316,6 +316,12 @@ export function is_nullish(value: T | null | undefined): value is null | unde return (value === undefined) || (value === null); } +/// returns true if the given value is not Thenable +/// +/// Useful if some function returns a value or a promise of a value. +export function notThenable(x: T | PromiseLike): x is T { + return typeof x !== "object" || typeof ((>x).then) !== "function"; +} /// An identifier for an EventPipe session. The id is unique during the lifetime of the runtime. /// Primarily intended for debugging purposes. From f803c3cc1e446e4e9d6d509618134736fe209b0f Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Wed, 29 Jun 2022 11:32:33 -0400 Subject: [PATCH 27/84] WIP diagnostic server server --- .../server_pthread/event_pipe.ts | 9 +++++++- .../diagnostic_server/server_pthread/index.ts | 22 +++++++++++++------ .../shared/controller-commands.ts | 5 +++++ 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/src/mono/wasm/runtime/diagnostic_server/server_pthread/event_pipe.ts b/src/mono/wasm/runtime/diagnostic_server/server_pthread/event_pipe.ts index d5c6b5c0d07362..15045949422e38 100644 --- a/src/mono/wasm/runtime/diagnostic_server/server_pthread/event_pipe.ts +++ b/src/mono/wasm/runtime/diagnostic_server/server_pthread/event_pipe.ts @@ -10,6 +10,10 @@ import type { /*DiagnosticServerControlCommandStart, DiagnosticServerControlCommandSetSessionID*/ } from "../shared/controller-commands"; +export interface DiagnosticServer { + stop(): void; +} + /// Everything the diagnostic server knows about a connection. /// The connection has a server ID and a websocket. If it's an eventpipe session, it will also have an eventpipe ID assigned when the runtime starts an EventPipe session. @@ -194,7 +198,7 @@ function startServer(url: string): SessionManager { let sessionManager: SessionManager | null = null; -export function controlCommandReceived(msg: DiagnosticMessage): void { +export function controlCommandReceived(server: DiagnosticServer, msg: DiagnosticMessage): void { const cmd = msg as DiagnosticServerControlCommand; switch (cmd.cmd) { case "start": @@ -202,6 +206,9 @@ export function controlCommandReceived(msg: DiagnosticMessage): void { throw new Error("server already started"); sessionManager = startServer(cmd.url); break; + case "stop": + server.stop(); + break; case "set_session_id": if (sessionManager === null) throw new Error("server not started"); diff --git a/src/mono/wasm/runtime/diagnostic_server/server_pthread/index.ts b/src/mono/wasm/runtime/diagnostic_server/server_pthread/index.ts index 5276c1190b5441..c61366c9aee913 100644 --- a/src/mono/wasm/runtime/diagnostic_server/server_pthread/index.ts +++ b/src/mono/wasm/runtime/diagnostic_server/server_pthread/index.ts @@ -8,9 +8,9 @@ import { Module } from "../../imports"; import { controlCommandReceived } from "./event_pipe"; import { isDiagnosticMessage } from "../shared/types"; import { CharPtr } from "../../types/emscripten"; +import { DiagnosticServer } from "./event_pipe"; - -class DiagnosticServer { +class DiagnosticServerImpl implements DiagnosticServer { readonly websocketUrl: string; readonly ws: WebSocket | null; constructor(websocketUrl: string) { @@ -33,19 +33,27 @@ class DiagnosticServer { } } + private stopRequested = false; + + stop(): void { + this.stopRequested = true; + } + private installTimeoutHandler(): void { - setTimeout(this.timeoutHandler.bind(this), 500); + if (!this.stopRequested) { + setTimeout(this.timeoutHandler.bind(this), 500); + } } - private timeoutHandler(this: DiagnosticServer): void { + private timeoutHandler(this: DiagnosticServerImpl): void { console.debug("ping from diagnostic server"); this.installTimeoutHandler(); } - onMessage(this: DiagnosticServer, event: MessageEvent): void { + onMessage(this: DiagnosticServerImpl, event: MessageEvent): void { const d = event.data; if (d && isDiagnosticMessage(d)) { - controlCommandReceived(d); + controlCommandReceived(this, d); } } } @@ -55,6 +63,6 @@ class DiagnosticServer { export function mono_wasm_diagnostic_server_on_server_thread_created(websocketUrlPtr: CharPtr): void { const websocketUrl = Module.UTF8ToString(websocketUrlPtr); console.debug(`mono_wasm_diagnostic_server_on_server_thread_created, url ${websocketUrl}`); - const server = new DiagnosticServer(websocketUrl); + const server = new DiagnosticServerImpl(websocketUrl); server.start(); } diff --git a/src/mono/wasm/runtime/diagnostic_server/shared/controller-commands.ts b/src/mono/wasm/runtime/diagnostic_server/shared/controller-commands.ts index 293b29571519b9..b825eac5ba347b 100644 --- a/src/mono/wasm/runtime/diagnostic_server/shared/controller-commands.ts +++ b/src/mono/wasm/runtime/diagnostic_server/shared/controller-commands.ts @@ -6,6 +6,7 @@ import { EventPipeSessionDiagnosticServerID, EventPipeSessionIDImpl, DiagnosticM export type DiagnosticServerControlCommand = DiagnosticServerControlCommandStart + | DiagnosticServerControlCommandStop | DiagnosticServerControlCommandSetSessionID ; @@ -14,6 +15,10 @@ export interface DiagnosticServerControlCommandStart extends DiagnosticMessage { url: string, // websocket url to connect to } +export interface DiagnosticServerControlCommandStop extends DiagnosticMessage { + cmd: "stop", +} + export interface DiagnosticServerControlCommandSetSessionID extends DiagnosticMessage { cmd: "set_session_id"; diagnostic_server_id: EventPipeSessionDiagnosticServerID; From 58851b4d6983ba00d3575f08097cdc383988e7bc Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Fri, 1 Jul 2022 16:45:23 -0400 Subject: [PATCH 28/84] Add a mock WebSocket connection to simulate the remote end Start the diagnostic server and have it perform the open/advertise steps with the mock. --- .../diagnostic_server/server_pthread/index.ts | 64 ++++++- .../server_pthread/mock-remote.ts | 163 ++++++++++++++++++ .../server_pthread/promise-controller.ts | 15 ++ 3 files changed, 235 insertions(+), 7 deletions(-) create mode 100644 src/mono/wasm/runtime/diagnostic_server/server_pthread/mock-remote.ts create mode 100644 src/mono/wasm/runtime/diagnostic_server/server_pthread/promise-controller.ts diff --git a/src/mono/wasm/runtime/diagnostic_server/server_pthread/index.ts b/src/mono/wasm/runtime/diagnostic_server/server_pthread/index.ts index c61366c9aee913..d94a207bcb89c2 100644 --- a/src/mono/wasm/runtime/diagnostic_server/server_pthread/index.ts +++ b/src/mono/wasm/runtime/diagnostic_server/server_pthread/index.ts @@ -10,6 +10,23 @@ import { isDiagnosticMessage } from "../shared/types"; import { CharPtr } from "../../types/emscripten"; import { DiagnosticServer } from "./event_pipe"; +import { mockScript } from "./mock-remote"; +import PromiseController from "./promise-controller"; + +//function delay(ms: number): Promise { +// return new Promise(resolve => setTimeout(resolve, ms)); +//} + +function addOneShotMessageEventListener(src: EventTarget): Promise> { + return new Promise((resolve) => { + const listener: (event: Event) => void = ((event: MessageEvent) => { + src.removeEventListener("message", listener); + resolve(event); + }) as (event: Event) => void; + src.addEventListener("message", listener); + }); +} + class DiagnosticServerImpl implements DiagnosticServer { readonly websocketUrl: string; readonly ws: WebSocket | null; @@ -29,27 +46,54 @@ class DiagnosticServerImpl implements DiagnosticServer { "cmd": "started", "thread_id": pthread_self.pthread_id }); - this.installTimeoutHandler(); } } private stopRequested = false; + private stopRequestedController = new PromiseController(); stop(): void { this.stopRequested = true; + this.stopRequestedController.resolve(); } - private installTimeoutHandler(): void { - if (!this.stopRequested) { - setTimeout(this.timeoutHandler.bind(this), 500); + async serverLoop(this: DiagnosticServerImpl): Promise { + while (!this.stopRequested) { + const firstPromise: Promise<["first", string] | ["second", undefined]> = this.advertiseAndWaitForClient().then((r) => ["first", r]); + const secondPromise: Promise<["first", string] | ["second", undefined]> = this.stopRequestedController.promise.then(() => ["second", undefined]); + const clientCommandState = await Promise.race([firstPromise, secondPromise]); + // dispatchClientCommand(clientCommandState); + if (clientCommandState[0] === "first") { + console.debug("command received: ", clientCommandState[1]); + } else if (clientCommandState[0] === "second") { + console.debug("stop requested"); + break; + } } } - private timeoutHandler(this: DiagnosticServerImpl): void { - console.debug("ping from diagnostic server"); - this.installTimeoutHandler(); + async advertiseAndWaitForClient(): Promise { + const sock = mockScript.open(); + const p = addOneShotMessageEventListener(sock); + sock.send("ADVR"); + const message = await p; + return message.data.toString(); } + // async eventPipeSessionLoop(): Promise { + // await runtimeStarted(); + // const eventPipeFlushThread = await enableEventPipeSessionAndSignalResume(); + // while (!this.stopRequested) { + // const outcome = await oneOfStoppedOrMessageReceived(eventPipeFlushThread); + // if (outcome === "stopped") { + // break; + // } else { + // sendEPBufferToWebSocket(outcome); + // } + // } + // await closeWebSocket(); + // } + onMessage(this: DiagnosticServerImpl, event: MessageEvent): void { const d = event.data; if (d && isDiagnosticMessage(d)) { @@ -65,4 +109,10 @@ export function mono_wasm_diagnostic_server_on_server_thread_created(websocketUr console.debug(`mono_wasm_diagnostic_server_on_server_thread_created, url ${websocketUrl}`); const server = new DiagnosticServerImpl(websocketUrl); server.start(); + queueMicrotask(() => { + mockScript.run(); + }); + queueMicrotask(() => { + server.serverLoop(); + }); } diff --git a/src/mono/wasm/runtime/diagnostic_server/server_pthread/mock-remote.ts b/src/mono/wasm/runtime/diagnostic_server/server_pthread/mock-remote.ts new file mode 100644 index 00000000000000..09398dae4faaa9 --- /dev/null +++ b/src/mono/wasm/runtime/diagnostic_server/server_pthread/mock-remote.ts @@ -0,0 +1,163 @@ + +import PromiseController from "./promise-controller"; + +interface MockRemoteEventMap { + "open": Event; + "close": CloseEvent; + "message": MessageEvent; + "error": Event; +} + +export interface MockRemoteSocket extends EventTarget { + addEventListener(event: keyof MockRemoteEventMap, listener: (event: MockRemoteEventMap[keyof MockRemoteEventMap]) => void): void; + removeEventListener(event: keyof MockRemoteEventMap, listener: (event: MockRemoteEventMap[keyof MockRemoteEventMap]) => void): void; + send(data: string | ArrayBuffer | Uint8Array | Blob | DataView): void; + close(): void; +} + +export interface Mock { + open(): MockRemoteSocket; + run(): Promise; +} + +class MockScriptEngineSocketImpl implements MockRemoteSocket { + constructor(private readonly engine: MockScriptEngineImpl) { } + send(data: string | ArrayBuffer): void { + if (this.engine.trace) { + console.debug(`mock ${this.engine.ident} client sent: `, data); + } + let event: MessageEvent | null = null; + if (typeof data === "string") { + event = new MessageEvent("message", { data }); + } else { + const message = new ArrayBuffer(data.byteLength); + const messageView = new Uint8Array(message); + const dataView = new Uint8Array(data); + messageView.set(dataView); + event = new MessageEvent("message", { data: message }); + } + this.engine.mockReplyEventTarget.dispatchEvent(event); + } + addEventListener(event: keyof MockRemoteEventMap, listener: (event: Event | CloseEvent | MessageEvent) => void): void { + if (this.engine.trace) { + console.debug(`mock ${this.engine.ident} client added listener for ${event}`); + } + this.engine.eventTarget.addEventListener(event, listener); + } + removeEventListener(event: keyof MockRemoteEventMap, listener: (event: Event | CloseEvent | MessageEvent) => void): void { + if (this.engine.trace) { + console.debug(`mock ${this.engine.ident} client removed listener for ${event}`); + } + this.engine.eventTarget.removeEventListener(event, listener); + } + close(): void { + if (this.engine.trace) { + console.debug(`mock ${this.engine.ident} client closed`); + } + this.engine.mockReplyEventTarget.dispatchEvent(new CloseEvent("close")); + } + dispatchEvent(): boolean { + throw new Error("don't call dispatchEvent on a MockRemoteSocket"); + } +} + +class MockScriptEngineImpl implements MockScriptEngine { + readonly socket: MockRemoteSocket; + // eventTarget that the MockReplySocket will dispatch to + readonly eventTarget: EventTarget = new EventTarget(); + // eventTarget that the MockReplySocket with send() to + readonly mockReplyEventTarget: EventTarget = new EventTarget(); + constructor(readonly trace: boolean, readonly ident: number) { + this.socket = new MockScriptEngineSocketImpl(this); + } + + reply(data: string | ArrayBuffer) { + if (this.trace) { + console.debug(`mock ${this.ident} reply:`, data); + } + this.eventTarget.dispatchEvent(new MessageEvent("message", { data })); + } + + async waitForSend(filter: (data: string | ArrayBuffer) => boolean): Promise { + const trace = this.trace; + if (trace) { + console.debug(`mock ${this.ident} waitForSend`); + } + const event = await new Promise>((resolve) => { + this.mockReplyEventTarget.addEventListener("message", (event) => { + if (trace) { + console.debug(`mock ${this.ident} waitForSend got:`, event); + } + resolve(event as MessageEvent); + }, { once: true }); + }); + if (!filter(event.data)) { + throw new Error("Unexpected data"); + } + return; + } +} + +interface MockOptions { + readonly trace: boolean; +} + +class MockImpl { + openCount: number; + engines: MockScriptEngineImpl[]; + readonly trace: boolean; + constructor(public readonly script: ((engine: MockScriptEngine) => Promise)[], options?: MockOptions) { + this.openCount = 0; + this.trace = options?.trace ?? false; + const count = script.length; + this.engines = new Array(count); + for (let i = 0; i < count; ++i) { + this.engines[i] = new MockScriptEngineImpl(this.trace, i); + } + } + open(): MockRemoteSocket { + const i = this.openCount++; + if (this.trace) { + console.debug(`mock ${i} open`); + } + return this.engines[i].socket; + } + + async run(): Promise { + await Promise.all(this.script.map((script, i) => script(this.engines[i]))); + } +} + +export interface MockScriptEngine { + waitForSend(filter: (data: string | ArrayBuffer) => boolean): Promise; + waitForSend(filter: (data: string | ArrayBuffer) => boolean, extract: (data: string | ArrayBuffer) => T): Promise; + reply(data: string | ArrayBuffer): void; +} +export function mock(script: ((engine: MockScriptEngine) => Promise)[], options?: MockOptions): Mock { + return new MockImpl(script, options); +} + +function expectAdvertise(data: string | ArrayBuffer) { return data === "ADVR"; } + +const scriptPC = new PromiseController(); +const scriptPCunfulfilled = new PromiseController(); + +const script: ((engine: MockScriptEngine) => Promise)[] = [ + async (engine) => { + await engine.waitForSend(expectAdvertise); + engine.reply("start session"); + scriptPC.resolve(); + }, + async (engine) => { + await engine.waitForSend(expectAdvertise); + await scriptPC.promise; + engine.reply("resume"); + // engine.close(); + }, + async (engine) => { + await engine.waitForSend(expectAdvertise); + await scriptPCunfulfilled.promise; + } +]; + +export const mockScript = mock(script, { trace: true }); diff --git a/src/mono/wasm/runtime/diagnostic_server/server_pthread/promise-controller.ts b/src/mono/wasm/runtime/diagnostic_server/server_pthread/promise-controller.ts new file mode 100644 index 00000000000000..5e6fe7ef9e5b8d --- /dev/null +++ b/src/mono/wasm/runtime/diagnostic_server/server_pthread/promise-controller.ts @@ -0,0 +1,15 @@ +export default class PromiseController { + readonly promise: Promise; + readonly resolve: (value: T | PromiseLike) => void; + readonly reject: (reason: any) => void; + constructor() { + let rs: (value: T | PromiseLike) => void = undefined as any; + let rj: (reason: any) => void = undefined as any; + this.promise = new Promise((resolve, reject) => { + rs = resolve; + rj = reject; + }); + this.resolve = rs; + this.reject = rj; + } +} From c497c595c59d728b17c7dee38fa9b4d305fd0904 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Tue, 5 Jul 2022 13:14:12 -0400 Subject: [PATCH 29/84] cleanup diagnostics.ts we're not going ot need a JS version of the DS EP streaming sessions. And file-based auto-stop sessions are not going in --- src/mono/wasm/runtime/diagnostics.ts | 98 ++++++++-------------------- src/mono/wasm/runtime/types.ts | 27 +------- 2 files changed, 27 insertions(+), 98 deletions(-) diff --git a/src/mono/wasm/runtime/diagnostics.ts b/src/mono/wasm/runtime/diagnostics.ts index a1fb0a15d30455..51ffd6c7d20e51 100644 --- a/src/mono/wasm/runtime/diagnostics.ts +++ b/src/mono/wasm/runtime/diagnostics.ts @@ -3,7 +3,11 @@ import { Module } from "./imports"; import cwraps from "./cwraps"; -import type { DiagnosticOptions, EventPipeSession, EventPipeStreamingSession, EventPipeSessionOptions, EventPipeSessionIPCOptions, EventPipeSessionDiagnosticServerID, EventPipeSessionAutoStopOptions } from "./types"; +import type { + DiagnosticOptions, + EventPipeSessionOptions, + EventPipeSessionID, +} from "./types"; import { is_nullish } from "./types"; import type { VoidPtr } from "./types/emscripten"; import { getController, startDiagnosticServer } from "./diagnostic_server/browser/controller"; @@ -13,6 +17,20 @@ const sizeOfInt32 = 4; type EventPipeSessionIDImpl = number; +// An EventPipe session object represents a single diagnostic tracing session that is collecting +/// events from the runtime and managed libraries. There may be multiple active sessions at the same time. +/// Each session subscribes to a number of providers and will collect events from the time that start() is called, until stop() is called. +/// Upon completion the session saves the events to a file on the VFS. +/// The data can then be retrieved as Blob. +export interface EventPipeSession { + // session ID for debugging logging only + get sessionID(): EventPipeSessionID; + isIPCStreamingSession(): boolean; + start(): void; + stop(): void; + getTraceBlob(): Blob; +} + // internal session state of the JS instance enum State { @@ -30,38 +48,9 @@ function stop_streaming(sessionID: EventPipeSessionIDImpl): void { } abstract class EventPipeSessionBase { - isIPCStreamingSession(): this is EventPipeStreamingSession { - return this instanceof EventPipeIPCSession; - } + isIPCStreamingSession() { return false; } } -class EventPipeIPCSession extends EventPipeSessionBase implements EventPipeStreamingSession { - private _sessionID: EventPipeSessionIDImpl; - private _diagnosticServerID: EventPipeSessionDiagnosticServerID; - - constructor(diagnosticServerID: EventPipeSessionDiagnosticServerID, sessionID: EventPipeSessionIDImpl) { - super(); - this._sessionID = sessionID; - this._diagnosticServerID = diagnosticServerID; - } - - get sessionID(): bigint { - return BigInt(this._sessionID); - } - - start() { - throw new Error("implement me"); - } - stop() { - throw new Error("implement me"); - } - getTraceBlob(): Blob { - throw new Error("implement me"); - } - postIPCStreamingSessionStarted() { - // getController().postIPCStreamingSessionStarted(this._diagnosticServerID, this._sessionID); - } -} /// An EventPipe session that saves the event data to a file in the VFS. class EventPipeFileSession extends EventPipeSessionBase implements EventPipeSession { @@ -106,25 +95,6 @@ class EventPipeFileSession extends EventPipeSessionBase implements EventPipeSess } } -// an EventPipeSession that starts at runtime startup -class StartupEventPipeFileSession extends EventPipeFileSession implements EventPipeSession { - readonly _on_stop_callback: null | ((session: EventPipeSession) => void); - constructor(sessionID: EventPipeSessionIDImpl, tracePath: string, on_stop_callback?: (session: EventPipeSession) => void) { - super(sessionID, tracePath); - // By the time we create the JS object, it's already running - this._state = State.Started; - this._on_stop_callback = on_stop_callback ?? null; - } - - stop = () => { - super.stop(); - if (this._on_stop_callback !== null) { - const cb = this._on_stop_callback; - setTimeout(cb, 0, this); - } - } -} - const eventLevel = { LogAlways: 0, Critical: 1, @@ -272,7 +242,7 @@ export interface Diagnostics { } -let startup_session_configs: ((EventPipeSessionOptions & EventPipeSessionAutoStopOptions) | EventPipeSessionIPCOptions)[] = []; +let startup_session_configs: EventPipeSessionOptions[] = []; let startup_sessions: (EventPipeSession | null)[] | null = null; export function mono_wasm_event_pipe_early_startup_callback(): void { @@ -287,42 +257,28 @@ export function mono_wasm_event_pipe_early_startup_callback(): void { } -function createAndStartEventPipeSession(options: ((EventPipeSessionOptions & EventPipeSessionAutoStopOptions) | EventPipeSessionIPCOptions)): EventPipeSession | null { +function createAndStartEventPipeSession(options: (EventPipeSessionOptions)): EventPipeSession | null { const session = createEventPipeSession(options); if (session === null) { return null; } - if (session instanceof EventPipeIPCSession) { - session.postIPCStreamingSessionStarted(); - } session.start(); return session; } -function createEventPipeSession(options?: EventPipeSessionOptions | EventPipeSessionIPCOptions): EventPipeSession | null { +function createEventPipeSession(options?: EventPipeSessionOptions): EventPipeSession | null { // The session trace is saved to a file in the VFS. The file name doesn't matter, // but we'd like it to be distinct from other traces. const tracePath = `/trace-${totalSessions++}.nettrace`; - let success: number | false; - - if ((options)?.diagnostic_server_id !== undefined) { - success = false; - } else { - success = memory.withStackAlloc(sizeOfInt32, createSessionWithPtrCB, options, tracePath); - } + const success = memory.withStackAlloc(sizeOfInt32, createSessionWithPtrCB, options, tracePath); if (success === false) return null; const sessionID = success; - if ((options)?.diagnostic_server_id !== undefined) { - return new EventPipeIPCSession((options).diagnostic_server_id, sessionID); - } else { - const session = new EventPipeFileSession(sessionID, tracePath); - return session; - } + return new EventPipeFileSession(sessionID, tracePath); } /// APIs for working with .NET diagnostics from JavaScript. @@ -369,9 +325,7 @@ export async function mono_wasm_init_diagnostics(options: DiagnosticOptions): Pr if (suspend) { console.debug("waiting for the diagnostic server to resume us"); const response = await controller.wait_for_resume(); - const session_configs = response.sessions.map(session => { return { diagnostic_server_id: session }; }); - /// FIXME: decide if main thread or the diagnostic server will start the streaming sessions - startup_session_configs.push(...session_configs); + console.debug("diagnostic server resumed us", response); } } } diff --git a/src/mono/wasm/runtime/types.ts b/src/mono/wasm/runtime/types.ts index 28a2d89769f4ce..7bdb805d83f61c 100644 --- a/src/mono/wasm/runtime/types.ts +++ b/src/mono/wasm/runtime/types.ts @@ -189,20 +189,11 @@ export type DiagnosticServerOptions = { /// Options to configure EventPipe sessions that will be created and started at runtime startup export type DiagnosticOptions = { - sessions?: (EventPipeSessionOptions & EventPipeSessionAutoStopOptions)[], + sessions?: EventPipeSessionOptions[], /// If true, the diagnostic server will be started. If "wait", the runtime will wait at startup until a diagnsotic session connects to the server server?: DiagnosticServerOptions, } -/// For EventPipe sessions that will be created and started at runtime startup -export interface EventPipeSessionAutoStopOptions { - /// Should be in the format ::, default: 'WebAssembly.Runtime::StopProfile' - /// The session will be stopped when the jit_done event is fired for this method. - stop_at?: string; - /// Called after the session has been stopped. - on_session_stopped?: (session: EventPipeSession) => void; -} - export type EventPipeSessionDiagnosticServerID = number; /// For EventPipe sessions started by the diagnostic server, an id is assigned to each session before it is associated with an eventpipe session in the runtime. export interface EventPipeSessionIPCOptions { @@ -327,19 +318,3 @@ export function notThenable(x: T | PromiseLike): x is T { /// Primarily intended for debugging purposes. export type EventPipeSessionID = bigint; - -/// An EventPipe session object represents a single diagnostic tracing session that is collecting -/// events from the runtime and managed libraries. There may be multiple active sessions at the same time. -/// Each session subscribes to a number of providers and will collect events from the time that start() is called, until stop() is called. -/// Upon completion the session saves the events to a file on the VFS. -/// The data can then be retrieved as Blob. -export interface EventPipeSession { - // session ID for debugging logging only - get sessionID(): EventPipeSessionID; - isIPCStreamingSession(): this is EventPipeStreamingSession; - start(): void; - stop(): void; - getTraceBlob(): Blob; -} - -export type EventPipeStreamingSession = EventPipeSession From 74d844314cc76177dd5c2bed7b21b6532fc623ec Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Tue, 5 Jul 2022 13:15:39 -0400 Subject: [PATCH 30/84] wasm-mt: use a PThreadSelf struct instead of a raw MessagePort --- .../wasm/runtime/pthreads/shared/index.ts | 4 +- .../wasm/runtime/pthreads/worker/events.ts | 29 ++++++++------ .../wasm/runtime/pthreads/worker/index.ts | 40 +++++++------------ src/mono/wasm/runtime/startup.ts | 2 +- 4 files changed, 34 insertions(+), 41 deletions(-) diff --git a/src/mono/wasm/runtime/pthreads/shared/index.ts b/src/mono/wasm/runtime/pthreads/shared/index.ts index 1b53b1799a0bd8..ef71b31022af30 100644 --- a/src/mono/wasm/runtime/pthreads/shared/index.ts +++ b/src/mono/wasm/runtime/pthreads/shared/index.ts @@ -8,14 +8,14 @@ export type pthread_ptr = number; export interface PThreadInfo { readonly pthread_id: pthread_ptr; - readonly is_main_thread: boolean; + readonly isBrowserThread: boolean; } export const MainThread: PThreadInfo = { get pthread_id(): pthread_ptr { return getBrowserThreadID(); }, - is_main_thread: true + isBrowserThread: true }; let browser_thread_id_lazy: pthread_ptr | undefined; diff --git a/src/mono/wasm/runtime/pthreads/worker/events.ts b/src/mono/wasm/runtime/pthreads/worker/events.ts index 4544811fd96315..7022497d0a4fc4 100644 --- a/src/mono/wasm/runtime/pthreads/worker/events.ts +++ b/src/mono/wasm/runtime/pthreads/worker/events.ts @@ -1,5 +1,14 @@ import MonoWasmThreads from "consts:monoWasmThreads"; -import type { pthread_ptr } from "../shared"; +import type { pthread_ptr, PThreadInfo, MonoThreadMessage } from "../shared"; + +/// Identification of the current thread executing on a worker +export interface PThreadSelf extends PThreadInfo { + readonly pthread_id: pthread_ptr; + readonly portToBrowser: MessagePort; + readonly isBrowserThread: boolean; + postMessageToBrowser: (message: T, transfer?: Transferable[]) => void; + addEventListenerFromBrowser: (listener: (event: MessageEvent) => void) => void; +} export const dotnetPthreadCreated = "dotnet:pthread:created" as const; export const dotnetPthreadAttached = "dotnet:pthread:attached" as const; @@ -15,8 +24,7 @@ export interface WorkerThreadEventMap { } export interface WorkerThreadEvent extends Event { - readonly pthread_ptr: pthread_ptr; - readonly portToMain: MessagePort; + readonly pthread_self: PThreadSelf; } export interface WorkerThreadEventTarget extends EventTarget { @@ -26,18 +34,15 @@ export interface WorkerThreadEventTarget extends EventTarget { addEventListener(type: string, callback: EventListenerOrEventListenerObject | null, options?: boolean | AddEventListenerOptions): void; } -let WorkerThreadEventClassConstructor: new (type: keyof WorkerThreadEventMap, pthread_ptr: pthread_ptr, port: MessagePort) => WorkerThreadEvent; -export const makeWorkerThreadEvent: (type: keyof WorkerThreadEventMap, pthread_ptr: pthread_ptr, port: MessagePort) => WorkerThreadEvent = !MonoWasmThreads +let WorkerThreadEventClassConstructor: new (type: keyof WorkerThreadEventMap, pthread_self: PThreadSelf) => WorkerThreadEvent; +export const makeWorkerThreadEvent: (type: keyof WorkerThreadEventMap, pthread_self: PThreadSelf) => WorkerThreadEvent = !MonoWasmThreads ? (() => { throw new Error("threads support disabled"); }) - : ((type: keyof WorkerThreadEventMap, pthread_ptr: pthread_ptr, port: MessagePort) => { + : ((type: keyof WorkerThreadEventMap, pthread_self: PThreadSelf) => { if (!WorkerThreadEventClassConstructor) WorkerThreadEventClassConstructor = class WorkerThreadEventImpl extends Event implements WorkerThreadEvent { - readonly pthread_ptr: pthread_ptr; - readonly portToMain: MessagePort; - constructor(type: keyof WorkerThreadEventMap, pthread_ptr: pthread_ptr, portToMain: MessagePort) { + constructor(type: keyof WorkerThreadEventMap, readonly pthread_self: PThreadSelf) { super(type); - this.pthread_ptr = pthread_ptr; - this.portToMain = portToMain; } }; - return new WorkerThreadEventClassConstructor(type, pthread_ptr, port); + return new WorkerThreadEventClassConstructor(type, pthread_self); }); + diff --git a/src/mono/wasm/runtime/pthreads/worker/index.ts b/src/mono/wasm/runtime/pthreads/worker/index.ts index 5172dc59a7cdd8..c350ef5e99eabc 100644 --- a/src/mono/wasm/runtime/pthreads/worker/index.ts +++ b/src/mono/wasm/runtime/pthreads/worker/index.ts @@ -7,8 +7,9 @@ import MonoWasmThreads from "consts:monoWasmThreads"; import { Module, ENVIRONMENT_IS_PTHREAD } from "../../imports"; import { makeChannelCreatedMonoMessage, pthread_ptr } from "../shared"; import { mono_assert, is_nullish } from "../../types"; -import type { MonoThreadMessage, PThreadInfo } from "../shared"; +import type { MonoThreadMessage } from "../shared"; import { + PThreadSelf, makeWorkerThreadEvent, dotnetPthreadCreated, dotnetPthreadAttached, @@ -24,28 +25,18 @@ export { WorkerThreadEventTarget, } from "./events"; -export interface PThreadSelf extends PThreadInfo { - postMessageToBrowser: (message: T, transfer?: Transferable[]) => void; - addEventListenerFromBrowser: (listener: (event: MessageEvent) => void) => void; -} - class WorkerSelf implements PThreadSelf { - readonly port: MessagePort; - readonly pthread_id: pthread_ptr; - readonly is_main_thread = false; - constructor(pthread_id: pthread_ptr, port: MessagePort) { - this.port = port; - this.pthread_id = pthread_id; - } + readonly isBrowserThread = false; + constructor(readonly pthread_id: pthread_ptr, readonly portToBrowser: MessagePort) { } postMessageToBrowser(message: MonoThreadMessage, transfer?: Transferable[]) { if (transfer) { - this.port.postMessage(message, transfer); + this.portToBrowser.postMessage(message, transfer); } else { - this.port.postMessage(message); + this.portToBrowser.postMessage(message); } } addEventListenerFromBrowser(listener: (event: MessageEvent) => void) { - this.port.addEventListener("message", listener); + this.portToBrowser.addEventListener("message", listener); } } @@ -64,28 +55,25 @@ function monoDedicatedChannelMessageFromMainToWorker(event: MessageEvent console.debug("got message from main on the dedicated channel", event.data); } -let portToMain: MessagePort | null = null; - -function setupChannelToMainThread(pthread_ptr: pthread_ptr): MessagePort { +function setupChannelToMainThread(pthread_ptr: pthread_ptr): PThreadSelf { console.debug("creating a channel", pthread_ptr); const channel = new MessageChannel(); const workerPort = channel.port1; const mainPort = channel.port2; workerPort.addEventListener("message", monoDedicatedChannelMessageFromMainToWorker); workerPort.start(); - portToMain = workerPort; pthread_self = new WorkerSelf(pthread_ptr, workerPort); self.postMessage(makeChannelCreatedMonoMessage(pthread_ptr, mainPort), [mainPort]); - return workerPort; + return pthread_self; } /// This is an implementation detail function. /// Called in the worker thread from mono when a pthread becomes attached to the mono runtime. export function mono_wasm_pthread_on_pthread_attached(pthread_id: pthread_ptr): void { - const port = portToMain; - mono_assert(port !== null, "expected a port to the main thread"); + const self = pthread_self; + mono_assert(self !== null && self.pthread_id == pthread_id, "expected pthread_self to be set already when attaching"); console.debug("attaching pthread to runtime", pthread_id); - currentWorkerThreadEvents.dispatchEvent(makeWorkerThreadEvent(dotnetPthreadAttached, pthread_id, port)); + currentWorkerThreadEvents.dispatchEvent(makeWorkerThreadEvent(dotnetPthreadAttached, self)); } /// This is an implementation detail function. @@ -97,7 +85,7 @@ export function afterThreadInitTLS(): void { const pthread_ptr = (Module)["_pthread_self"](); mono_assert(!is_nullish(pthread_ptr), "pthread_self() returned null"); console.debug("after thread init, pthread ptr", pthread_ptr); - const port = setupChannelToMainThread(pthread_ptr); - currentWorkerThreadEvents.dispatchEvent(makeWorkerThreadEvent(dotnetPthreadCreated, pthread_ptr, port)); + const self = setupChannelToMainThread(pthread_ptr); + currentWorkerThreadEvents.dispatchEvent(makeWorkerThreadEvent(dotnetPthreadCreated, self)); } } diff --git a/src/mono/wasm/runtime/startup.ts b/src/mono/wasm/runtime/startup.ts index 04406ac75eda91..4863e690efc1c5 100644 --- a/src/mono/wasm/runtime/startup.ts +++ b/src/mono/wasm/runtime/startup.ts @@ -808,6 +808,6 @@ async function mono_wasm_pthread_worker_init(): Promise { // This is a good place for subsystems to attach listeners for pthreads_worker.currrentWorkerThreadEvents console.debug("mono_wasm_pthread_worker_init"); pthreads_worker.currentWorkerThreadEvents.addEventListener(pthreads_worker.dotnetPthreadCreated, (ev) => { - console.debug("thread created", ev.pthread_ptr); + console.debug("thread created", ev.pthread_self.pthread_id); }); } From 9ce734812ee4ab64b75a4a4f493520b9e29bc7e1 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Tue, 5 Jul 2022 13:31:38 -0400 Subject: [PATCH 31/84] Move all the EP and diagnostic server modules to one directory --- .../{diagnostic_server => diagnostics}/README.md | 2 ++ .../browser/controller.ts | 0 .../diagnostic-server.md | 0 .../{diagnostics.ts => diagnostics/index.ts} | 14 +++++++------- .../server_pthread/event_pipe.ts | 0 .../server_pthread/index.ts | 0 .../server_pthread/mock-remote.ts | 0 .../server_pthread/promise-controller.ts | 0 .../server_pthread/tsconfig.json | 0 .../shared/controller-commands.ts | 0 .../shared/tsconfig.json | 0 .../shared/types.ts | 0 src/mono/wasm/runtime/exports.ts | 2 +- 13 files changed, 10 insertions(+), 8 deletions(-) rename src/mono/wasm/runtime/{diagnostic_server => diagnostics}/README.md (78%) rename src/mono/wasm/runtime/{diagnostic_server => diagnostics}/browser/controller.ts (100%) rename src/mono/wasm/runtime/{diagnostic_server => diagnostics}/diagnostic-server.md (100%) rename src/mono/wasm/runtime/{diagnostics.ts => diagnostics/index.ts} (97%) rename src/mono/wasm/runtime/{diagnostic_server => diagnostics}/server_pthread/event_pipe.ts (100%) rename src/mono/wasm/runtime/{diagnostic_server => diagnostics}/server_pthread/index.ts (100%) rename src/mono/wasm/runtime/{diagnostic_server => diagnostics}/server_pthread/mock-remote.ts (100%) rename src/mono/wasm/runtime/{diagnostic_server => diagnostics}/server_pthread/promise-controller.ts (100%) rename src/mono/wasm/runtime/{diagnostic_server => diagnostics}/server_pthread/tsconfig.json (100%) rename src/mono/wasm/runtime/{diagnostic_server => diagnostics}/shared/controller-commands.ts (100%) rename src/mono/wasm/runtime/{diagnostic_server => diagnostics}/shared/tsconfig.json (100%) rename src/mono/wasm/runtime/{diagnostic_server => diagnostics}/shared/types.ts (100%) diff --git a/src/mono/wasm/runtime/diagnostic_server/README.md b/src/mono/wasm/runtime/diagnostics/README.md similarity index 78% rename from src/mono/wasm/runtime/diagnostic_server/README.md rename to src/mono/wasm/runtime/diagnostics/README.md index fe73aa17104d80..964c9b563d864f 100644 --- a/src/mono/wasm/runtime/diagnostic_server/README.md +++ b/src/mono/wasm/runtime/diagnostics/README.md @@ -2,9 +2,11 @@ What's in here: +- `index.ts` toplevel APIs - `browser/` APIs for the main thread. The main thread has 2 responsibilities: - control the overall diagnostic server `browser/controller.ts` - establish communication channels between EventPipe session streaming threads and the diagnostic server pthread - `server_pthread/` A long-running worker that owns the WebSocket connections out of the browser and that receives the session payloads from the streaming threads. - `pthread/` (**TODO* decide if this is necessary) APIs for normal pthreads that need to do things to diagnostics - `shared/` type definitions to be shared between the worker and browser main thread +- `mock/` a utility to fake WebSocket connectings by playing back a script. Used for prototyping the diagnostic server without hooking up to a real WebSocket. diff --git a/src/mono/wasm/runtime/diagnostic_server/browser/controller.ts b/src/mono/wasm/runtime/diagnostics/browser/controller.ts similarity index 100% rename from src/mono/wasm/runtime/diagnostic_server/browser/controller.ts rename to src/mono/wasm/runtime/diagnostics/browser/controller.ts diff --git a/src/mono/wasm/runtime/diagnostic_server/diagnostic-server.md b/src/mono/wasm/runtime/diagnostics/diagnostic-server.md similarity index 100% rename from src/mono/wasm/runtime/diagnostic_server/diagnostic-server.md rename to src/mono/wasm/runtime/diagnostics/diagnostic-server.md diff --git a/src/mono/wasm/runtime/diagnostics.ts b/src/mono/wasm/runtime/diagnostics/index.ts similarity index 97% rename from src/mono/wasm/runtime/diagnostics.ts rename to src/mono/wasm/runtime/diagnostics/index.ts index 51ffd6c7d20e51..083b147bb96411 100644 --- a/src/mono/wasm/runtime/diagnostics.ts +++ b/src/mono/wasm/runtime/diagnostics/index.ts @@ -1,17 +1,17 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -import { Module } from "./imports"; -import cwraps from "./cwraps"; +import { Module } from "../imports"; +import cwraps from "../cwraps"; import type { DiagnosticOptions, EventPipeSessionOptions, EventPipeSessionID, -} from "./types"; -import { is_nullish } from "./types"; -import type { VoidPtr } from "./types/emscripten"; -import { getController, startDiagnosticServer } from "./diagnostic_server/browser/controller"; -import * as memory from "./memory"; +} from "../types"; +import { is_nullish } from "../types"; +import type { VoidPtr } from "../types/emscripten"; +import { getController, startDiagnosticServer } from "./browser/controller"; +import * as memory from "../memory"; const sizeOfInt32 = 4; diff --git a/src/mono/wasm/runtime/diagnostic_server/server_pthread/event_pipe.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/event_pipe.ts similarity index 100% rename from src/mono/wasm/runtime/diagnostic_server/server_pthread/event_pipe.ts rename to src/mono/wasm/runtime/diagnostics/server_pthread/event_pipe.ts diff --git a/src/mono/wasm/runtime/diagnostic_server/server_pthread/index.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts similarity index 100% rename from src/mono/wasm/runtime/diagnostic_server/server_pthread/index.ts rename to src/mono/wasm/runtime/diagnostics/server_pthread/index.ts diff --git a/src/mono/wasm/runtime/diagnostic_server/server_pthread/mock-remote.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/mock-remote.ts similarity index 100% rename from src/mono/wasm/runtime/diagnostic_server/server_pthread/mock-remote.ts rename to src/mono/wasm/runtime/diagnostics/server_pthread/mock-remote.ts diff --git a/src/mono/wasm/runtime/diagnostic_server/server_pthread/promise-controller.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/promise-controller.ts similarity index 100% rename from src/mono/wasm/runtime/diagnostic_server/server_pthread/promise-controller.ts rename to src/mono/wasm/runtime/diagnostics/server_pthread/promise-controller.ts diff --git a/src/mono/wasm/runtime/diagnostic_server/server_pthread/tsconfig.json b/src/mono/wasm/runtime/diagnostics/server_pthread/tsconfig.json similarity index 100% rename from src/mono/wasm/runtime/diagnostic_server/server_pthread/tsconfig.json rename to src/mono/wasm/runtime/diagnostics/server_pthread/tsconfig.json diff --git a/src/mono/wasm/runtime/diagnostic_server/shared/controller-commands.ts b/src/mono/wasm/runtime/diagnostics/shared/controller-commands.ts similarity index 100% rename from src/mono/wasm/runtime/diagnostic_server/shared/controller-commands.ts rename to src/mono/wasm/runtime/diagnostics/shared/controller-commands.ts diff --git a/src/mono/wasm/runtime/diagnostic_server/shared/tsconfig.json b/src/mono/wasm/runtime/diagnostics/shared/tsconfig.json similarity index 100% rename from src/mono/wasm/runtime/diagnostic_server/shared/tsconfig.json rename to src/mono/wasm/runtime/diagnostics/shared/tsconfig.json diff --git a/src/mono/wasm/runtime/diagnostic_server/shared/types.ts b/src/mono/wasm/runtime/diagnostics/shared/types.ts similarity index 100% rename from src/mono/wasm/runtime/diagnostic_server/shared/types.ts rename to src/mono/wasm/runtime/diagnostics/shared/types.ts diff --git a/src/mono/wasm/runtime/exports.ts b/src/mono/wasm/runtime/exports.ts index f058f363d1c535..c0819d0c741937 100644 --- a/src/mono/wasm/runtime/exports.ts +++ b/src/mono/wasm/runtime/exports.ts @@ -58,7 +58,7 @@ import { } from "./diagnostics"; import { mono_wasm_diagnostic_server_on_server_thread_created, -} from "./diagnostic_server/server_pthread"; +} from "./diagnostics/server_pthread"; import { mono_wasm_typed_array_copy_to_ref, mono_wasm_typed_array_from_ref, mono_wasm_typed_array_copy_from_ref, mono_wasm_load_bytes_into_heap } from "./buffers"; import { mono_wasm_release_cs_owned_object } from "./gc-handles"; import cwraps from "./cwraps"; From e8fd671cec73bccced21aeb3163c9c3e29fe90a0 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Tue, 5 Jul 2022 16:24:30 -0400 Subject: [PATCH 32/84] Refactor; remove dead code; rationalize controller the flow is now: main -{creates pthread}-> server . server creates event listener . <-{sends diagnostic MessagePort}- . main creates event listener . . -{posts "start" message}-> . . begins server loop after the server loop is running, the main thread will get a "resume_startup" message once the diagnostic server receives the right command from the websocket. next TODO: the runtime needs to send a "attach to runtime" message which will signal the server that it can attach to the runtime (in native) and start calling EP session creation functions. --- .../runtime/diagnostics/browser/controller.ts | 92 +++++------- src/mono/wasm/runtime/diagnostics/index.ts | 127 +--------------- .../wasm/runtime/diagnostics/mock/index.ts | 136 +++++++++++++++++ .../runtime/diagnostics/mock/tsconfig.json | 3 + .../diagnostics/server_pthread/event_pipe.ts | 133 ++--------------- .../diagnostics/server_pthread/index.ts | 78 +++++----- .../diagnostics/server_pthread/mock-remote.ts | 140 +----------------- .../server_pthread/promise-controller.ts | 15 -- .../diagnostics/session-options-builder.ts | 122 +++++++++++++++ .../diagnostics/shared/controller-commands.ts | 34 +++-- .../wasm/runtime/diagnostics/shared/types.ts | 2 - src/mono/wasm/runtime/polyfills.ts | 51 +++++-- src/mono/wasm/runtime/promise-utils.ts | 25 ++++ .../wasm/runtime/pthreads/browser/index.ts | 11 +- .../wasm/runtime/pthreads/worker/index.ts | 4 +- src/mono/wasm/runtime/types.ts | 18 +-- 16 files changed, 460 insertions(+), 531 deletions(-) create mode 100644 src/mono/wasm/runtime/diagnostics/mock/index.ts create mode 100644 src/mono/wasm/runtime/diagnostics/mock/tsconfig.json delete mode 100644 src/mono/wasm/runtime/diagnostics/server_pthread/promise-controller.ts create mode 100644 src/mono/wasm/runtime/diagnostics/session-options-builder.ts create mode 100644 src/mono/wasm/runtime/promise-utils.ts diff --git a/src/mono/wasm/runtime/diagnostics/browser/controller.ts b/src/mono/wasm/runtime/diagnostics/browser/controller.ts index 6afce0527cce7e..696d2429847875 100644 --- a/src/mono/wasm/runtime/diagnostics/browser/controller.ts +++ b/src/mono/wasm/runtime/diagnostics/browser/controller.ts @@ -1,69 +1,55 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -import type { EventPipeSessionDiagnosticServerID } from "../../types"; import cwraps from "../../cwraps"; import { withStackAlloc, getI32 } from "../../memory"; +import { PromiseController } from "../../promise-utils"; import { Thread, waitForThread } from "../../pthreads/browser"; - -// interface ServerReadyResult { -// sessions?: (EventPipeSessionOptions & EventPipeSessionIPCOptions)[]; // provider configs -// } - -// interface ServerConfigureResult { -// serverStarted: boolean; -// serverReady?: Promise; -// } - -// async function configureServer(options: DiagnosticOptions): Promise { -// if (options.server !== undefined && options.server) { -// // TODO start the server -// let serverReady: Promise; -// if (options.server == "wait") { -// //TODO: make a promise to wait for the connection -// serverReady = Promise.resolve({}); -// } else { -// // server is ready now, no need to wait -// serverReady = Promise.resolve({}); -// } -// // TODO: start the server and wait for a connection -// return { serverStarted: false, serverReady: serverReady }; -// } else -// return { serverStarted: false }; -// } - -// function postIPCStreamingSessionStarted(/*diagnosticSessionID: EventPipeSessionDiagnosticServerID, sessionID: EventPipeSessionIDImpl*/): void { -// // TODO: For IPC streaming sessions this is the place to send back an acknowledgement with the session ID -// } +import { makeDiagnosticServerControlCommand } from "../shared/controller-commands"; +import { isDiagnosticMessage } from "../shared/types"; /// An object that can be used to control the diagnostic server. export interface ServerController { - wait_for_resume(): Promise<{ sessions: EventPipeSessionDiagnosticServerID[] }>; - post_diagnostic_server_attach_to_runtime(): void; - // configureServer(options: DiagnosticOptions): Promise; - // postIPCStreamingSessionStarted(diagnosticSessionID: EventPipeSessionDiagnosticServerID, sessionID: EventPipeSessionIDImpl): void; + waitForStartupResume(): Promise; + postServerAttachToRuntime(): void; } class ServerControllerImpl implements ServerController { - constructor(private server: Thread) { } - async wait_for_resume(): Promise<{ sessions: EventPipeSessionDiagnosticServerID[] }> { - console.debug("waiting for the diagnostic server to allow us to resume"); - const promise = new Promise((resolve, /*reject*/) => { - setTimeout(() => { resolve(); }, 1000); - }); - await promise; - // let req = this.server.allocateRequest({ type: "diagnostic_server", cmd: "wait_for_resume" }); - // let respone = await this.server.sendAndWait(req); - // if (respone.type !== "diagnostic_server" || respone.cmd !== "wait_for_resume_response") { - // throw new Error("unexpected response"); - // } - return { sessions: [] }; + private readonly startupResumePromise: PromiseController = new PromiseController(); + constructor(private server: Thread) { + server.port.addEventListener("message", this.onServerReply.bind(this)); + } + start(): void { + console.debug("signaling the diagnostic server to start"); + this.server.postMessageToWorker(makeDiagnosticServerControlCommand("start")); } - post_diagnostic_server_attach_to_runtime(): void { + stop(): void { + console.debug("signaling the diagnostic server to stop"); + this.server.postMessageToWorker(makeDiagnosticServerControlCommand("stop")); + } + async waitForStartupResume(): Promise { + await this.startupResumePromise.promise; + } + postServerAttachToRuntime(): void { console.debug("signal the diagnostic server to attach to the runtime"); + this.server.postMessageToWorker(makeDiagnosticServerControlCommand("attach_to_runtime")); } -} + onServerReply(event: MessageEvent): void { + const d = event.data; + if (isDiagnosticMessage(d)) { + switch (d.cmd) { + case "startup_resume": + console.debug("diagnostic server startup resume"); + this.startupResumePromise.resolve(); + break; + default: + console.warn("Unknown control command: ", d); + break; + } + } + } +} let serverController: ServerController | null = null; @@ -91,7 +77,9 @@ export async function startDiagnosticServer(websocket_url: string): Promise - -/// The configuration for an individual provider. Each provider configuration has the name of the provider, -/// the level of events to collect, and a string containing a 32-bit hexadecimal mask (without an "0x" prefix) of -/// the "keywords" to filter a subset of the events. The keyword mask may be the number 0 or "" to skips the filtering. -/// See https://docs.microsoft.com/en-us/dotnet/core/diagnostics/well-known-event-providers for a list of known providers. -/// Additional providers may be added by applications or libraries that implement an EventSource subclass. -/// See https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.tracing.eventsource?view=net-6.0 -/// -/// Some providers also have an "args" string in an arbitrary format. For example the EventSource providers that -/// include EventCounters have a "EventCounterIntervalSec=NNN" argument that specified how often the counters of -/// the event source should be polled. -export interface ProviderConfiguration extends UnnamedProviderConfiguration { - name: string; -} - -const runtimeProviderName = "Microsoft-Windows-DotNETRuntime"; -const runtimePrivateProviderName = "Microsoft-Windows-DotNETRuntimePrivate"; -const sampleProfilerProviderName = "Microsoft-DotNETCore-SampleProfiler"; - -const runtimeProviderDefault: ProviderConfiguration = { - name: runtimeProviderName, - keyword_mask: "4c14fccbd", - level: eventLevel.Verbose, -}; - -const runtimePrivateProviderDefault: ProviderConfiguration = { - name: runtimePrivateProviderName, - keyword_mask: "4002000b", - level: eventLevel.Verbose, -}; - -const sampleProfilerProviderDefault: ProviderConfiguration = { - name: sampleProfilerProviderName, - keyword_mask: "0", - level: eventLevel.Verbose, -}; - -/// A helper class to create EventPipeSessionOptions -export class SessionOptionsBuilder { - private _rundown?: boolean; - private _providers: ProviderConfiguration[]; - /// Create an empty builder. Prefer to use SesssionOptionsBuilder.Empty - constructor() { - this._providers = []; - } - /// Gets a builder with no providers. - static get Empty(): SessionOptionsBuilder { return new SessionOptionsBuilder(); } - /// Gets a builder with default providers and rundown events enabled. - /// See https://docs.microsoft.com/en-us/dotnet/core/diagnostics/eventpipe#trace-using-environment-variables - static get DefaultProviders(): SessionOptionsBuilder { - return this.Empty.addRuntimeProvider().addRuntimePrivateProvider().addSampleProfilerProvider(); - } - /// Change whether to collect rundown events. - /// Certain providers may need rundown events to be collected in order to provide useful diagnostic information. - setRundownEnabled(enabled: boolean): SessionOptionsBuilder { - this._rundown = enabled; - return this; - } - /// Add a provider configuration to the builder. - addProvider(provider: ProviderConfiguration): SessionOptionsBuilder { - this._providers.push(provider); - return this; - } - /// Add the Microsoft-Windows-DotNETRuntime provider. Use override options to change the event level or keyword mask. - /// The default is { keyword_mask: "4c14fccbd", level: eventLevel.Verbose } - addRuntimeProvider(overrideOptions?: UnnamedProviderConfiguration): SessionOptionsBuilder { - const options = { ...runtimeProviderDefault, ...overrideOptions }; - this._providers.push(options); - return this; - } - /// Add the Microsoft-Windows-DotNETRuntimePrivate provider. Use override options to change the event level or keyword mask. - /// The default is { keyword_mask: "4002000b", level: eventLevel.Verbose} - addRuntimePrivateProvider(overrideOptions?: UnnamedProviderConfiguration): SessionOptionsBuilder { - const options = { ...runtimePrivateProviderDefault, ...overrideOptions }; - this._providers.push(options); - return this; - } - /// Add the Microsoft-DotNETCore-SampleProfiler. Use override options to change the event level or keyword mask. - // The default is { keyword_mask: 0, level: eventLevel.Verbose } - addSampleProfilerProvider(overrideOptions?: UnnamedProviderConfiguration): SessionOptionsBuilder { - const options = { ...sampleProfilerProviderDefault, ...overrideOptions }; - this._providers.push(options); - return this; - } - /// Create an EventPipeSessionOptions from the builder. - build(): EventPipeSessionOptions { - const providers = this._providers.map(p => { - const name = p.name; - const keyword_mask = "" + (p?.keyword_mask ?? ""); - const level = p?.level ?? eventLevel.Verbose; - const args = p?.args ?? ""; - const maybeArgs = args != "" ? `:${args}` : ""; - return `${name}:${keyword_mask}:${level}${maybeArgs}`; - }); - return { - collectRundownEvents: this._rundown, - providers: providers.join(",") - }; - } -} - // a conter for the number of sessions created let totalSessions = 0; @@ -324,7 +213,7 @@ export async function mono_wasm_init_diagnostics(options: DiagnosticOptions): Pr if (controller) { if (suspend) { console.debug("waiting for the diagnostic server to resume us"); - const response = await controller.wait_for_resume(); + const response = await controller.waitForStartupResume(); console.debug("diagnostic server resumed us", response); } } @@ -335,7 +224,7 @@ export async function mono_wasm_init_diagnostics(options: DiagnosticOptions): Pr export function mono_wasm_diagnostic_server_attach(): void { const controller = getController(); - controller.post_diagnostic_server_attach_to_runtime(); + controller.postServerAttachToRuntime(); } export default diagnostics; diff --git a/src/mono/wasm/runtime/diagnostics/mock/index.ts b/src/mono/wasm/runtime/diagnostics/mock/index.ts new file mode 100644 index 00000000000000..1a68a7e5df2087 --- /dev/null +++ b/src/mono/wasm/runtime/diagnostics/mock/index.ts @@ -0,0 +1,136 @@ + +interface MockRemoteEventMap { + "open": Event; + "close": CloseEvent; + "message": MessageEvent; + "error": Event; +} + +export interface MockRemoteSocket extends EventTarget { + addEventListener(event: keyof MockRemoteEventMap, listener: (event: MockRemoteEventMap[keyof MockRemoteEventMap]) => void): void; + removeEventListener(event: keyof MockRemoteEventMap, listener: (event: MockRemoteEventMap[keyof MockRemoteEventMap]) => void): void; + send(data: string | ArrayBuffer | Uint8Array | Blob | DataView): void; + close(): void; +} + +export interface Mock { + open(): MockRemoteSocket; + run(): Promise; +} + +class MockScriptEngineSocketImpl implements MockRemoteSocket { + constructor(private readonly engine: MockScriptEngineImpl) { } + send(data: string | ArrayBuffer): void { + if (this.engine.trace) { + console.debug(`mock ${this.engine.ident} client sent: `, data); + } + let event: MessageEvent | null = null; + if (typeof data === "string") { + event = new MessageEvent("message", { data }); + } else { + const message = new ArrayBuffer(data.byteLength); + const messageView = new Uint8Array(message); + const dataView = new Uint8Array(data); + messageView.set(dataView); + event = new MessageEvent("message", { data: message }); + } + this.engine.mockReplyEventTarget.dispatchEvent(event); + } + addEventListener(event: keyof MockRemoteEventMap, listener: (event: Event | CloseEvent | MessageEvent) => void): void { + if (this.engine.trace) { + console.debug(`mock ${this.engine.ident} client added listener for ${event}`); + } + this.engine.eventTarget.addEventListener(event, listener); + } + removeEventListener(event: keyof MockRemoteEventMap, listener: (event: Event | CloseEvent | MessageEvent) => void): void { + if (this.engine.trace) { + console.debug(`mock ${this.engine.ident} client removed listener for ${event}`); + } + this.engine.eventTarget.removeEventListener(event, listener); + } + close(): void { + if (this.engine.trace) { + console.debug(`mock ${this.engine.ident} client closed`); + } + this.engine.mockReplyEventTarget.dispatchEvent(new CloseEvent("close")); + } + dispatchEvent(): boolean { + throw new Error("don't call dispatchEvent on a MockRemoteSocket"); + } +} + +class MockScriptEngineImpl implements MockScriptEngine { + readonly socket: MockRemoteSocket; + // eventTarget that the MockReplySocket will dispatch to + readonly eventTarget: EventTarget = new EventTarget(); + // eventTarget that the MockReplySocket with send() to + readonly mockReplyEventTarget: EventTarget = new EventTarget(); + constructor(readonly trace: boolean, readonly ident: number) { + this.socket = new MockScriptEngineSocketImpl(this); + } + + reply(data: string | ArrayBuffer) { + if (this.trace) { + console.debug(`mock ${this.ident} reply:`, data); + } + this.eventTarget.dispatchEvent(new MessageEvent("message", { data })); + } + + async waitForSend(filter: (data: string | ArrayBuffer) => boolean): Promise { + const trace = this.trace; + if (trace) { + console.debug(`mock ${this.ident} waitForSend`); + } + const event = await new Promise>((resolve) => { + this.mockReplyEventTarget.addEventListener("message", (event) => { + if (trace) { + console.debug(`mock ${this.ident} waitForSend got:`, event); + } + resolve(event as MessageEvent); + }, { once: true }); + }); + if (!filter(event.data)) { + throw new Error("Unexpected data"); + } + return; + } +} + +interface MockOptions { + readonly trace: boolean; +} + +class MockImpl { + openCount: number; + engines: MockScriptEngineImpl[]; + readonly trace: boolean; + constructor(public readonly script: ((engine: MockScriptEngine) => Promise)[], options?: MockOptions) { + this.openCount = 0; + this.trace = options?.trace ?? false; + const count = script.length; + this.engines = new Array(count); + for (let i = 0; i < count; ++i) { + this.engines[i] = new MockScriptEngineImpl(this.trace, i); + } + } + open(): MockRemoteSocket { + const i = this.openCount++; + if (this.trace) { + console.debug(`mock ${i} open`); + } + return this.engines[i].socket; + } + + async run(): Promise { + await Promise.all(this.script.map((script, i) => script(this.engines[i]))); + } +} + +export interface MockScriptEngine { + waitForSend(filter: (data: string | ArrayBuffer) => boolean): Promise; + waitForSend(filter: (data: string | ArrayBuffer) => boolean, extract: (data: string | ArrayBuffer) => T): Promise; + reply(data: string | ArrayBuffer): void; +} +export function mock(script: ((engine: MockScriptEngine) => Promise)[], options?: MockOptions): Mock { + return new MockImpl(script, options); +} diff --git a/src/mono/wasm/runtime/diagnostics/mock/tsconfig.json b/src/mono/wasm/runtime/diagnostics/mock/tsconfig.json new file mode 100644 index 00000000000000..ea592d1a092a80 --- /dev/null +++ b/src/mono/wasm/runtime/diagnostics/mock/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../tsconfig.worker.json" +} diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/event_pipe.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/event_pipe.ts index 15045949422e38..4d308d0e494cf9 100644 --- a/src/mono/wasm/runtime/diagnostics/server_pthread/event_pipe.ts +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/event_pipe.ts @@ -2,40 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. import type { - EventPipeSessionIDImpl, EventPipeSessionDiagnosticServerID, + EventPipeSessionIDImpl, } from "../shared/types"; -import { DiagnosticMessage } from "../shared/types"; -import type { - DiagnosticServerControlCommand, - /*DiagnosticServerControlCommandStart, DiagnosticServerControlCommandSetSessionID*/ -} from "../shared/controller-commands"; - -export interface DiagnosticServer { - stop(): void; -} - -/// Everything the diagnostic server knows about a connection. -/// The connection has a server ID and a websocket. If it's an eventpipe session, it will also have an eventpipe ID assigned when the runtime starts an EventPipe session. - -interface DiagnosticServerConnection { - readonly type: string; - get diagnosticSessionID(): EventPipeSessionDiagnosticServerID; - get socket(): WebSocket; - addListeners(): void; - postMessage(message: string): boolean; -} - -interface DiagnosticServerEventPipeConnection extends DiagnosticServerConnection { - type: "eventpipe"; - get sessionID(): EventPipeSessionIDImpl | null; - setSessionID(sessionID: EventPipeSessionIDImpl): void; -} - -interface ServerSessionManager { - createSession(socket: WebSocket): DiagnosticServerConnection; - assignEventPipeSessionID(diagnosticServerID: EventPipeSessionDiagnosticServerID, sessionID: EventPipeSessionIDImpl): void; - getSession(sessionID: EventPipeSessionDiagnosticServerID): DiagnosticServerEventPipeConnection | undefined; -} enum ListenerState { AwaitingCommand, @@ -49,33 +17,23 @@ function assertNever(x: never): never { throw new Error("Unexpected object: " + x); } -class EventPipeServerConnection implements DiagnosticServerEventPipeConnection { +export class EventPipeServerConnection { readonly type = "eventpipe"; - private _diagnostic_server_id: EventPipeSessionDiagnosticServerID; private _sessionID: EventPipeSessionIDImpl | null = null; - private readonly _socket: WebSocket; private _state: ListenerState; - constructor(socket: WebSocket, diagnostic_server_id: EventPipeSessionDiagnosticServerID) { - this._socket = socket; - this._diagnostic_server_id = diagnostic_server_id; + constructor(readonly socket: WebSocket) { this._state = ListenerState.AwaitingCommand; } - get diagnosticSessionID(): EventPipeSessionDiagnosticServerID { - return this._diagnostic_server_id; - } - get socket(): WebSocket { - return this._socket; - } get sessionID(): EventPipeSessionIDImpl | null { return this._sessionID; } - setSessionID(sessionID: EventPipeSessionIDImpl) { + setSessionID(sessionID: EventPipeSessionIDImpl): void { if (this._sessionID !== null) throw new Error("Session ID already set"); this._sessionID = sessionID; } - public close() { + close(): void { switch (this._state) { case ListenerState.Error: return; @@ -83,21 +41,21 @@ class EventPipeServerConnection implements DiagnosticServerEventPipeConnection { return; default: this._state = ListenerState.Closed; - this._socket.close(); + this.socket.close(); return; } } - public postMessage(message: string): boolean { + postMessage(message: string): boolean { switch (this._state) { case ListenerState.AwaitingCommand: throw new Error("Unexpected postMessage: " + message); case ListenerState.DispatchedCommand: this._state = ListenerState.SendingTrailingData; - this._socket.send(message); + this.socket.send(message); return true; case ListenerState.SendingTrailingData: - this._socket.send(message); + this.socket.send(message); return true; case ListenerState.Closed: // ignore @@ -147,75 +105,10 @@ class EventPipeServerConnection implements DiagnosticServerEventPipeConnection { this._state = ListenerState.Error; } - public addListeners() { - this._socket.addEventListener("message", this._onMessage.bind(this)); - this._socket.addEventListener("close", this._onClose.bind(this)); - this._socket.addEventListener("error", this._onError.bind(this)); + addListeners(): void { + this.socket.addEventListener("message", this._onMessage.bind(this)); + this.socket.addEventListener("close", this._onClose.bind(this)); + this.socket.addEventListener("error", this._onError.bind(this)); } } -class SessionManager implements ServerSessionManager { - private readonly sessions: Map = new Map(); - private _nextSessionID: EventPipeSessionDiagnosticServerID = 1; - createSession(socket: WebSocket): DiagnosticServerConnection { - const id = this._nextSessionID; - this._nextSessionID++; - const session = new EventPipeServerConnection(socket, id); - this.sessions.set(id, session); - return session; - } - assignEventPipeSessionID(diagnosticServerID: number, sessionID: number): void { - const session = this.sessions.get(diagnosticServerID); - if (session) { - session.setSessionID(sessionID); - } - } - getSession(sessionID: number): DiagnosticServerEventPipeConnection | undefined { - return this.sessions.get(sessionID); - } -} - -function advertiseSession(session: DiagnosticServerConnection): void { - // TODO: send ADVR message to client and wait for response - console.debug("TODO: advertiseSession"); - session.addListeners(); - session.socket.send("ADVR"); // FIXME: this is a dummy response -} -function startServer(url: string): SessionManager { - - const sessionManager = new SessionManager(); - const webSocket = new WebSocket(url); - webSocket.addEventListener("open", () => { - console.log("WebSocket opened"); - const session = sessionManager.createSession(webSocket); - console.log("session created"); - advertiseSession(session); - }); - console.debug("started server"); - // TODO: connect again and advertise for the next command - return sessionManager; -} - -let sessionManager: SessionManager | null = null; - -export function controlCommandReceived(server: DiagnosticServer, msg: DiagnosticMessage): void { - const cmd = msg as DiagnosticServerControlCommand; - switch (cmd.cmd) { - case "start": - if (sessionManager !== null) - throw new Error("server already started"); - sessionManager = startServer(cmd.url); - break; - case "stop": - server.stop(); - break; - case "set_session_id": - if (sessionManager === null) - throw new Error("server not started"); - sessionManager.assignEventPipeSessionID(cmd.diagnostic_server_id, cmd.session_id); - break; - default: - console.warn("Unknown control command: ", cmd); - break; - } -} diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts index d94a207bcb89c2..f955774eee4188 100644 --- a/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts @@ -5,59 +5,51 @@ import { pthread_self } from "../../pthreads/worker"; import { Module } from "../../imports"; -import { controlCommandReceived } from "./event_pipe"; import { isDiagnosticMessage } from "../shared/types"; import { CharPtr } from "../../types/emscripten"; -import { DiagnosticServer } from "./event_pipe"; +import type { + DiagnosticServerControlCommand, + /*DiagnosticServerControlCommandStart, DiagnosticServerControlCommandSetSessionID*/ +} from "../shared/controller-commands"; import { mockScript } from "./mock-remote"; -import PromiseController from "./promise-controller"; - -//function delay(ms: number): Promise { -// return new Promise(resolve => setTimeout(resolve, ms)); -//} +import { PromiseController } from "../../promise-utils"; function addOneShotMessageEventListener(src: EventTarget): Promise> { return new Promise((resolve) => { - const listener: (event: Event) => void = ((event: MessageEvent) => { - src.removeEventListener("message", listener); - resolve(event); - }) as (event: Event) => void; - src.addEventListener("message", listener); + const listener = (event: Event) => { resolve(event as MessageEvent); }; + src.addEventListener("message", listener, { once: true }); }); } +export interface DiagnosticServer { + stop(): void; +} + class DiagnosticServerImpl implements DiagnosticServer { readonly websocketUrl: string; readonly ws: WebSocket | null; constructor(websocketUrl: string) { this.websocketUrl = websocketUrl; this.ws = null; // new WebSocket(this.websocketUrl); + pthread_self.addEventListenerFromBrowser(this.onMessageFromMainThread.bind(this)); } - start(): void { - console.log(`starting diagnostic server with url: ${this.websocketUrl}`); - // XXX FIXME: we started before the runtime is ready, so we don't get a port because it gets created on attach. - - if (pthread_self) { - pthread_self.addEventListenerFromBrowser(this.onMessage.bind(this)); - pthread_self.postMessageToBrowser({ - "type": "diagnostic_server", - "cmd": "started", - "thread_id": pthread_self.pthread_id - }); - } - } - + private startRequestedController = new PromiseController(); private stopRequested = false; private stopRequestedController = new PromiseController(); + start(): void { + console.log(`starting diagnostic server with url: ${this.websocketUrl}`); + this.startRequestedController.resolve(); + } stop(): void { this.stopRequested = true; this.stopRequestedController.resolve(); } async serverLoop(this: DiagnosticServerImpl): Promise { + await this.startRequestedController.promise; while (!this.stopRequested) { const firstPromise: Promise<["first", string] | ["second", undefined]> = this.advertiseAndWaitForClient().then((r) => ["first", r]); const secondPromise: Promise<["first", string] | ["second", undefined]> = this.stopRequestedController.promise.then(() => ["second", undefined]); @@ -80,24 +72,25 @@ class DiagnosticServerImpl implements DiagnosticServer { return message.data.toString(); } - // async eventPipeSessionLoop(): Promise { - // await runtimeStarted(); - // const eventPipeFlushThread = await enableEventPipeSessionAndSignalResume(); - // while (!this.stopRequested) { - // const outcome = await oneOfStoppedOrMessageReceived(eventPipeFlushThread); - // if (outcome === "stopped") { - // break; - // } else { - // sendEPBufferToWebSocket(outcome); - // } - // } - // await closeWebSocket(); - // } - - onMessage(this: DiagnosticServerImpl, event: MessageEvent): void { + onMessageFromMainThread(this: DiagnosticServerImpl, event: MessageEvent): void { const d = event.data; if (d && isDiagnosticMessage(d)) { - controlCommandReceived(this, d); + this.controlCommandReceived(d as DiagnosticServerControlCommand); + } + } + + /// dispatch commands received from the main thread + controlCommandReceived(cmd: DiagnosticServerControlCommand): void { + switch (cmd.cmd) { + case "start": + this.start(); + break; + case "stop": + this.stop(); + break; + default: + console.warn("Unknown control command: ", cmd); + break; } } } @@ -108,7 +101,6 @@ export function mono_wasm_diagnostic_server_on_server_thread_created(websocketUr const websocketUrl = Module.UTF8ToString(websocketUrlPtr); console.debug(`mono_wasm_diagnostic_server_on_server_thread_created, url ${websocketUrl}`); const server = new DiagnosticServerImpl(websocketUrl); - server.start(); queueMicrotask(() => { mockScript.run(); }); diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/mock-remote.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/mock-remote.ts index 09398dae4faaa9..ad932292e4768d 100644 --- a/src/mono/wasm/runtime/diagnostics/server_pthread/mock-remote.ts +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/mock-remote.ts @@ -1,141 +1,6 @@ -import PromiseController from "./promise-controller"; - -interface MockRemoteEventMap { - "open": Event; - "close": CloseEvent; - "message": MessageEvent; - "error": Event; -} - -export interface MockRemoteSocket extends EventTarget { - addEventListener(event: keyof MockRemoteEventMap, listener: (event: MockRemoteEventMap[keyof MockRemoteEventMap]) => void): void; - removeEventListener(event: keyof MockRemoteEventMap, listener: (event: MockRemoteEventMap[keyof MockRemoteEventMap]) => void): void; - send(data: string | ArrayBuffer | Uint8Array | Blob | DataView): void; - close(): void; -} - -export interface Mock { - open(): MockRemoteSocket; - run(): Promise; -} - -class MockScriptEngineSocketImpl implements MockRemoteSocket { - constructor(private readonly engine: MockScriptEngineImpl) { } - send(data: string | ArrayBuffer): void { - if (this.engine.trace) { - console.debug(`mock ${this.engine.ident} client sent: `, data); - } - let event: MessageEvent | null = null; - if (typeof data === "string") { - event = new MessageEvent("message", { data }); - } else { - const message = new ArrayBuffer(data.byteLength); - const messageView = new Uint8Array(message); - const dataView = new Uint8Array(data); - messageView.set(dataView); - event = new MessageEvent("message", { data: message }); - } - this.engine.mockReplyEventTarget.dispatchEvent(event); - } - addEventListener(event: keyof MockRemoteEventMap, listener: (event: Event | CloseEvent | MessageEvent) => void): void { - if (this.engine.trace) { - console.debug(`mock ${this.engine.ident} client added listener for ${event}`); - } - this.engine.eventTarget.addEventListener(event, listener); - } - removeEventListener(event: keyof MockRemoteEventMap, listener: (event: Event | CloseEvent | MessageEvent) => void): void { - if (this.engine.trace) { - console.debug(`mock ${this.engine.ident} client removed listener for ${event}`); - } - this.engine.eventTarget.removeEventListener(event, listener); - } - close(): void { - if (this.engine.trace) { - console.debug(`mock ${this.engine.ident} client closed`); - } - this.engine.mockReplyEventTarget.dispatchEvent(new CloseEvent("close")); - } - dispatchEvent(): boolean { - throw new Error("don't call dispatchEvent on a MockRemoteSocket"); - } -} - -class MockScriptEngineImpl implements MockScriptEngine { - readonly socket: MockRemoteSocket; - // eventTarget that the MockReplySocket will dispatch to - readonly eventTarget: EventTarget = new EventTarget(); - // eventTarget that the MockReplySocket with send() to - readonly mockReplyEventTarget: EventTarget = new EventTarget(); - constructor(readonly trace: boolean, readonly ident: number) { - this.socket = new MockScriptEngineSocketImpl(this); - } - - reply(data: string | ArrayBuffer) { - if (this.trace) { - console.debug(`mock ${this.ident} reply:`, data); - } - this.eventTarget.dispatchEvent(new MessageEvent("message", { data })); - } - - async waitForSend(filter: (data: string | ArrayBuffer) => boolean): Promise { - const trace = this.trace; - if (trace) { - console.debug(`mock ${this.ident} waitForSend`); - } - const event = await new Promise>((resolve) => { - this.mockReplyEventTarget.addEventListener("message", (event) => { - if (trace) { - console.debug(`mock ${this.ident} waitForSend got:`, event); - } - resolve(event as MessageEvent); - }, { once: true }); - }); - if (!filter(event.data)) { - throw new Error("Unexpected data"); - } - return; - } -} - -interface MockOptions { - readonly trace: boolean; -} - -class MockImpl { - openCount: number; - engines: MockScriptEngineImpl[]; - readonly trace: boolean; - constructor(public readonly script: ((engine: MockScriptEngine) => Promise)[], options?: MockOptions) { - this.openCount = 0; - this.trace = options?.trace ?? false; - const count = script.length; - this.engines = new Array(count); - for (let i = 0; i < count; ++i) { - this.engines[i] = new MockScriptEngineImpl(this.trace, i); - } - } - open(): MockRemoteSocket { - const i = this.openCount++; - if (this.trace) { - console.debug(`mock ${i} open`); - } - return this.engines[i].socket; - } - - async run(): Promise { - await Promise.all(this.script.map((script, i) => script(this.engines[i]))); - } -} - -export interface MockScriptEngine { - waitForSend(filter: (data: string | ArrayBuffer) => boolean): Promise; - waitForSend(filter: (data: string | ArrayBuffer) => boolean, extract: (data: string | ArrayBuffer) => T): Promise; - reply(data: string | ArrayBuffer): void; -} -export function mock(script: ((engine: MockScriptEngine) => Promise)[], options?: MockOptions): Mock { - return new MockImpl(script, options); -} +import { mock, MockScriptEngine } from "../mock"; +import { PromiseController } from "../../promise-utils"; function expectAdvertise(data: string | ArrayBuffer) { return data === "ADVR"; } @@ -160,4 +25,5 @@ const script: ((engine: MockScriptEngine) => Promise)[] = [ } ]; +/// a mock script that simulates the initial part of the diagnostic server protocol export const mockScript = mock(script, { trace: true }); diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/promise-controller.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/promise-controller.ts deleted file mode 100644 index 5e6fe7ef9e5b8d..00000000000000 --- a/src/mono/wasm/runtime/diagnostics/server_pthread/promise-controller.ts +++ /dev/null @@ -1,15 +0,0 @@ -export default class PromiseController { - readonly promise: Promise; - readonly resolve: (value: T | PromiseLike) => void; - readonly reject: (reason: any) => void; - constructor() { - let rs: (value: T | PromiseLike) => void = undefined as any; - let rj: (reason: any) => void = undefined as any; - this.promise = new Promise((resolve, reject) => { - rs = resolve; - rj = reject; - }); - this.resolve = rs; - this.reject = rj; - } -} diff --git a/src/mono/wasm/runtime/diagnostics/session-options-builder.ts b/src/mono/wasm/runtime/diagnostics/session-options-builder.ts new file mode 100644 index 00000000000000..5092bd57416f82 --- /dev/null +++ b/src/mono/wasm/runtime/diagnostics/session-options-builder.ts @@ -0,0 +1,122 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import { EventPipeSessionOptions } from "../types"; + +export const eventLevel = { + LogAlways: 0, + Critical: 1, + Error: 2, + Warning: 3, + Informational: 4, + Verbose: 5, +} as const; + +export type EventLevel = typeof eventLevel; + +type UnnamedProviderConfiguration = Partial<{ + keyword_mask: string | 0; + level: number; + args: string; +}> + +/// The configuration for an individual provider. Each provider configuration has the name of the provider, +/// the level of events to collect, and a string containing a 32-bit hexadecimal mask (without an "0x" prefix) of +/// the "keywords" to filter a subset of the events. The keyword mask may be the number 0 or "" to skips the filtering. +/// See https://docs.microsoft.com/en-us/dotnet/core/diagnostics/well-known-event-providers for a list of known providers. +/// Additional providers may be added by applications or libraries that implement an EventSource subclass. +/// See https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.tracing.eventsource?view=net-6.0 +/// +/// Some providers also have an "args" string in an arbitrary format. For example the EventSource providers that +/// include EventCounters have a "EventCounterIntervalSec=NNN" argument that specified how often the counters of +/// the event source should be polled. +export interface ProviderConfiguration extends UnnamedProviderConfiguration { + name: string; +} + +const runtimeProviderName = "Microsoft-Windows-DotNETRuntime"; +const runtimePrivateProviderName = "Microsoft-Windows-DotNETRuntimePrivate"; +const sampleProfilerProviderName = "Microsoft-DotNETCore-SampleProfiler"; + +const runtimeProviderDefault: ProviderConfiguration = { + name: runtimeProviderName, + keyword_mask: "4c14fccbd", + level: eventLevel.Verbose, +}; + +const runtimePrivateProviderDefault: ProviderConfiguration = { + name: runtimePrivateProviderName, + keyword_mask: "4002000b", + level: eventLevel.Verbose, +}; + +const sampleProfilerProviderDefault: ProviderConfiguration = { + name: sampleProfilerProviderName, + keyword_mask: "0", + level: eventLevel.Verbose, +}; + +/// A helper class to create EventPipeSessionOptions +export class SessionOptionsBuilder { + private _rundown?: boolean; + private _providers: ProviderConfiguration[]; + /// Create an empty builder. Prefer to use SesssionOptionsBuilder.Empty + constructor() { + this._providers = []; + } + /// Gets a builder with no providers. + static get Empty(): SessionOptionsBuilder { return new SessionOptionsBuilder(); } + /// Gets a builder with default providers and rundown events enabled. + /// See https://docs.microsoft.com/en-us/dotnet/core/diagnostics/eventpipe#trace-using-environment-variables + static get DefaultProviders(): SessionOptionsBuilder { + return this.Empty.addRuntimeProvider().addRuntimePrivateProvider().addSampleProfilerProvider(); + } + /// Change whether to collect rundown events. + /// Certain providers may need rundown events to be collected in order to provide useful diagnostic information. + setRundownEnabled(enabled: boolean): SessionOptionsBuilder { + this._rundown = enabled; + return this; + } + /// Add a provider configuration to the builder. + addProvider(provider: ProviderConfiguration): SessionOptionsBuilder { + this._providers.push(provider); + return this; + } + /// Add the Microsoft-Windows-DotNETRuntime provider. Use override options to change the event level or keyword mask. + /// The default is { keyword_mask: "4c14fccbd", level: eventLevel.Verbose } + addRuntimeProvider(overrideOptions?: UnnamedProviderConfiguration): SessionOptionsBuilder { + const options = { ...runtimeProviderDefault, ...overrideOptions }; + this._providers.push(options); + return this; + } + /// Add the Microsoft-Windows-DotNETRuntimePrivate provider. Use override options to change the event level or keyword mask. + /// The default is { keyword_mask: "4002000b", level: eventLevel.Verbose} + addRuntimePrivateProvider(overrideOptions?: UnnamedProviderConfiguration): SessionOptionsBuilder { + const options = { ...runtimePrivateProviderDefault, ...overrideOptions }; + this._providers.push(options); + return this; + } + /// Add the Microsoft-DotNETCore-SampleProfiler. Use override options to change the event level or keyword mask. + // The default is { keyword_mask: 0, level: eventLevel.Verbose } + addSampleProfilerProvider(overrideOptions?: UnnamedProviderConfiguration): SessionOptionsBuilder { + const options = { ...sampleProfilerProviderDefault, ...overrideOptions }; + this._providers.push(options); + return this; + } + /// Create an EventPipeSessionOptions from the builder. + build(): EventPipeSessionOptions { + const providers = this._providers.map(p => { + const name = p.name; + const keyword_mask = "" + (p?.keyword_mask ?? ""); + const level = p?.level ?? eventLevel.Verbose; + const args = p?.args ?? ""; + const maybeArgs = args != "" ? `:${args}` : ""; + return `${name}:${keyword_mask}:${level}${maybeArgs}`; + }); + return { + collectRundownEvents: this._rundown, + providers: providers.join(",") + }; + } +} + diff --git a/src/mono/wasm/runtime/diagnostics/shared/controller-commands.ts b/src/mono/wasm/runtime/diagnostics/shared/controller-commands.ts index b825eac5ba347b..ac52d07a7fd1e4 100644 --- a/src/mono/wasm/runtime/diagnostics/shared/controller-commands.ts +++ b/src/mono/wasm/runtime/diagnostics/shared/controller-commands.ts @@ -1,26 +1,34 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -import { EventPipeSessionDiagnosticServerID, EventPipeSessionIDImpl, DiagnosticMessage } from "./types"; - +import { DiagnosticMessage } from "./types"; +/// Commands from the main thread to the diagnostic server export type DiagnosticServerControlCommand = - DiagnosticServerControlCommandStart + | DiagnosticServerControlCommandStart | DiagnosticServerControlCommandStop - | DiagnosticServerControlCommandSetSessionID + | DiagnosticServerControlCommandAttachToRuntime ; -export interface DiagnosticServerControlCommandStart extends DiagnosticMessage { - cmd: "start", - url: string, // websocket url to connect to +interface DiagnosticServerControlCommandSpecific extends DiagnosticMessage { + cmd: Cmd; } -export interface DiagnosticServerControlCommandStop extends DiagnosticMessage { - cmd: "stop", +export type DiagnosticServerControlCommandStart = DiagnosticServerControlCommandSpecific<"start">; +export type DiagnosticServerControlCommandStop = DiagnosticServerControlCommandSpecific<"stop">; +export type DiagnosticServerControlCommandAttachToRuntime = DiagnosticServerControlCommandSpecific<"attach_to_runtime">; + +export function makeDiagnosticServerControlCommand(cmd: T): DiagnosticServerControlCommandSpecific { + return { + type: "diagnostic_server", + cmd: cmd, + }; } -export interface DiagnosticServerControlCommandSetSessionID extends DiagnosticMessage { - cmd: "set_session_id"; - diagnostic_server_id: EventPipeSessionDiagnosticServerID; - session_id: EventPipeSessionIDImpl; +export type DiagnosticServerControlReply = + | DiagnosticServerControlReplyStartupResume + ; + +export interface DiagnosticServerControlReplyStartupResume extends DiagnosticMessage { + cmd: "startup_resume", } diff --git a/src/mono/wasm/runtime/diagnostics/shared/types.ts b/src/mono/wasm/runtime/diagnostics/shared/types.ts index ceffa5329ca3e9..b15e23b3891270 100644 --- a/src/mono/wasm/runtime/diagnostics/shared/types.ts +++ b/src/mono/wasm/runtime/diagnostics/shared/types.ts @@ -4,8 +4,6 @@ import type { MonoThreadMessage } from "../../pthreads/shared"; import { isMonoThreadMessage } from "../../pthreads/shared"; -export type { EventPipeSessionDiagnosticServerID } from "../../types"; - export type EventPipeSessionIDImpl = number; export interface DiagnosticMessage extends MonoThreadMessage { diff --git a/src/mono/wasm/runtime/polyfills.ts b/src/mono/wasm/runtime/polyfills.ts index c5b6ed88714271..3b4aa02ec8c576 100644 --- a/src/mono/wasm/runtime/polyfills.ts +++ b/src/mono/wasm/runtime/polyfills.ts @@ -41,20 +41,28 @@ export async function init_polyfills(): Promise { } if (MonoWasmThreads && typeof globalThis.EventTarget === "undefined") { globalThis.EventTarget = class EventTarget { - private listeners = new Map>(); + private subscribers = new Map>(); addEventListener(type: string, listener: EventListenerOrEventListenerObject | null, options?: boolean | AddEventListenerOptions) { if (listener === undefined || listener == null) return; - if (options !== undefined) - throw new Error("FIXME: addEventListener polyfill doesn't implement options"); - if (!this.listeners.has(type)) { - this.listeners.set(type, []); + let oneShot = false; + if (options !== undefined) { + for (const [k, v] of Object.entries(options)) { + if (k === "once") { + oneShot = v ? true : false; + continue; + } + throw new Error(`FIXME: addEventListener polyfill doesn't implement option '${k}'`); + } } - const listeners = this.listeners.get(type); + if (!this.subscribers.has(type)) { + this.subscribers.set(type, []); + } + const listeners = this.subscribers.get(type); if (listeners === undefined) { throw new Error("can't happen"); } - listeners.push(listener); + listeners.push({ listener, oneShot }); } removeEventListener(type: string, listener: EventListenerOrEventListenerObject | null, options?: boolean | EventListenerOptions) { if (listener === undefined || listener == null) @@ -62,26 +70,37 @@ export async function init_polyfills(): Promise { if (options !== undefined) { throw new Error("FIXME: removeEventListener polyfill doesn't implement options"); } - if (!this.listeners.has(type)) { + if (!this.subscribers.has(type)) { return; } - const listeners = this.listeners.get(type); - if (listeners === undefined) + const subscribers = this.subscribers.get(type); + if (subscribers === undefined) return; - const index = listeners.indexOf(listener); + let index = -1; + const n = subscribers.length; + for (let i = 0; i < n; ++i) { + if (subscribers[i].listener === listener) { + index = i; + break; + } + } if (index > -1) { - listeners.splice(index, 1); + subscribers.splice(index, 1); } } dispatchEvent(event: Event) { - if (!this.listeners.has(event.type)) { + if (!this.subscribers.has(event.type)) { return true; } - const listeners = this.listeners.get(event.type); - if (listeners === undefined) { + const subscribers = this.subscribers.get(event.type); + if (subscribers === undefined) { return true; } - for (const listener of listeners) { + for (const sub of subscribers) { + const listener = sub.listener; + if (sub.oneShot) { + this.removeEventListener(event.type, listener); + } if (typeof listener === "function") { listener.call(this, event); } else { diff --git a/src/mono/wasm/runtime/promise-utils.ts b/src/mono/wasm/runtime/promise-utils.ts new file mode 100644 index 00000000000000..78ba0f4bf9201d --- /dev/null +++ b/src/mono/wasm/runtime/promise-utils.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. + +/// Make a promise that resolves after a given number of milliseconds. +export function delay(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +/// A PromiseController encapsulates a Promise together with easy access to its resolve and reject functions. +/// It's a bit like a CancelationTokenSource in .NET +export class PromiseController { + readonly promise: Promise; + readonly resolve: (value: T | PromiseLike) => void; + readonly reject: (reason: any) => void; + constructor() { + let rs: (value: T | PromiseLike) => void = undefined as any; + let rj: (reason: any) => void = undefined as any; + this.promise = new Promise((resolve, reject) => { + rs = resolve; + rj = reject; + }); + this.resolve = rs; + this.reject = rj; + } +} diff --git a/src/mono/wasm/runtime/pthreads/browser/index.ts b/src/mono/wasm/runtime/pthreads/browser/index.ts index cb2e7970f1cbdb..f4e4bafcfed0b2 100644 --- a/src/mono/wasm/runtime/pthreads/browser/index.ts +++ b/src/mono/wasm/runtime/pthreads/browser/index.ts @@ -3,6 +3,7 @@ import { Module } from "../../imports"; import { pthread_ptr, MonoWorkerMessageChannelCreated, isMonoWorkerMessageChannelCreated, monoSymbol } from "../shared"; +import { MonoThreadMessage } from "../shared"; const threads: Map = new Map(); @@ -10,6 +11,14 @@ export interface Thread { readonly pthread_ptr: pthread_ptr; readonly worker: Worker; readonly port: MessagePort; + postMessageToWorker(message: T): void; +} + +class ThreadImpl implements Thread { + constructor(readonly pthread_ptr: pthread_ptr, readonly worker: Worker, readonly port: MessagePort) { } + postMessageToWorker(message: T): void { + this.port.postMessage(message); + } } interface ThreadCreateResolveReject { @@ -44,7 +53,7 @@ function resolvePromises(pthread_ptr: pthread_ptr, thread: Thread): void { } function addThread(pthread_ptr: pthread_ptr, worker: Worker, port: MessagePort): Thread { - const thread = { pthread_ptr, worker, port }; + const thread = new ThreadImpl(pthread_ptr, worker, port); threads.set(pthread_ptr, thread); return thread; } diff --git a/src/mono/wasm/runtime/pthreads/worker/index.ts b/src/mono/wasm/runtime/pthreads/worker/index.ts index c350ef5e99eabc..71ec15a38d271e 100644 --- a/src/mono/wasm/runtime/pthreads/worker/index.ts +++ b/src/mono/wasm/runtime/pthreads/worker/index.ts @@ -40,7 +40,9 @@ class WorkerSelf implements PThreadSelf { } } -export let pthread_self: PThreadSelf | null = null; +// we are lying that this is never null, but afterThreadInit should be the first time we get to run any code +// in the worker, so this becomes non-null very early. +export let pthread_self: PThreadSelf = null as any as PThreadSelf; /// This is the "public internal" API for runtime subsystems that wish to be notified about /// pthreads that are running on the current worker. diff --git a/src/mono/wasm/runtime/types.ts b/src/mono/wasm/runtime/types.ts index 7bdb805d83f61c..aa8ea8e61d298b 100644 --- a/src/mono/wasm/runtime/types.ts +++ b/src/mono/wasm/runtime/types.ts @@ -181,25 +181,14 @@ export type CoverageProfilerOptions = { send_to?: string // should be in the format ::, default: 'WebAssembly.Runtime::DumpCoverageProfileData' (DumpCoverageProfileData stores the data into INTERNAL.coverage_profile_data.) } -/// Options to configure the diagnostic server -export type DiagnosticServerOptions = { - suspend: boolean, // if true, the server will suspend the app when it starts until a diagnostic tool tells the runtime to resume. - connect_url: string, // websocket URL to connect to. -} - /// Options to configure EventPipe sessions that will be created and started at runtime startup export type DiagnosticOptions = { + /// An array of sessions to start at runtime startup sessions?: EventPipeSessionOptions[], /// If true, the diagnostic server will be started. If "wait", the runtime will wait at startup until a diagnsotic session connects to the server server?: DiagnosticServerOptions, } -export type EventPipeSessionDiagnosticServerID = number; -/// For EventPipe sessions started by the diagnostic server, an id is assigned to each session before it is associated with an eventpipe session in the runtime. -export interface EventPipeSessionIPCOptions { - diagnostic_server_id: EventPipeSessionDiagnosticServerID; -} - /// Options to configure the event pipe session /// The recommended method is to MONO.diagnostics.SesisonOptionsBuilder to create an instance of this type export interface EventPipeSessionOptions { @@ -211,6 +200,11 @@ export interface EventPipeSessionOptions { providers: string; } +/// Options to configure the diagnostic server +export type DiagnosticServerOptions = { + connect_url: string, // websocket URL to connect to. + suspend: boolean, // if true, the server will suspend the app when it starts until a diagnostic tool tells the runtime to resume. +} // how we extended emscripten Module export type DotnetModule = EmscriptenModule & DotnetModuleConfig; From e8aace4170a7424928f8abf97bb706c67d609ec5 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Tue, 5 Jul 2022 22:56:44 -0400 Subject: [PATCH 33/84] WIP more server pthread impl --- .../diagnostics/server_pthread/index.ts | 115 +++++++++++++++--- .../diagnostics/shared/controller-commands.ts | 7 ++ 2 files changed, 104 insertions(+), 18 deletions(-) diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts index f955774eee4188..7363588a183d18 100644 --- a/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts @@ -7,12 +7,13 @@ import { pthread_self } from "../../pthreads/worker"; import { Module } from "../../imports"; import { isDiagnosticMessage } from "../shared/types"; import { CharPtr } from "../../types/emscripten"; -import type { +import { DiagnosticServerControlCommand, - /*DiagnosticServerControlCommandStart, DiagnosticServerControlCommandSetSessionID*/ + makeDiagnosticServerControlReplyStartupResume } from "../shared/controller-commands"; import { mockScript } from "./mock-remote"; +import type { MockRemoteSocket } from "../mock"; import { PromiseController } from "../../promise-utils"; function addOneShotMessageEventListener(src: EventTarget): Promise> { @@ -26,12 +27,28 @@ export interface DiagnosticServer { stop(): void; } +interface ClientCommandBase { + command_set: "EventPipe" | "Process"; + command: string; +} + +interface EventPipeClientCommand extends ClientCommandBase { + command_set: "EventPipe"; + command: "CollectTracing2" | "Stop"; + args: string; +} + +interface ProcessClientCommand extends ClientCommandBase { + command_set: "Process"; + command: "Resume"; +} + +type ClientCommand = EventPipeClientCommand | ProcessClientCommand; + class DiagnosticServerImpl implements DiagnosticServer { readonly websocketUrl: string; - readonly ws: WebSocket | null; constructor(websocketUrl: string) { this.websocketUrl = websocketUrl; - this.ws = null; // new WebSocket(this.websocketUrl); pthread_self.addEventListenerFromBrowser(this.onMessageFromMainThread.bind(this)); } @@ -39,6 +56,8 @@ class DiagnosticServerImpl implements DiagnosticServer { private stopRequested = false; private stopRequestedController = new PromiseController(); + private attachToRuntimeController = new PromiseController(); + start(): void { console.log(`starting diagnostic server with url: ${this.websocketUrl}`); this.startRequestedController.resolve(); @@ -48,28 +67,51 @@ class DiagnosticServerImpl implements DiagnosticServer { this.stopRequestedController.resolve(); } + attachToRuntime(): void { + // TODO: mono_wasm_diagnostic_server_thread_attach (); + this.attachToRuntimeController.resolve(); + } + async serverLoop(this: DiagnosticServerImpl): Promise { await this.startRequestedController.promise; while (!this.stopRequested) { - const firstPromise: Promise<["first", string] | ["second", undefined]> = this.advertiseAndWaitForClient().then((r) => ["first", r]); - const secondPromise: Promise<["first", string] | ["second", undefined]> = this.stopRequestedController.promise.then(() => ["second", undefined]); - const clientCommandState = await Promise.race([firstPromise, secondPromise]); - // dispatchClientCommand(clientCommandState); - if (clientCommandState[0] === "first") { - console.debug("command received: ", clientCommandState[1]); - } else if (clientCommandState[0] === "second") { - console.debug("stop requested"); - break; + const p1: Promise<"first" | "second"> = this.advertiseAndWaitForClient().then(() => "first"); + const p2: Promise<"first" | "second"> = this.stopRequestedController.promise.then(() => "second"); + const result = await Promise.race([p1, p2]); + switch (result) { + case "first": + break; + case "second": + console.debug("stop requested"); + break; + default: + assertNever(result); } } } - async advertiseAndWaitForClient(): Promise { - const sock = mockScript.open(); - const p = addOneShotMessageEventListener(sock); - sock.send("ADVR"); + + async advertiseAndWaitForClient(): Promise { + const ws = mockScript.open(); + const p = addOneShotMessageEventListener(ws); + ws.send("ADVR"); const message = await p; - return message.data.toString(); + const cmd = this.parseCommand(message); + switch (cmd.command_set) { + case "EventPipe": + await this.dispatchEventPipeCommand(ws, cmd); + break; + case "Process": + await this.dispatchProcessCommand(ws, cmd); // resume + break; + default: + console.warn("Client sent unknown command", cmd); + break; + } + } + + parseCommand(message: MessageEvent): ClientCommand { + throw new Error("TODO"); } onMessageFromMainThread(this: DiagnosticServerImpl, event: MessageEvent): void { @@ -88,11 +130,44 @@ class DiagnosticServerImpl implements DiagnosticServer { case "stop": this.stop(); break; + case "attach_to_runtime": + this.attachToRuntime(); + break; default: console.warn("Unknown control command: ", cmd); break; } } + + // dispatch EventPipe commands received from the diagnostic client + async dispatchEventPipeCommand(ws: WebSocket | MockRemoteSocket, cmd: EventPipeClientCommand): Promise { + switch (cmd.command) { + case "CollectTracing2": { + await this.attachToRuntimeController.promise; // can't start tracing until we've attached to the runtime + const session = await createEventPipeStreamingSession(ws, cmd.args); + this.postClientReply(ws, "OK", session.id); + break; + } + case "Stop": + await this.stopEventPipe(cmd.args); + break; + default: + assertNever(cmd.command); + break; + } + } + + // dispatch Process commands received from the diagnostic client + async dispatchProcessCommand(ws: WebSocket | MockRemoteSocket, cmd: ProcessClientCommand): Promise { + switch (cmd.command) { + case "Resume": + pthread_self.postMessageToBrowser(makeDiagnosticServerControlReplyStartupResume()); + break; + default: + assertNever(cmd.command); + break; + } + } } @@ -108,3 +183,7 @@ export function mono_wasm_diagnostic_server_on_server_thread_created(websocketUr server.serverLoop(); }); } + +function assertNever(t: never): never { + throw new Error("Unexpected unreachable result: " + t); +} diff --git a/src/mono/wasm/runtime/diagnostics/shared/controller-commands.ts b/src/mono/wasm/runtime/diagnostics/shared/controller-commands.ts index ac52d07a7fd1e4..97b8745ebb0032 100644 --- a/src/mono/wasm/runtime/diagnostics/shared/controller-commands.ts +++ b/src/mono/wasm/runtime/diagnostics/shared/controller-commands.ts @@ -32,3 +32,10 @@ export type DiagnosticServerControlReply = export interface DiagnosticServerControlReplyStartupResume extends DiagnosticMessage { cmd: "startup_resume", } + +export function makeDiagnosticServerControlReplyStartupResume(): DiagnosticServerControlReplyStartupResume { + return { + type: "diagnostic_server", + cmd: "startup_resume", + }; +} From 54e618ba5aa9eb47f043951dbaee2dcfe5ccc232 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Wed, 6 Jul 2022 16:30:52 -0400 Subject: [PATCH 34/84] WIP: start adding queue from streaming thread to DS thread We can't set up a shared MessagePort very easily (we need to bounce through the main thread but it probably won't be able to process our message until it's too late). Also Atomics.waitAsync isn't available on many browsers (Chrome only). So we use emscripten's dispatch mechanism to trigger an event in the diagnostic thread to wake up and service the streaming thread's queue. Right now the queue is dumb so we trigger on every write. and also the write is synchronous. But it's simple to think about and it's implementable. --- .../wasm/runtime/diagnostics/mock/index.ts | 18 ++- .../diagnostics/server_pthread/event_pipe.ts | 90 +++++++++------ .../diagnostics/server_pthread/index.ts | 47 ++++++-- .../server_pthread/stream-queue.ts | 104 ++++++++++++++++++ src/mono/wasm/runtime/memory.ts | 15 +++ src/mono/wasm/runtime/types.ts | 6 + 6 files changed, 228 insertions(+), 52 deletions(-) create mode 100644 src/mono/wasm/runtime/diagnostics/server_pthread/stream-queue.ts diff --git a/src/mono/wasm/runtime/diagnostics/mock/index.ts b/src/mono/wasm/runtime/diagnostics/mock/index.ts index 1a68a7e5df2087..d77cfe9d038b11 100644 --- a/src/mono/wasm/runtime/diagnostics/mock/index.ts +++ b/src/mono/wasm/runtime/diagnostics/mock/index.ts @@ -1,14 +1,9 @@ -interface MockRemoteEventMap { - "open": Event; - "close": CloseEvent; - "message": MessageEvent; - "error": Event; -} export interface MockRemoteSocket extends EventTarget { - addEventListener(event: keyof MockRemoteEventMap, listener: (event: MockRemoteEventMap[keyof MockRemoteEventMap]) => void): void; - removeEventListener(event: keyof MockRemoteEventMap, listener: (event: MockRemoteEventMap[keyof MockRemoteEventMap]) => void): void; + addEventListener(type: T, listener: (this: MockRemoteSocket, ev: WebSocketEventMap[T]) => any, options?: boolean | AddEventListenerOptions): void; + addEventListener(event: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void; + removeEventListener(event: string, listener: EventListenerOrEventListenerObject): void; send(data: string | ArrayBuffer | Uint8Array | Blob | DataView): void; close(): void; } @@ -36,13 +31,14 @@ class MockScriptEngineSocketImpl implements MockRemoteSocket { } this.engine.mockReplyEventTarget.dispatchEvent(event); } - addEventListener(event: keyof MockRemoteEventMap, listener: (event: Event | CloseEvent | MessageEvent) => void): void { + addEventListener(event: T, listener: (event: WebSocketEventMap[T]) => any, options?: boolean | AddEventListenerOptions): void; + addEventListener(event: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void { if (this.engine.trace) { console.debug(`mock ${this.engine.ident} client added listener for ${event}`); } - this.engine.eventTarget.addEventListener(event, listener); + this.engine.eventTarget.addEventListener(event, listener, options); } - removeEventListener(event: keyof MockRemoteEventMap, listener: (event: Event | CloseEvent | MessageEvent) => void): void { + removeEventListener(event: string, listener: EventListenerOrEventListenerObject): void { if (this.engine.trace) { console.debug(`mock ${this.engine.ident} client removed listener for ${event}`); } diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/event_pipe.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/event_pipe.ts index 4d308d0e494cf9..5f48733af5df20 100644 --- a/src/mono/wasm/runtime/diagnostics/server_pthread/event_pipe.ts +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/event_pipe.ts @@ -1,36 +1,59 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -import type { - EventPipeSessionIDImpl, -} from "../shared/types"; +import { assertNever } from "../../types"; +import { MockRemoteSocket } from "../mock"; +import { VoidPtr } from "../../types/emscripten"; +import { Module } from "../../imports"; enum ListenerState { - AwaitingCommand, - DispatchedCommand, SendingTrailingData, Closed, Error } -function assertNever(x: never): never { - throw new Error("Unexpected object: " + x); + +// the common bits that we depend on from a real WebSocket or a MockRemoteSocket used for testing +interface CommonSocket { + addEventListener(type: T, listener: (this: CommonSocket, ev: WebSocketEventMap[T]) => any, options?: boolean | AddEventListenerOptions): void; + addEventListener(event: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void; + removeEventListener(event: string, listener: EventListenerOrEventListenerObject): void; + send(data: string | ArrayBuffer | Uint8Array | Blob | DataView): void; + close(): void; } -export class EventPipeServerConnection { - readonly type = "eventpipe"; - private _sessionID: EventPipeSessionIDImpl | null = null; - private _state: ListenerState; - constructor(readonly socket: WebSocket) { - this._state = ListenerState.AwaitingCommand; +type AssignableTo = Q extends T ? true : false; + +function static_assert(x: Cond): asserts x is Cond { /*empty*/ } + +{ + static_assert>(true); + static_assert>(true); + + static_assert>(false); // sanity check that static_assert works +} + + +class SocketGuts { + constructor(private readonly ws: CommonSocket) { } + close(): void { + this.ws.close(); } - get sessionID(): EventPipeSessionIDImpl | null { - return this._sessionID; + write(data: VoidPtr, size: number): void { + const buf = new ArrayBuffer(size); + const view = new Uint8Array(buf); + // Can we avoid this copy? + view.set(new Uint8Array(Module.HEAPU8.buffer, data as unknown as number, size)); + this.ws.send(buf); } - setSessionID(sessionID: EventPipeSessionIDImpl): void { - if (this._sessionID !== null) - throw new Error("Session ID already set"); - this._sessionID = sessionID; +} + +export class EventPipeSocketConnection { + private _state: ListenerState; + readonly stream: SocketGuts; + constructor(readonly socket: CommonSocket) { + this._state = ListenerState.SendingTrailingData; + this.stream = new SocketGuts(socket); } close(): void { @@ -41,21 +64,15 @@ export class EventPipeServerConnection { return; default: this._state = ListenerState.Closed; - this.socket.close(); + this.stream.close(); return; } } - postMessage(message: string): boolean { + write(ptr: VoidPtr, len: number): boolean { switch (this._state) { - case ListenerState.AwaitingCommand: - throw new Error("Unexpected postMessage: " + message); - case ListenerState.DispatchedCommand: - this._state = ListenerState.SendingTrailingData; - this.socket.send(message); - return true; case ListenerState.SendingTrailingData: - this.socket.send(message); + this.stream.write(ptr, len); return true; case ListenerState.Closed: // ignore @@ -65,15 +82,12 @@ export class EventPipeServerConnection { } } - private _onMessage(/*event: MessageEvent*/) { + private _onMessage(event: MessageEvent): void { switch (this._state) { - case ListenerState.AwaitingCommand: - /* TODO process command */ - this._state = ListenerState.DispatchedCommand; - break; - case ListenerState.DispatchedCommand: case ListenerState.SendingTrailingData: /* unexpected message */ + console.warn("EventPipe session stream received unexpected message from websocket", event); + // TODO notify runtime that the connection had an error this._state = ListenerState.Error; break; case ListenerState.Closed: @@ -96,6 +110,7 @@ export class EventPipeServerConnection { return; /* do nothing */ default: this._state = ListenerState.Closed; + this.stream.close(); // TODO: notify runtime that connection is closed return; } @@ -103,6 +118,8 @@ export class EventPipeServerConnection { private _onError(/*event: Event*/) { this._state = ListenerState.Error; + this.stream.close(); + // TODO: notify runtime that connection had an error } addListeners(): void { @@ -112,3 +129,8 @@ export class EventPipeServerConnection { } } +export function takeOverSocket(socket: CommonSocket): EventPipeSocketConnection { + const connection = new EventPipeSocketConnection(socket); + connection.addListeners(); + return connection; +} diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts index 7363588a183d18..8e13b3b03c87fb 100644 --- a/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts @@ -3,10 +3,11 @@ /// +import { assertNever } from "../../types"; import { pthread_self } from "../../pthreads/worker"; import { Module } from "../../imports"; -import { isDiagnosticMessage } from "../shared/types"; -import { CharPtr } from "../../types/emscripten"; +import { EventPipeSessionIDImpl, isDiagnosticMessage } from "../shared/types"; +import { CharPtr, VoidPtr } from "../../types/emscripten"; import { DiagnosticServerControlCommand, makeDiagnosticServerControlReplyStartupResume @@ -15,6 +16,8 @@ import { import { mockScript } from "./mock-remote"; import type { MockRemoteSocket } from "../mock"; import { PromiseController } from "../../promise-utils"; +import { EventPipeSocketConnection, takeOverSocket } from "./event_pipe"; +import { StreamQueue, allocateQueue } from "./stream-queue"; function addOneShotMessageEventListener(src: EventTarget): Promise> { return new Promise((resolve) => { @@ -145,7 +148,8 @@ class DiagnosticServerImpl implements DiagnosticServer { case "CollectTracing2": { await this.attachToRuntimeController.promise; // can't start tracing until we've attached to the runtime const session = await createEventPipeStreamingSession(ws, cmd.args); - this.postClientReply(ws, "OK", session.id); + this.postClientReply(ws, "OK", session.sessionID); + break; } case "Stop": @@ -170,6 +174,39 @@ class DiagnosticServerImpl implements DiagnosticServer { } } +class EventPipeStreamingSession { + + constructor(readonly sessionID: EventPipeSessionIDImpl, readonly ws: WebSocket | MockRemoteSocket, + readonly queue: StreamQueue, readonly connection: EventPipeSocketConnection) { } +} + +async function createEventPipeStreamingSession(ws: WebSocket | MockRemoteSocket, args: string): Promise { + // First, create the native IPC stream and get its queue. + const ipcStreamAddr = mono_wasm_diagnostic_server_create_stream(); // FIXME: this should be a wrapped in a JS object so we can free it when we're done. + const queueAddr = mono_wasm_diagnostic_server_get_stream_queue(ipcStreamAddr); + // then take over the websocket connection + const conn = takeOverSocket(ws); + // and set up queue notifications + const queue = allocateQueue(queueAddr, conn.write.bind(conn)); + // create the event pipe session + const sessionID = mono_wasm_event_pipe_stream_session_enable(ipcStreamAddr, args); + return new EventPipeStreamingSession(sessionID, ws, queue, conn); +} + +function mono_wasm_diagnostic_server_create_stream(): VoidPtr { + // this shoudl be in C and it should jsut allocate one of our IPC streams + throw new Error("TODO"); +} + +function mono_wasm_diagnostic_server_get_stream_queue(streamAddr: VoidPtr): VoidPtr { + // TODO: this can probably be in JS if we put the queue at a known address in the stream. (probably offset 0); + return streamAddr; +} + +function mono_wasm_event_pipe_stream_session_enable(ipcStreamAddr: VoidPtr, args: string): EventPipeSessionIDImpl { + // this should be implemented in C. and it should call ep_enable. + throw new Error("TODO"); +} /// Called by the runtime to initialize the diagnostic server workers export function mono_wasm_diagnostic_server_on_server_thread_created(websocketUrlPtr: CharPtr): void { @@ -183,7 +220,3 @@ export function mono_wasm_diagnostic_server_on_server_thread_created(websocketUr server.serverLoop(); }); } - -function assertNever(t: never): never { - throw new Error("Unexpected unreachable result: " + t); -} diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/stream-queue.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/stream-queue.ts new file mode 100644 index 00000000000000..ea0f32d4e93366 --- /dev/null +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/stream-queue.ts @@ -0,0 +1,104 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import { VoidPtr } from "../../types/emscripten"; +import * as Memory from "../../memory"; + + +/// One-reader, one-writer, size 1 queue for messages from an EventPipe streaming thread to +// the diagnostic server thread that owns the WebSocket. + +// EventPipeStreamQueue has 3 memory words that are used to communicate with the streaming thread: +// struct MonoWasmEventPipeStreamQueue { +// void* buf; +// int32_t count; +// volatile int32_t write_done; +// } +// +// To write, the streaming thread does: +// +// int32_t queue_write (MonoWasmEventPipeSteamQueue *queue, uint8_t *buf, int32_t len, int32_t *bytes_written) +// { +// queue->buf = buf; +// queue->count = len; +// //WISH: mono_wasm_memory_atomic_notify (&queue->wakeup_write, 1); // __builtin_wasm_memory_atomic_notify((int*)addr, count); +// emscripten_dispatch_to_thread (diagnostic_thread_id, wakeup_stream_queue, queue); +// int r = mono_wasm_memory_atomic_wait (&queue->wakeup_write_done, 0, -1); // __builtin_wasm_memory_atomic_wait((int*)addr, expected, timeout); // returns 0 ok, 1 not_equal, 2 timed out +// if (G_UNLIKELY (r != 0) { +// return -1; +// } +// result = Atomics.load (wakeup_write_done); // 0 or errno +// if (bytes_writen) *bytes_written = len; +// mono_atomic_store_int32 (&queue->wakeup_write_done, 0); +// +// This would be a lot less hacky if more browsers implemented Atomics.waitAsync. +// Then we wouldn't have to use emscripten_dispatch_to_thread, and instead the diagnostic server could +// just call Atomics.waitAsync to wait for the streaming thread to write. + +const BUF_OFFSET = 0; +const COUNT_OFFSET = 4; +const WRITE_DONE_OFFSET = 8; + +type SyncSendBuffer = (buf: VoidPtr, len: number) => void; + +export class StreamQueue { + readonly workAvailable: EventTarget = new EventTarget(); + readonly signalWorkAvailable = this.signalWorkAvailableImpl.bind(this); + + constructor(readonly queue_addr: VoidPtr, readonly syncSendBuffer: SyncSendBuffer) { + this.workAvailable.addEventListener("workAvailable", this.onWorkAvailable.bind(this)); + } + + private get wakeup_write_addr(): VoidPtr { + return this.queue_addr + BUF_OFFSET; + } + private get count_addr(): VoidPtr { + return this.queue_addr + COUNT_OFFSET; + } + private get wakeup_write_done_addr(): VoidPtr { + return this.queue_addr + WRITE_DONE_OFFSET; + } + + /// called from native code on the diagnostic thread when the streaming thread queues a call to notify the + /// diagnostic thread that it can send the buffer. + wakeup(): void { + queueMicrotask(this.signalWorkAvailable); + } + + private signalWorkAvailableImpl(this: StreamQueue): void { + this.workAvailable.dispatchEvent(new Event("workAvailable")); + } + + private onWorkAvailable(this: StreamQueue /*,event: Event */): void { + const buf = Memory.getI32(this.wakeup_write_addr) as unknown as VoidPtr; + const count = Memory.getI32(this.count_addr); + Memory.setI32(this.wakeup_write_addr, 0); + if (count > 0) { + this.syncSendBuffer(buf, count); + } + Memory.Atomics.storeI32(this.wakeup_write_done_addr, 0); + Memory.Atomics.notifyI32(this.wakeup_write_done_addr, 1); + } +} + +// maps stream queue addresses to StreamQueue instances +const streamQueueMap = new Map(); + +export function allocateQueue(nativeQueueAddr: VoidPtr, syncSendBuffer: SyncSendBuffer): StreamQueue { + const queue = new StreamQueue(nativeQueueAddr, syncSendBuffer); + streamQueueMap.set(nativeQueueAddr, queue); + return queue; +} + +export function closeQueue(nativeQueueAddr: VoidPtr): void { + streamQueueMap.delete(nativeQueueAddr); + // TODO: remove the event listener? +} + +// called from native code on the diagnostic thread by queueing a call from the streaming thread. +export function mono_wasm_event_pipe_stream_signal_work_available(nativeQueueAddr: VoidPtr): void { + const queue = streamQueueMap.get(nativeQueueAddr); + if (queue) { + queue.wakeup(); + } +} diff --git a/src/mono/wasm/runtime/memory.ts b/src/mono/wasm/runtime/memory.ts index 2f3a7293581666..ecc4275493d611 100644 --- a/src/mono/wasm/runtime/memory.ts +++ b/src/mono/wasm/runtime/memory.ts @@ -1,3 +1,4 @@ +import monoWasmThreads from "consts:monoWasmThreads"; import { Module, runtimeHelpers } from "./imports"; import { mono_assert } from "./types"; import { VoidPtr, NativePointer, ManagedPointer } from "./types/emscripten"; @@ -255,3 +256,17 @@ export function withStackAlloc(bytesWanted: number, f: (ptr } } +const BuiltinAtomics = globalThis.Atomics; + +export const Atomics = monoWasmThreads ? { + storeI32(offset: _MemOffset, value: number): void { + + BuiltinAtomics.store(Module.HEAP32, offset >>> 2, value); + }, + notifyI32(offset: _MemOffset, count: number): void { + BuiltinAtomics.notify(Module.HEAP32, offset >>> 2, count); + } +} : { + storeI32: setI32, + notifyI32: () => { /*empty*/ } +}; diff --git a/src/mono/wasm/runtime/types.ts b/src/mono/wasm/runtime/types.ts index aa8ea8e61d298b..d9ffe05146e225 100644 --- a/src/mono/wasm/runtime/types.ts +++ b/src/mono/wasm/runtime/types.ts @@ -301,6 +301,12 @@ export function is_nullish(value: T | null | undefined): value is null | unde return (value === undefined) || (value === null); } +/// Always throws. Used to handle unreachable switch branches when TypeScript refines the type of a variable +/// to 'never' after you handle all the cases it knows about. +export function assertNever(x: never): never { + throw new Error("Unexpected value: " + x); +} + /// returns true if the given value is not Thenable /// /// Useful if some function returns a value or a promise of a value. From c1c4c86d6f9b2aa54e6837e7d3b3f459a5d0c24c Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Thu, 7 Jul 2022 16:28:05 -0400 Subject: [PATCH 35/84] [wasm] Incremental build and rollup warnings cleanups - Add 'node/buffer' as an extrenal dependency. This doesn't do anything except quiet a rollup warning about the import. - Add all the .ts files, and the tsconfig files (except node_modules) to the rollup inputs, to make sure we re-run rollup when anything changes. --- src/mono/wasm/runtime/rollup.config.js | 6 ++++++ src/mono/wasm/wasm.proj | 7 ++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/mono/wasm/runtime/rollup.config.js b/src/mono/wasm/runtime/rollup.config.js index d8721129c82412..9ab2cf18b1302f 100644 --- a/src/mono/wasm/runtime/rollup.config.js +++ b/src/mono/wasm/runtime/rollup.config.js @@ -64,6 +64,10 @@ const inlineAssert = [ }]; const outputCodePlugins = [regexReplace(inlineAssert), consts({ productVersion, configuration, monoWasmThreads }), typescript()]; +const externalDependencies = [ + "node/buffer" +]; + const iffeConfig = { treeshake: !isDebug, input: "exports.ts", @@ -83,6 +87,7 @@ const iffeConfig = { plugins, } ], + external: externalDependencies, plugins: outputCodePlugins }; const typesConfig = { @@ -95,6 +100,7 @@ const typesConfig = { plugins: [writeOnChangePlugin()], } ], + external: externalDependencies, plugins: [dts()], }; diff --git a/src/mono/wasm/wasm.proj b/src/mono/wasm/wasm.proj index 62ef5f58b45ab1..b9caef72ceb794 100644 --- a/src/mono/wasm/wasm.proj +++ b/src/mono/wasm/wasm.proj @@ -351,10 +351,11 @@ - <_RollupInputs Include="$(MonoProjectRoot)wasm/runtime/*.ts"/> + <_RollupInputs Include="$(MonoProjectRoot)wasm/runtime/**/*.ts" + Exclude="$(MonoProjectRoot)wasm/runtime/dotnet.d.ts;$(MonoProjectRoot)wasm/runtime/node_modules/**/*.ts" /> + <_RollupInputs Include="$(MonoProjectRoot)wasm/runtime/**/tsconfig.*" + Exclude="$(MonoProjectRoot)wasm/runtime/node_modules/**/tsconfig.*" /> <_RollupInputs Include="$(MonoProjectRoot)wasm/runtime/workers/**/*.js"/> - <_RollupInputs Include="$(MonoProjectRoot)wasm/runtime/workers/**/*.ts"/> - <_RollupInputs Include="$(MonoProjectRoot)wasm/runtime/types/*.ts"/> <_RollupInputs Include="$(MonoProjectRoot)wasm/runtimetypes/*.d.ts"/> <_RollupInputs Include="$(MonoProjectRoot)wasm/runtime/*.json"/> <_RollupInputs Include="$(MonoProjectRoot)wasm/runtime/*.js"/> From 2cca7f062fc0845aefde5f8f67d6f40068a864d1 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Thu, 7 Jul 2022 16:29:54 -0400 Subject: [PATCH 36/84] WIP: work on wiring up DS protocol commands (mock); resume hack - start adding commands so that we can strt some sessions from DS - we can't avoid a busy loop in ds_server_wasm_pause_for_diagnostics_monitor. we can't make the main thread pause until we get a resume command until after we're able to start an EP session (DS client won't send a resume command until we send an EP session ID back). If the DS pauses until it can attach to the runtime, and the runtime pauses until DS tells it to resume, the main thread pause has to be after we get EP and DS initialized. But that means it can't be async. So we'll just have to busy wait on a condition variable in native. --- src/mono/mono/component/diagnostics_server.c | 4 + src/mono/wasm/runtime/cjs/dotnet.cjs.lib.js | 1 + src/mono/wasm/runtime/diagnostics/index.ts | 16 ++- .../diagnostics/server_pthread/index.ts | 117 +++++++++--------- .../server_pthread/mock-command-parser.ts | 19 +++ .../diagnostics/server_pthread/mock-remote.ts | 21 +++- .../protocol-client-commands.ts | 80 ++++++++++++ src/mono/wasm/runtime/dotnet.d.ts | 27 ++-- src/mono/wasm/runtime/es6/dotnet.es6.lib.js | 1 + src/mono/wasm/runtime/exports.ts | 4 +- 10 files changed, 212 insertions(+), 78 deletions(-) create mode 100644 src/mono/wasm/runtime/diagnostics/server_pthread/mock-command-parser.ts create mode 100644 src/mono/wasm/runtime/diagnostics/server_pthread/protocol-client-commands.ts diff --git a/src/mono/mono/component/diagnostics_server.c b/src/mono/mono/component/diagnostics_server.c index fa47935b1d4c0e..c25b218b56f179 100644 --- a/src/mono/mono/component/diagnostics_server.c +++ b/src/mono/mono/component/diagnostics_server.c @@ -36,6 +36,9 @@ ds_server_wasm_pause_for_diagnostics_monitor (void); static void ds_server_wasm_disable (void); +extern void +mono_wasm_diagnostic_server_on_runtime_server_init (void); + static MonoComponentDiagnosticsServer fn_table = { { MONO_COMPONENT_ITF_VERSION, &diagnostics_server_available }, &ds_server_wasm_init, @@ -71,6 +74,7 @@ ds_server_wasm_init (void) EM_ASM({ console.log ("ds_server_wasm_init"); }); + mono_wasm_diagnostic_server_on_runtime_server_init(); return true; } diff --git a/src/mono/wasm/runtime/cjs/dotnet.cjs.lib.js b/src/mono/wasm/runtime/cjs/dotnet.cjs.lib.js index b92276bc36293b..7f4df9a563d6a6 100644 --- a/src/mono/wasm/runtime/cjs/dotnet.cjs.lib.js +++ b/src/mono/wasm/runtime/cjs/dotnet.cjs.lib.js @@ -96,6 +96,7 @@ const linked_functions = [ "mono_wasm_pthread_on_pthread_attached", /// diagnostics_server.c "mono_wasm_diagnostic_server_on_server_thread_created", + "mono_wasm_diagnostic_server_on_runtime_server_init", #endif ]; diff --git a/src/mono/wasm/runtime/diagnostics/index.ts b/src/mono/wasm/runtime/diagnostics/index.ts index 47d7fd58885f6d..8256e3ddfc9528 100644 --- a/src/mono/wasm/runtime/diagnostics/index.ts +++ b/src/mono/wasm/runtime/diagnostics/index.ts @@ -201,6 +201,7 @@ export const diagnostics: Diagnostics = { /// * The IPC sessions first send an IPC message with the session ID and then they start streaming //// * If the diagnostic server gets more commands it will send us a message through the serverController and we will start additional sessions +let suspendOnStartup = false; export async function mono_wasm_init_diagnostics(options: DiagnosticOptions): Promise { if (!is_nullish(options.server)) { @@ -212,9 +213,10 @@ export async function mono_wasm_init_diagnostics(options: DiagnosticOptions): Pr const controller = await startDiagnosticServer(url); if (controller) { if (suspend) { - console.debug("waiting for the diagnostic server to resume us"); - const response = await controller.waitForStartupResume(); - console.debug("diagnostic server resumed us", response); + suspendOnStartup = true; + //console.debug("waiting for the diagnostic server to resume us"); + //const response = await controller.waitForStartupResume(); + //console.debug("diagnostic server resumed us", response); } } } @@ -222,9 +224,15 @@ export async function mono_wasm_init_diagnostics(options: DiagnosticOptions): Pr startup_session_configs.push(...sessions); } -export function mono_wasm_diagnostic_server_attach(): void { +export function mono_wasm_diagnostic_server_on_runtime_server_init(): void { const controller = getController(); controller.postServerAttachToRuntime(); + if (suspendOnStartup) { + /* FIXME: this is a hack. we should just use a condition variable in native. */ + for (let i = 0; i < 10000; ++i) { + (Module)["_emscripten_main_thread_process_queued_calls"](); + } + } } export default diagnostics; diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts index 8e13b3b03c87fb..4cee973604b59c 100644 --- a/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts @@ -18,6 +18,18 @@ import type { MockRemoteSocket } from "../mock"; import { PromiseController } from "../../promise-utils"; import { EventPipeSocketConnection, takeOverSocket } from "./event_pipe"; import { StreamQueue, allocateQueue } from "./stream-queue"; +import { + isEventPipeCommand, + isProcessCommand, + ProtocolClientCommandBase, + EventPipeClientCommandBase, + ProcessClientCommandBase, + isEventPipeCommandCollectTracing2, + isEventPipeCommandStopTracing, + isProcessCommandResumeRuntime, + EventPipeCommandCollectTracing2, +} from "./protocol-client-commands"; +import parseMockCommand from "./mock-command-parser"; function addOneShotMessageEventListener(src: EventTarget): Promise> { return new Promise((resolve) => { @@ -30,24 +42,6 @@ export interface DiagnosticServer { stop(): void; } -interface ClientCommandBase { - command_set: "EventPipe" | "Process"; - command: string; -} - -interface EventPipeClientCommand extends ClientCommandBase { - command_set: "EventPipe"; - command: "CollectTracing2" | "Stop"; - args: string; -} - -interface ProcessClientCommand extends ClientCommandBase { - command_set: "Process"; - command: "Resume"; -} - -type ClientCommand = EventPipeClientCommand | ProcessClientCommand; - class DiagnosticServerImpl implements DiagnosticServer { readonly websocketUrl: string; constructor(websocketUrl: string) { @@ -97,24 +91,30 @@ class DiagnosticServerImpl implements DiagnosticServer { async advertiseAndWaitForClient(): Promise { const ws = mockScript.open(); const p = addOneShotMessageEventListener(ws); - ws.send("ADVR"); + ws.send("ADVR_V1"); const message = await p; + console.debug("received advertising response: ", message); const cmd = this.parseCommand(message); - switch (cmd.command_set) { - case "EventPipe": - await this.dispatchEventPipeCommand(ws, cmd); - break; - case "Process": - await this.dispatchProcessCommand(ws, cmd); // resume - break; - default: - console.warn("Client sent unknown command", cmd); - break; + if (cmd === null) { + console.error("unexpected message from client", message); + return; + } else if (isEventPipeCommand(cmd)) { + await this.dispatchEventPipeCommand(ws, cmd); + } else if (isProcessCommand(cmd)) { + await this.dispatchProcessCommand(ws, cmd); // resume + } else { + console.warn("Client sent unknown command", cmd); } } - parseCommand(message: MessageEvent): ClientCommand { - throw new Error("TODO"); + + parseCommand(message: MessageEvent): ProtocolClientCommandBase | null { + if (typeof message.data === "string") { + return parseMockCommand(message.data); + } else { + console.debug("parsing byte command: ", message.data); + throw new Error("TODO"); + } } onMessageFromMainThread(this: DiagnosticServerImpl, event: MessageEvent): void { @@ -143,33 +143,35 @@ class DiagnosticServerImpl implements DiagnosticServer { } // dispatch EventPipe commands received from the diagnostic client - async dispatchEventPipeCommand(ws: WebSocket | MockRemoteSocket, cmd: EventPipeClientCommand): Promise { - switch (cmd.command) { - case "CollectTracing2": { - await this.attachToRuntimeController.promise; // can't start tracing until we've attached to the runtime - const session = await createEventPipeStreamingSession(ws, cmd.args); - this.postClientReply(ws, "OK", session.sessionID); - - break; - } - case "Stop": - await this.stopEventPipe(cmd.args); - break; - default: - assertNever(cmd.command); - break; + async dispatchEventPipeCommand(ws: WebSocket | MockRemoteSocket, cmd: EventPipeClientCommandBase): Promise { + if (isEventPipeCommandCollectTracing2(cmd)) { + // FIXME: if the runtime is waiting for us to resume them, we deadlock here + // the runtime is waiting for us to send a resume command, and we're waiting for eventpipe to send us a resume command, which it won't do until the eventpipe session is enabled. + await this.attachToRuntimeController.promise; // can't start tracing until we've attached to the runtime + const session = await createEventPipeStreamingSession(ws, cmd); + this.postClientReply(ws, "OK", session.sessionID); + } else if (isEventPipeCommandStopTracing(cmd)) { + await this.stopEventPipe(cmd.sessionID); + } else { + console.warn("unknown EventPipe command: ", cmd); } } + postClientReply(ws: WebSocket | MockRemoteSocket, status: "OK", rest?: string | number): void { + ws.send(JSON.stringify([status, rest])); + } + + async stopEventPipe(sessionID: EventPipeSessionIDImpl): Promise { + /* TODO: finish me */ + console.debug("stopEventPipe", sessionID); + } + // dispatch Process commands received from the diagnostic client - async dispatchProcessCommand(ws: WebSocket | MockRemoteSocket, cmd: ProcessClientCommand): Promise { - switch (cmd.command) { - case "Resume": - pthread_self.postMessageToBrowser(makeDiagnosticServerControlReplyStartupResume()); - break; - default: - assertNever(cmd.command); - break; + async dispatchProcessCommand(ws: WebSocket | MockRemoteSocket, cmd: ProcessClientCommandBase): Promise { + if (isProcessCommandResumeRuntime(cmd)) { + pthread_self.postMessageToBrowser(makeDiagnosticServerControlReplyStartupResume()); + } else { + console.warn("unknown Process command", cmd); } } } @@ -180,7 +182,7 @@ class EventPipeStreamingSession { readonly queue: StreamQueue, readonly connection: EventPipeSocketConnection) { } } -async function createEventPipeStreamingSession(ws: WebSocket | MockRemoteSocket, args: string): Promise { +async function createEventPipeStreamingSession(ws: WebSocket | MockRemoteSocket, cmd: EventPipeCommandCollectTracing2): Promise { // First, create the native IPC stream and get its queue. const ipcStreamAddr = mono_wasm_diagnostic_server_create_stream(); // FIXME: this should be a wrapped in a JS object so we can free it when we're done. const queueAddr = mono_wasm_diagnostic_server_get_stream_queue(ipcStreamAddr); @@ -189,7 +191,7 @@ async function createEventPipeStreamingSession(ws: WebSocket | MockRemoteSocket, // and set up queue notifications const queue = allocateQueue(queueAddr, conn.write.bind(conn)); // create the event pipe session - const sessionID = mono_wasm_event_pipe_stream_session_enable(ipcStreamAddr, args); + const sessionID = mono_wasm_event_pipe_stream_session_enable(ipcStreamAddr, cmd); return new EventPipeStreamingSession(sessionID, ws, queue, conn); } @@ -203,8 +205,9 @@ function mono_wasm_diagnostic_server_get_stream_queue(streamAddr: VoidPtr): Void return streamAddr; } -function mono_wasm_event_pipe_stream_session_enable(ipcStreamAddr: VoidPtr, args: string): EventPipeSessionIDImpl { +function mono_wasm_event_pipe_stream_session_enable(ipcStreamAddr: VoidPtr, cmd: EventPipeCommandCollectTracing2): EventPipeSessionIDImpl { // this should be implemented in C. and it should call ep_enable. + console.debug("mono_wasm_event_pipe_stream_session_enable", ipcStreamAddr, cmd); throw new Error("TODO"); } diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/mock-command-parser.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/mock-command-parser.ts new file mode 100644 index 00000000000000..2f29ed9daed20f --- /dev/null +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/mock-command-parser.ts @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +import { ProtocolClientCommandBase, isDiagnosticCommandBase } from "./protocol-client-commands"; + +export default function parseCommand(x: string): ProtocolClientCommandBase | null { + let command: object; + try { + command = JSON.parse(x); + } catch (err) { + console.warn("error while parsing JSON diagnostic server protocol command", err); + return null; + } + if (isDiagnosticCommandBase(command)) { + return command; + } else { + console.warn("received a JSON diagnostic server protocol command without command_set or command", command); + return null; + } +} diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/mock-remote.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/mock-remote.ts index ad932292e4768d..80cbf271b463ab 100644 --- a/src/mono/wasm/runtime/diagnostics/server_pthread/mock-remote.ts +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/mock-remote.ts @@ -1,8 +1,10 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. import { mock, MockScriptEngine } from "../mock"; import { PromiseController } from "../../promise-utils"; -function expectAdvertise(data: string | ArrayBuffer) { return data === "ADVR"; } +function expectAdvertise(data: string | ArrayBuffer) { return data === "ADVR_V1"; } const scriptPC = new PromiseController(); const scriptPCunfulfilled = new PromiseController(); @@ -10,13 +12,26 @@ const scriptPCunfulfilled = new PromiseController(); const script: ((engine: MockScriptEngine) => Promise)[] = [ async (engine) => { await engine.waitForSend(expectAdvertise); - engine.reply("start session"); + engine.reply(JSON.stringify({ + command_set: "EventPipe", command: "CollectTracing2", + circularBufferMB: 1, + format: 1, + requestRundown: true, + providers: [ + { + keywords: 0, + logLevel: 5, + provider_name: "WasmHello", + filter_data: "EventCounterIntervalSec=1" + } + ] + })); scriptPC.resolve(); }, async (engine) => { await engine.waitForSend(expectAdvertise); await scriptPC.promise; - engine.reply("resume"); + engine.reply(JSON.stringify({ "command_set": "Process", "command": "ResumeRuntime" })); // engine.close(); }, async (engine) => { diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/protocol-client-commands.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/protocol-client-commands.ts new file mode 100644 index 00000000000000..cc99b9b9e6b908 --- /dev/null +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/protocol-client-commands.ts @@ -0,0 +1,80 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +export interface ProtocolClientCommandBase { + command_set: string; + command: string; +} + +export interface ProcessClientCommandBase extends ProtocolClientCommandBase { + command_set: "Process" +} + +export interface EventPipeClientCommandBase extends ProtocolClientCommandBase { + command_set: "EventPipe" +} + +export type ProcessCommand = + | ProcessCommandResumeRuntime + ; + +export type EventPipeCommand = + | EventPipeCommandCollectTracing2 + | EventPipeCommandStopTracing + ; + +export interface ProcessCommandResumeRuntime extends ProcessClientCommandBase { + command: "ResumeRuntime" +} + +export interface EventPipeCommandCollectTracing2 extends EventPipeClientCommandBase { + command: "CollectTracing2"; + circularBufferMB: number; + format: number; + requestRundown: boolean; + providers: EventPipeCollectTracingCommandProvider[]; +} + +export interface EventPipeCommandStopTracing extends EventPipeClientCommandBase { + command: "StopTracing"; + sessionID: number;// FIXME: this is 64-bits in the protocol +} + +export interface EventPipeCollectTracingCommandProvider { + keywords: number; + logLevel: number; + provider_name: string; + filter_data: string; +} + +export type ProtocolClientCommand = ProcessCommand | EventPipeCommand; + +export function isDiagnosticCommandBase(x: object): x is ProtocolClientCommandBase { + return typeof x === "object" && "command_set" in x && "command" in x; +} + +export function isProcessCommand(x: object): x is ProcessClientCommandBase { + return isDiagnosticCommandBase(x) && x.command_set === "Process"; +} + +export function isEventPipeCommand(x: object): x is EventPipeClientCommandBase { + return isDiagnosticCommandBase(x) && x.command_set === "EventPipe"; +} + +export function isProcessCommandResumeRuntime(x: ProcessClientCommandBase): x is ProcessCommandResumeRuntime { + return isProcessCommand(x) && x.command === "ResumeRuntime"; +} + +export function isEventPipeCollectTracingCommandProvider(x: object): x is EventPipeCollectTracingCommandProvider { + return typeof x === "object" && "keywords" in x && "logLevel" in x && "provider_name" in x && "filter_data" in x; +} + +export function isEventPipeCommandCollectTracing2(x: object): x is EventPipeCommandCollectTracing2 { + return isEventPipeCommand(x) && x.command === "CollectTracing2" && "circularBufferMB" in x && + "format" in x && "requestRundown" in x && "providers" in x && + Array.isArray((x).providers) && (x).providers.every(isEventPipeCollectTracingCommandProvider); +} + +export function isEventPipeCommandStopTracing(x: object): x is EventPipeCommandStopTracing { + return isEventPipeCommand(x) && x.command === "StopTracing" && "sessionID" in x; +} diff --git a/src/mono/wasm/runtime/dotnet.d.ts b/src/mono/wasm/runtime/dotnet.d.ts index 6b63dc8be81725..781eaf92adb89f 100644 --- a/src/mono/wasm/runtime/dotnet.d.ts +++ b/src/mono/wasm/runtime/dotnet.d.ts @@ -210,17 +210,17 @@ declare type CoverageProfilerOptions = { send_to?: string; }; declare type DiagnosticOptions = { - sessions?: (EventPipeSessionOptions & EventPipeSessionAutoStopOptions)[]; - server?: boolean | "wait"; + sessions?: EventPipeSessionOptions[]; + server?: DiagnosticServerOptions; }; -interface EventPipeSessionAutoStopOptions { - stop_at?: string; - on_session_stopped?: (session: EventPipeSession) => void; -} interface EventPipeSessionOptions { collectRundownEvents?: boolean; providers: string; } +declare type DiagnosticServerOptions = { + connect_url: string; + suspend: boolean; +}; declare type DotnetModuleConfig = { disableDotnet6Compatibility?: boolean; config?: MonoConfig | MonoConfigError; @@ -252,13 +252,6 @@ declare type DotnetModuleConfigImports = { url?: any; }; declare type EventPipeSessionID = bigint; -interface EventPipeSession { - get sessionID(): EventPipeSessionID; - readonly isIPCStreamingSession: boolean; - start(): void; - stop(): void; - getTraceBlob(): Blob; -} declare const eventLevel: { readonly LogAlways: 0; @@ -290,6 +283,14 @@ declare class SessionOptionsBuilder { addSampleProfilerProvider(overrideOptions?: UnnamedProviderConfiguration): SessionOptionsBuilder; build(): EventPipeSessionOptions; } + +interface EventPipeSession { + get sessionID(): EventPipeSessionID; + isIPCStreamingSession(): boolean; + start(): void; + stop(): void; + getTraceBlob(): Blob; +} interface Diagnostics { EventLevel: EventLevel; SessionOptionsBuilder: typeof SessionOptionsBuilder; diff --git a/src/mono/wasm/runtime/es6/dotnet.es6.lib.js b/src/mono/wasm/runtime/es6/dotnet.es6.lib.js index 938d8347b6531b..81adcfcbf807ba 100644 --- a/src/mono/wasm/runtime/es6/dotnet.es6.lib.js +++ b/src/mono/wasm/runtime/es6/dotnet.es6.lib.js @@ -133,6 +133,7 @@ const linked_functions = [ "mono_wasm_pthread_on_pthread_attached", // diagnostics_server.c "mono_wasm_diagnostic_server_on_server_thread_created", + "mono_wasm_diagnostic_server_on_runtime_server_init", #endif ]; diff --git a/src/mono/wasm/runtime/exports.ts b/src/mono/wasm/runtime/exports.ts index c0819d0c741937..681fa041c32436 100644 --- a/src/mono/wasm/runtime/exports.ts +++ b/src/mono/wasm/runtime/exports.ts @@ -54,7 +54,8 @@ import { mono_wasm_invoke_js_with_args_ref, mono_wasm_set_by_index_ref, mono_wasm_set_object_property_ref } from "./method-calls"; import { - mono_wasm_event_pipe_early_startup_callback + mono_wasm_event_pipe_early_startup_callback, + mono_wasm_diagnostic_server_on_runtime_server_init } from "./diagnostics"; import { mono_wasm_diagnostic_server_on_server_thread_created, @@ -369,6 +370,7 @@ const mono_wasm_threads_exports = !MonoWasmThreads ? undefined : { mono_wasm_pthread_on_pthread_attached, // diagnostics_server.c mono_wasm_diagnostic_server_on_server_thread_created, + mono_wasm_diagnostic_server_on_runtime_server_init, }; // the methods would be visible to EMCC linker From 0609be77b82aec8b8df7e855e6fba6e49e1dda0f Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Fri, 8 Jul 2022 16:25:59 -0400 Subject: [PATCH 37/84] WIP: set up a WasmIpcStream, create EP sessions from DS Seems to create the sesssion, but not seeing write events yet. possibly due to not flushing? --- src/mono/mono/component/diagnostics_server.c | 173 +++++++++++++++++- src/mono/mono/component/event_pipe-stub.c | 2 +- src/mono/mono/component/event_pipe-wasm.h | 11 +- src/mono/mono/component/event_pipe.c | 8 +- src/mono/mono/utils/mono-threads-wasm.h | 17 ++ .../Wasm.Browser.EventPipe.Sample.csproj | 2 +- src/mono/wasm/runtime/cjs/dotnet.cjs.lib.js | 1 + src/mono/wasm/runtime/cwraps.ts | 10 +- src/mono/wasm/runtime/diagnostics/index.ts | 27 ++- .../diagnostics/server_pthread/index.ts | 107 +++++++---- .../server_pthread/stream-queue.ts | 2 +- .../diagnostics/shared/controller-commands.ts | 15 -- src/mono/wasm/runtime/dotnet.d.ts | 2 +- src/mono/wasm/runtime/es6/dotnet.es6.lib.js | 1 + src/mono/wasm/runtime/exports.ts | 4 + src/mono/wasm/runtime/types.ts | 2 +- 16 files changed, 314 insertions(+), 70 deletions(-) diff --git a/src/mono/mono/component/diagnostics_server.c b/src/mono/mono/component/diagnostics_server.c index c25b218b56f179..0f1e50a32ff94e 100644 --- a/src/mono/mono/component/diagnostics_server.c +++ b/src/mono/mono/component/diagnostics_server.c @@ -8,8 +8,12 @@ #include #include #ifdef HOST_WASM +#include #include +#include +#include #include +#include #endif static bool @@ -24,6 +28,14 @@ static MonoComponentDiagnosticsServer fn_table = { &ds_server_disable }; #else +typedef struct _MonoWasmDiagnosticServerOptions { + int32_t suspend; /* set from JS! */ + MonoCoopSem suspend_resume; +} MonoWasmDiagnosticServerOptions; + +static MonoWasmDiagnosticServerOptions wasm_ds_options; +static pthread_t ds_thread_id; + static bool ds_server_wasm_init (void); @@ -37,7 +49,11 @@ static void ds_server_wasm_disable (void); extern void -mono_wasm_diagnostic_server_on_runtime_server_init (void); +mono_wasm_diagnostic_server_on_runtime_server_init (MonoWasmDiagnosticServerOptions *out_options); + +EMSCRIPTEN_KEEPALIVE void +mono_wasm_diagnostic_server_resume_runtime_startup (void); + static MonoComponentDiagnosticsServer fn_table = { { MONO_COMPONENT_ITF_VERSION, &diagnostics_server_available }, @@ -71,10 +87,12 @@ mono_component_diagnostics_server_init (void) static bool ds_server_wasm_init (void) { + /* called on the main thread when the runtime is sufficiently initialized */ EM_ASM({ console.log ("ds_server_wasm_init"); }); - mono_wasm_diagnostic_server_on_runtime_server_init(); + mono_coop_sem_init (&wasm_ds_options.suspend_resume, 0); + mono_wasm_diagnostic_server_on_runtime_server_init(&wasm_ds_options); return true; } @@ -94,8 +112,31 @@ ds_server_wasm_pause_for_diagnostics_monitor (void) EM_ASM({ console.log ("ds_server_wasm_pause_for_diagnostics_monitor"); }); + + /* wait until the DS receives a resume */ + if (wasm_ds_options.suspend) { + const guint timeout = 50; + const guint warn_threshold = 5000; + guint cumulative_timeout = 0; + while (true) { + MonoSemTimedwaitRet res = mono_coop_sem_timedwait (&wasm_ds_options.suspend_resume, timeout, MONO_SEM_FLAGS_ALERTABLE); + if (res == MONO_SEM_TIMEDWAIT_RET_SUCCESS || res == MONO_SEM_TIMEDWAIT_RET_ALERTED) + break; + else { + /* timed out */ + cumulative_timeout += timeout; + if (cumulative_timeout > warn_threshold) { + EM_ASM({ + console.log ("ds_server_wasm_pause_for_diagnostics_monitor paused for 5 seconds"); + }); + cumulative_timeout = 0; + } + } + } + } } + static void ds_server_wasm_disable (void) { @@ -138,5 +179,131 @@ mono_wasm_diagnostic_server_create_thread (const char *websocket_url, pthread_t return FALSE; } +void +mono_wasm_diagnostic_server_thread_attach_to_runtime (void) +{ + ds_thread_id = pthread_self(); + MonoThread *thread = mono_thread_internal_attach (mono_get_root_domain ()); + mono_thread_set_state (thread, ThreadState_Background); + mono_thread_info_set_flags (MONO_THREAD_INFO_FLAGS_NO_SAMPLE); + /* diagnostic server thread is now in GC Unsafe mode */ +} + +void +mono_wasm_diagnostic_server_post_resume_runtime (void) +{ + if (wasm_ds_options.suspend) { + /* wake the main thread */ + mono_coop_sem_post (&wasm_ds_options.suspend_resume); + } +} + +/* single-reader single-writer one-element queue. See + * src/mono/wasm/runtime/diagnostics/server_pthread/stream-queue.ts + */ +typedef struct WasmIpcStreamQueue { + uint8_t *buf; + int32_t count; + volatile int32_t write_done; +} WasmIpcStreamQueue; + +extern void +mono_wasm_diagnostic_server_stream_signal_work_available (WasmIpcStreamQueue *queue); + +static void +queue_wake_reader (void *ptr) { + /* asynchronously invoked on the ds server thread by the writer. */ + WasmIpcStreamQueue *q = (WasmIpcStreamQueue *)ptr; + mono_wasm_diagnostic_server_stream_signal_work_available (q); +} + +static int32_t +queue_push_sync (WasmIpcStreamQueue *q, const uint8_t *buf, uint32_t buf_size, uint32_t *bytes_written) +{ + /* to be called on the writing thread */ + /* single-writer, so there is no write contention */ + q->buf = (uint8_t*)buf; + q->count = buf_size; + emscripten_dispatch_to_thread (ds_thread_id, EM_FUNC_SIG_VI, NULL, queue_wake_reader, q); + // wait until the reader reads the value + int r = mono_wasm_atomic_wait_i32 (&q->write_done, 0, -1); + if (G_UNLIKELY (r != 0)) { + return -1; + } + if (mono_atomic_load_i32 (&q->write_done) != 0) + return -1; + if (bytes_written) + *bytes_written = buf_size; + return 0; +} + +typedef struct { + IpcStream stream; + WasmIpcStreamQueue queue; +} WasmIpcStream; + +static void +wasm_ipc_stream_free (void *self); +static bool +wasm_ipc_stream_read (void *self, uint8_t *buffer, uint32_t bytes_to_read, uint32_t *bytes_read, uint32_t timeout_ms); +static bool +wasm_ipc_stream_write (void *self, const uint8_t *buffer, uint32_t bytes_to_write, uint32_t *bytes_written, uint32_t timeout_ms); +static bool +wasm_ipc_stream_flush (void *self); +static bool +wasm_ipc_stream_close (void *self); + +static IpcStreamVtable wasm_ipc_stream_vtable = { + &wasm_ipc_stream_free, + &wasm_ipc_stream_read, + &wasm_ipc_stream_write, + &wasm_ipc_stream_flush, + &wasm_ipc_stream_close, +}; + +EMSCRIPTEN_KEEPALIVE IpcStream * +mono_wasm_diagnostic_server_create_stream (void) +{ + g_assert (G_STRUCT_OFFSET(WasmIpcStream, queue) == 4); // keep in sync with mono_wasm_diagnostic_server_get_stream_queue + WasmIpcStream *stream = g_new0 (WasmIpcStream, 1); + ep_ipc_stream_init (&stream->stream, &wasm_ipc_stream_vtable); + return &stream->stream; +} -#endif; +static void +wasm_ipc_stream_free (void *self) +{ + g_free (self); +} +static bool +wasm_ipc_stream_read (void *self, uint8_t *buffer, uint32_t bytes_to_read, uint32_t *bytes_read, uint32_t timeout_ms) +{ + /* our reader is in JS */ + g_assert_not_reached(); +} +static bool +wasm_ipc_stream_write (void *self, const uint8_t *buffer, uint32_t bytes_to_write, uint32_t *bytes_written, uint32_t timeout_ms) +{ + WasmIpcStream *stream = (WasmIpcStream *)self; + g_assert (timeout_ms == EP_INFINITE_WAIT); // pass it down to the queue if the timeout param starts being used + int r = queue_push_sync (&stream->queue, buffer, bytes_to_write, bytes_written); + return r == 0; +} + +static bool +wasm_ipc_stream_flush (void *self) +{ + return true; +} +static bool +wasm_ipc_stream_close (void *self) +{ + // TODO: signal the writer to close + EM_ASM({ + console.log ("wasm_ipc_stream_close"); + }); + return true; +} + + +#endif diff --git a/src/mono/mono/component/event_pipe-stub.c b/src/mono/mono/component/event_pipe-stub.c index 08b718011d286e..5dc8a6b82b600c 100644 --- a/src/mono/mono/component/event_pipe-stub.c +++ b/src/mono/mono/component/event_pipe-stub.c @@ -521,12 +521,12 @@ mono_component_event_pipe_init (void) EMSCRIPTEN_KEEPALIVE gboolean mono_wasm_event_pipe_enable (const ep_char8_t *output_path, + IpcStream *ipc_stream, uint32_t circular_buffer_size_in_mb, const ep_char8_t *providers, /* EventPipeSessionType session_type = EP_SESSION_TYPE_FILE, */ /* EventPipieSerializationFormat format = EP_SERIALIZATION_FORMAT_NETTRACE_V4, */ /* bool */ gboolean rundown_requested, - /* IpcStream stream = NULL, */ /* EventPipeSessionSycnhronousCallback sync_callback = NULL, */ /* void *callback_additional_data, */ MonoWasmEventPipeSessionID *out_session_id) diff --git a/src/mono/mono/component/event_pipe-wasm.h b/src/mono/mono/component/event_pipe-wasm.h index e4f3841b726453..291626173788f9 100644 --- a/src/mono/mono/component/event_pipe-wasm.h +++ b/src/mono/mono/component/event_pipe-wasm.h @@ -30,12 +30,12 @@ typedef void (*mono_wasm_event_pipe_early_startup_cb)(void); EMSCRIPTEN_KEEPALIVE gboolean mono_wasm_event_pipe_enable (const ep_char8_t *output_path, + IpcStream *ipc_stream, uint32_t circular_buffer_size_in_mb, const ep_char8_t *providers, /* EventPipeSessionType session_type = EP_SESSION_TYPE_FILE, */ /* EventPipieSerializationFormat format = EP_SERIALIZATION_FORMAT_NETTRACE_V4, */ /* bool */ gboolean rundown_requested, - /* IpcStream stream = NULL, */ /* EventPipeSessionSycnhronousCallback sync_callback = NULL, */ /* void *callback_additional_data, */ MonoWasmEventPipeSessionID *out_session_id); @@ -49,6 +49,15 @@ mono_wasm_event_pipe_session_disable (MonoWasmEventPipeSessionID session_id); EMSCRIPTEN_KEEPALIVE gboolean mono_wasm_diagnostic_server_create_thread (const char *websocket_url, pthread_t *out_thread_id); +EMSCRIPTEN_KEEPALIVE void +mono_wasm_diagnostic_server_thread_attach_to_runtime (void); + +EMSCRIPTEN_KEEPALIVE void +mono_wasm_diagnostic_server_post_resume_runtime (void); + +EMSCRIPTEN_KEEPALIVE IpcStream * +mono_wasm_diagnostic_server_create_stream (void); + void mono_wasm_event_pipe_set_early_startup_callback (mono_wasm_event_pipe_early_startup_cb callback); diff --git a/src/mono/mono/component/event_pipe.c b/src/mono/mono/component/event_pipe.c index a61e0fe875af76..de76dcaa45aeb3 100644 --- a/src/mono/mono/component/event_pipe.c +++ b/src/mono/mono/component/event_pipe.c @@ -363,6 +363,7 @@ wasm_to_ep_session_id (MonoWasmEventPipeSessionID session_id) EMSCRIPTEN_KEEPALIVE gboolean mono_wasm_event_pipe_enable (const ep_char8_t *output_path, + IpcStream *ipc_stream, uint32_t circular_buffer_size_in_mb, const ep_char8_t *providers, /* EventPipeSessionType session_type = EP_SESSION_TYPE_FILE, */ @@ -375,7 +376,10 @@ mono_wasm_event_pipe_enable (const ep_char8_t *output_path, { MONO_ENTER_GC_UNSAFE; EventPipeSerializationFormat format = EP_SERIALIZATION_FORMAT_NETTRACE_V4; - EventPipeSessionType session_type = EP_SESSION_TYPE_FILE; + EventPipeSessionType session_type = output_path != NULL ? EP_SESSION_TYPE_FILE : EP_SESSION_TYPE_IPCSTREAM; + + g_assert ((output_path == NULL && ipc_stream != NULL) || + (output_path != NULL && ipc_stream == NULL)); EventPipeSessionID session; session = ep_enable_2 (output_path, @@ -384,7 +388,7 @@ mono_wasm_event_pipe_enable (const ep_char8_t *output_path, session_type, format, !!rundown_requested, - /* stream */NULL, + ipc_stream, /* callback*/ NULL, /* callback_data*/ NULL); diff --git a/src/mono/mono/utils/mono-threads-wasm.h b/src/mono/mono/utils/mono-threads-wasm.h index f73192c7c47b7d..5ff92b2b1ee6ca 100644 --- a/src/mono/mono/utils/mono-threads-wasm.h +++ b/src/mono/mono/utils/mono-threads-wasm.h @@ -48,6 +48,23 @@ mono_threads_wasm_async_run_in_main_thread_vii (void (*func)(gpointer, gpointer) void mono_threads_wasm_on_thread_attached (void); +static inline +int32_t +mono_wasm_atomic_wait_i32 (volatile int32_t *addr, int32_t expected, int32_t timeout_ns) +{ + // Don't call this on the main thread! + // See https://github.com/WebAssembly/threads/issues/174 + // memory.atomic.wait32 + // + // timeout_ns == -1 means infinite wait + // + // return values: + // 0 == "ok", thread blocked and was woken up + // 1 == "not-equal", value at addr was not equal to expected + // 2 == "timed-out", timeout expired before thread was woken up + return __builtin_wasm_memory_atomic_wait32((int32_t*)addr, expected, timeout_ns); +} + #endif /* HOST_WASM*/ #endif /* __MONO_THREADS_WASM_H__ */ diff --git a/src/mono/sample/wasm/browser-eventpipe/Wasm.Browser.EventPipe.Sample.csproj b/src/mono/sample/wasm/browser-eventpipe/Wasm.Browser.EventPipe.Sample.csproj index f1689272f4907d..b723b907b560da 100644 --- a/src/mono/sample/wasm/browser-eventpipe/Wasm.Browser.EventPipe.Sample.csproj +++ b/src/mono/sample/wasm/browser-eventpipe/Wasm.Browser.EventPipe.Sample.csproj @@ -22,7 +22,7 @@ }' /> diff --git a/src/mono/wasm/runtime/cjs/dotnet.cjs.lib.js b/src/mono/wasm/runtime/cjs/dotnet.cjs.lib.js index 7f4df9a563d6a6..803054f4973d86 100644 --- a/src/mono/wasm/runtime/cjs/dotnet.cjs.lib.js +++ b/src/mono/wasm/runtime/cjs/dotnet.cjs.lib.js @@ -97,6 +97,7 @@ const linked_functions = [ /// diagnostics_server.c "mono_wasm_diagnostic_server_on_server_thread_created", "mono_wasm_diagnostic_server_on_runtime_server_init", + "mono_wasm_diagnostic_server_stream_signal_work_available", #endif ]; diff --git a/src/mono/wasm/runtime/cwraps.ts b/src/mono/wasm/runtime/cwraps.ts index 3f88501acc7ff2..317ad12a3f1d59 100644 --- a/src/mono/wasm/runtime/cwraps.ts +++ b/src/mono/wasm/runtime/cwraps.ts @@ -68,10 +68,13 @@ const fn_signatures: [ident: string, returnType: string | null, argTypes?: strin ["mono_wasm_get_type_aqn", "string", ["number"]], // MONO.diagnostics - ["mono_wasm_event_pipe_enable", "bool", ["string", "number", "string", "bool", "number"]], + ["mono_wasm_event_pipe_enable", "bool", ["string", "number", "number", "string", "bool", "number"]], ["mono_wasm_event_pipe_session_start_streaming", "bool", ["number"]], ["mono_wasm_event_pipe_session_disable", "bool", ["number"]], ["mono_wasm_diagnostic_server_create_thread", "bool", ["string", "number"]], + ["mono_wasm_diagnostic_server_thread_attach_to_runtime", "void", []], + ["mono_wasm_diagnostic_server_post_resume_runtime", "void", []], + ["mono_wasm_diagnostic_server_create_stream", "number", []], //DOTNET ["mono_wasm_string_from_js", "number", ["string"]], @@ -170,10 +173,13 @@ export interface t_Cwraps { mono_wasm_obj_array_set(array: MonoArray, idx: number, obj: MonoObject): void; // MONO.diagnostics - mono_wasm_event_pipe_enable(outputPath: string, bufferSizeInMB: number, providers: string, rundownRequested: boolean, outSessionId: VoidPtr): boolean; + mono_wasm_event_pipe_enable(outputPath: string | null, stream: VoidPtr, bufferSizeInMB: number, providers: string, rundownRequested: boolean, outSessionId: VoidPtr): boolean; mono_wasm_event_pipe_session_start_streaming(sessionId: number): boolean; mono_wasm_event_pipe_session_disable(sessionId: number): boolean; mono_wasm_diagnostic_server_create_thread(websocketURL: string, threadIdOutPtr: VoidPtr): boolean; + mono_wasm_diagnostic_server_thread_attach_to_runtime(): void; + mono_wasm_diagnostic_server_post_resume_runtime(): void; + mono_wasm_diagnostic_server_create_stream(): VoidPtr; //DOTNET /** diff --git a/src/mono/wasm/runtime/diagnostics/index.ts b/src/mono/wasm/runtime/diagnostics/index.ts index 8256e3ddfc9528..a4f2ef9f3f45d2 100644 --- a/src/mono/wasm/runtime/diagnostics/index.ts +++ b/src/mono/wasm/runtime/diagnostics/index.ts @@ -115,7 +115,7 @@ function createSessionWithPtrCB(sessionIdOutPtr: VoidPtr, options: EventPipeSess // TODO: if options.message_port, create a streaming session instead of a file session memory.setI32(sessionIdOutPtr, 0); - if (!cwraps.mono_wasm_event_pipe_enable(tracePath, defaultBufferSizeInMB, providers, rundown, sessionIdOutPtr)) { + if (!cwraps.mono_wasm_event_pipe_enable(tracePath, 0 as unknown as VoidPtr, defaultBufferSizeInMB, providers, rundown, sessionIdOutPtr)) { return false; } else { return memory.getI32(sessionIdOutPtr); @@ -209,7 +209,7 @@ export async function mono_wasm_init_diagnostics(options: DiagnosticOptions): Pr throw new Error("server.connect_url must be a string"); } const url = options.server.connect_url; - const suspend = options.server?.suspend ?? false; + const suspend = boolsyOption(options.server.suspend); const controller = await startDiagnosticServer(url); if (controller) { if (suspend) { @@ -224,15 +224,24 @@ export async function mono_wasm_init_diagnostics(options: DiagnosticOptions): Pr startup_session_configs.push(...sessions); } -export function mono_wasm_diagnostic_server_on_runtime_server_init(): void { +function boolsyOption(x: string | boolean): boolean { + if (x === true || x === false) + return x; + if (typeof x === "string") { + if (x === "true") + return true; + if (x === "false") + return false; + } + throw new Error(`invalid option: "${x}", should be true, false, or "true" or "false"`); +} + +export function mono_wasm_diagnostic_server_on_runtime_server_init(out_options: VoidPtr): void { + /* called on the main thread when the runtime is sufficiently initialized */ const controller = getController(); controller.postServerAttachToRuntime(); - if (suspendOnStartup) { - /* FIXME: this is a hack. we should just use a condition variable in native. */ - for (let i = 0; i < 10000; ++i) { - (Module)["_emscripten_main_thread_process_queued_calls"](); - } - } + // FIXME: is this really the best place to do this? + memory.setI32(out_options, suspendOnStartup ? 1 : 0); } export default diagnostics; diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts index 4cee973604b59c..2a2c115fc5b494 100644 --- a/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts @@ -5,12 +5,13 @@ import { assertNever } from "../../types"; import { pthread_self } from "../../pthreads/worker"; +import * as memory from "../../memory"; import { Module } from "../../imports"; +import cwraps from "../../cwraps"; import { EventPipeSessionIDImpl, isDiagnosticMessage } from "../shared/types"; import { CharPtr, VoidPtr } from "../../types/emscripten"; import { DiagnosticServerControlCommand, - makeDiagnosticServerControlReplyStartupResume } from "../shared/controller-commands"; import { mockScript } from "./mock-remote"; @@ -28,6 +29,7 @@ import { isEventPipeCommandStopTracing, isProcessCommandResumeRuntime, EventPipeCommandCollectTracing2, + EventPipeCollectTracingCommandProvider, } from "./protocol-client-commands"; import parseMockCommand from "./mock-command-parser"; @@ -44,6 +46,8 @@ export interface DiagnosticServer { class DiagnosticServerImpl implements DiagnosticServer { readonly websocketUrl: string; + runtimeResumed = false; + constructor(websocketUrl: string) { this.websocketUrl = websocketUrl; pthread_self.addEventListenerFromBrowser(this.onMessageFromMainThread.bind(this)); @@ -65,12 +69,13 @@ class DiagnosticServerImpl implements DiagnosticServer { } attachToRuntime(): void { - // TODO: mono_wasm_diagnostic_server_thread_attach (); + cwraps.mono_wasm_diagnostic_server_thread_attach_to_runtime(); this.attachToRuntimeController.resolve(); } async serverLoop(this: DiagnosticServerImpl): Promise { await this.startRequestedController.promise; + await this.attachToRuntimeController.promise; // can't start tracing until we've attached to the runtime while (!this.stopRequested) { const p1: Promise<"first" | "second"> = this.advertiseAndWaitForClient().then(() => "first"); const p2: Promise<"first" | "second"> = this.stopRequestedController.promise.then(() => "second"); @@ -89,21 +94,26 @@ class DiagnosticServerImpl implements DiagnosticServer { async advertiseAndWaitForClient(): Promise { - const ws = mockScript.open(); - const p = addOneShotMessageEventListener(ws); - ws.send("ADVR_V1"); - const message = await p; - console.debug("received advertising response: ", message); - const cmd = this.parseCommand(message); - if (cmd === null) { - console.error("unexpected message from client", message); - return; - } else if (isEventPipeCommand(cmd)) { - await this.dispatchEventPipeCommand(ws, cmd); - } else if (isProcessCommand(cmd)) { - await this.dispatchProcessCommand(ws, cmd); // resume - } else { - console.warn("Client sent unknown command", cmd); + try { + const ws = mockScript.open(); + const p = addOneShotMessageEventListener(ws); + ws.send("ADVR_V1"); + const message = await p; + console.debug("received advertising response: ", message); + const cmd = this.parseCommand(message); + if (cmd === null) { + console.error("unexpected message from client", message); + return; + } else if (isEventPipeCommand(cmd)) { + await this.dispatchEventPipeCommand(ws, cmd); + } else if (isProcessCommand(cmd)) { + await this.dispatchProcessCommand(ws, cmd); // resume + } else { + console.warn("Client sent unknown command", cmd); + } + } finally { + // if there were errors, resume the runtime anyway + this.resumeRuntime(); } } @@ -145,9 +155,6 @@ class DiagnosticServerImpl implements DiagnosticServer { // dispatch EventPipe commands received from the diagnostic client async dispatchEventPipeCommand(ws: WebSocket | MockRemoteSocket, cmd: EventPipeClientCommandBase): Promise { if (isEventPipeCommandCollectTracing2(cmd)) { - // FIXME: if the runtime is waiting for us to resume them, we deadlock here - // the runtime is waiting for us to send a resume command, and we're waiting for eventpipe to send us a resume command, which it won't do until the eventpipe session is enabled. - await this.attachToRuntimeController.promise; // can't start tracing until we've attached to the runtime const session = await createEventPipeStreamingSession(ws, cmd); this.postClientReply(ws, "OK", session.sessionID); } else if (isEventPipeCommandStopTracing(cmd)) { @@ -169,11 +176,19 @@ class DiagnosticServerImpl implements DiagnosticServer { // dispatch Process commands received from the diagnostic client async dispatchProcessCommand(ws: WebSocket | MockRemoteSocket, cmd: ProcessClientCommandBase): Promise { if (isProcessCommandResumeRuntime(cmd)) { - pthread_self.postMessageToBrowser(makeDiagnosticServerControlReplyStartupResume()); + this.resumeRuntime(); } else { console.warn("unknown Process command", cmd); } } + + resumeRuntime(): void { + if (!this.runtimeResumed) { + console.debug("resuming runtime startup"); + cwraps.mono_wasm_diagnostic_server_post_resume_runtime(); + this.runtimeResumed = true; + } + } } class EventPipeStreamingSession { @@ -184,31 +199,57 @@ class EventPipeStreamingSession { async function createEventPipeStreamingSession(ws: WebSocket | MockRemoteSocket, cmd: EventPipeCommandCollectTracing2): Promise { // First, create the native IPC stream and get its queue. - const ipcStreamAddr = mono_wasm_diagnostic_server_create_stream(); // FIXME: this should be a wrapped in a JS object so we can free it when we're done. + const ipcStreamAddr = cwraps.mono_wasm_diagnostic_server_create_stream(); // FIXME: this should be a wrapped in a JS object so we can free it when we're done. const queueAddr = mono_wasm_diagnostic_server_get_stream_queue(ipcStreamAddr); // then take over the websocket connection const conn = takeOverSocket(ws); // and set up queue notifications const queue = allocateQueue(queueAddr, conn.write.bind(conn)); // create the event pipe session - const sessionID = mono_wasm_event_pipe_stream_session_enable(ipcStreamAddr, cmd); + const sessionID = createEventPipeStreamingSessionNative(ipcStreamAddr, cmd); + if (sessionID === null) + throw new Error("failed to create event pipe session"); return new EventPipeStreamingSession(sessionID, ws, queue, conn); } -function mono_wasm_diagnostic_server_create_stream(): VoidPtr { - // this shoudl be in C and it should jsut allocate one of our IPC streams - throw new Error("TODO"); +function createEventPipeStreamingSessionNative(ipcStreamAddr: VoidPtr, options: EventPipeCommandCollectTracing2): EventPipeSessionIDImpl | null { + + const sizeOfInt32 = 4; + + const success = memory.withStackAlloc(sizeOfInt32, createStreamingSessionWithPtrCB, options, ipcStreamAddr); + + if (success === false) + return null; + const sessionID = success; + + return sessionID; } -function mono_wasm_diagnostic_server_get_stream_queue(streamAddr: VoidPtr): VoidPtr { - // TODO: this can probably be in JS if we put the queue at a known address in the stream. (probably offset 0); - return streamAddr; +function createStreamingSessionWithPtrCB(sessionIdOutPtr: VoidPtr, options: EventPipeCommandCollectTracing2, ipcStreamAddr: VoidPtr): false | number { + const providers = providersStringFromObject(options.providers); + memory.setI32(sessionIdOutPtr, 0); + if (!cwraps.mono_wasm_event_pipe_enable(null, ipcStreamAddr, options.circularBufferMB, providers, options.requestRundown, sessionIdOutPtr)) { + return false; + } else { + return memory.getI32(sessionIdOutPtr); + } +} + +function providersStringFromObject(providers: EventPipeCollectTracingCommandProvider[]) { + const providersString = providers.map(providerToString).join(","); + return providersString; + + function providerToString(provider: EventPipeCollectTracingCommandProvider): string { + const keyword_str = provider.keywords === 0 ? "" : provider.keywords.toString(); + const args_str = provider.filter_data === "" ? "" : ":" + provider.filter_data; + return provider.provider_name + ":" + keyword_str + ":" + provider.logLevel + args_str; + } } -function mono_wasm_event_pipe_stream_session_enable(ipcStreamAddr: VoidPtr, cmd: EventPipeCommandCollectTracing2): EventPipeSessionIDImpl { - // this should be implemented in C. and it should call ep_enable. - console.debug("mono_wasm_event_pipe_stream_session_enable", ipcStreamAddr, cmd); - throw new Error("TODO"); +const IPC_STREAM_QUEUE_OFFSET = 4; /* keep in sync with mono_wasm_diagnostic_server_create_stream() in C */ +function mono_wasm_diagnostic_server_get_stream_queue(streamAddr: VoidPtr): VoidPtr { + // TODO: this can probably be in JS if we put the queue at a known address in the stream. (probably offset 4); + return streamAddr + IPC_STREAM_QUEUE_OFFSET; } /// Called by the runtime to initialize the diagnostic server workers diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/stream-queue.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/stream-queue.ts index ea0f32d4e93366..2ada6c44482477 100644 --- a/src/mono/wasm/runtime/diagnostics/server_pthread/stream-queue.ts +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/stream-queue.ts @@ -96,7 +96,7 @@ export function closeQueue(nativeQueueAddr: VoidPtr): void { } // called from native code on the diagnostic thread by queueing a call from the streaming thread. -export function mono_wasm_event_pipe_stream_signal_work_available(nativeQueueAddr: VoidPtr): void { +export function mono_wasm_diagnostic_server_stream_signal_work_available(nativeQueueAddr: VoidPtr): void { const queue = streamQueueMap.get(nativeQueueAddr); if (queue) { queue.wakeup(); diff --git a/src/mono/wasm/runtime/diagnostics/shared/controller-commands.ts b/src/mono/wasm/runtime/diagnostics/shared/controller-commands.ts index 97b8745ebb0032..3f03c746b81c03 100644 --- a/src/mono/wasm/runtime/diagnostics/shared/controller-commands.ts +++ b/src/mono/wasm/runtime/diagnostics/shared/controller-commands.ts @@ -24,18 +24,3 @@ export function makeDiagnosticServerControlCommand Date: Fri, 8 Jul 2022 22:51:17 -0400 Subject: [PATCH 38/84] WIP: starting to stream works; needs PTHREAD_POOL_SIZE bump Looks like we can send the initial nettrace header some events. We're starting more threads, so we need a bigger thread pool. Also PTHREAD_POOL_SIZE_STRICT=1 (the default - warn if worker pool needs to grow, but still try to grow it) seems to deadlock the browser-eventpipe sample. Set PTHREAD_POOL_SIZE_STRICT=2 (don't try to allocate a worker, make pthread_create fail with EAGAIN) instead so we get some kind of exception instead in other circumstances. Set the pool size to 4. --- src/mono/mono/component/diagnostics_server.c | 58 ++++++++++++++++--- .../diagnostics/server_pthread/index.ts | 4 +- .../server_pthread/stream-queue.ts | 27 ++++++--- src/mono/wasm/wasm.proj | 3 +- 4 files changed, 73 insertions(+), 19 deletions(-) diff --git a/src/mono/mono/component/diagnostics_server.c b/src/mono/mono/component/diagnostics_server.c index 0f1e50a32ff94e..b212460052dd6c 100644 --- a/src/mono/mono/component/diagnostics_server.c +++ b/src/mono/mono/component/diagnostics_server.c @@ -204,17 +204,24 @@ mono_wasm_diagnostic_server_post_resume_runtime (void) typedef struct WasmIpcStreamQueue { uint8_t *buf; int32_t count; - volatile int32_t write_done; + volatile int32_t buf_full; } WasmIpcStreamQueue; extern void -mono_wasm_diagnostic_server_stream_signal_work_available (WasmIpcStreamQueue *queue); +mono_wasm_diagnostic_server_stream_signal_work_available (WasmIpcStreamQueue *queue, int32_t current_thread); static void queue_wake_reader (void *ptr) { /* asynchronously invoked on the ds server thread by the writer. */ WasmIpcStreamQueue *q = (WasmIpcStreamQueue *)ptr; - mono_wasm_diagnostic_server_stream_signal_work_available (q); + mono_wasm_diagnostic_server_stream_signal_work_available (q, 0); +} + +static void +queue_wake_reader_now (WasmIpcStreamQueue *q) +{ + // call only from the diagnostic server thread! + mono_wasm_diagnostic_server_stream_signal_work_available (q, 1); } static int32_t @@ -224,14 +231,41 @@ queue_push_sync (WasmIpcStreamQueue *q, const uint8_t *buf, uint32_t buf_size, u /* single-writer, so there is no write contention */ q->buf = (uint8_t*)buf; q->count = buf_size; - emscripten_dispatch_to_thread (ds_thread_id, EM_FUNC_SIG_VI, NULL, queue_wake_reader, q); + /* there's one instance where a thread other than the + * streaming thread is writing: in ep_file_initialize_file + * (called from ep_session_start_streaming), there's a write + * from either the main thread (if the streaming was deferred + * until ep_finish_init is called) or the diagnostic thread if + * the session is started later. + */ + pthread_t cur = pthread_self (); + gboolean will_wait = TRUE; + mono_atomic_store_i32 (&q->buf_full, 1); + if (cur == ds_thread_id) { + queue_wake_reader_now (q); + /* doesn't return until the buffer is empty again; no need to wait */ + will_wait = FALSE; + } else { + emscripten_dispatch_to_thread (ds_thread_id, EM_FUNC_SIG_VI, &queue_wake_reader, NULL, q); + } // wait until the reader reads the value - int r = mono_wasm_atomic_wait_i32 (&q->write_done, 0, -1); - if (G_UNLIKELY (r != 0)) { - return -1; + int r = 0; + if (G_LIKELY (will_wait)) { + while (mono_atomic_load_i32 (&q->buf_full) != 0) { + if (G_UNLIKELY (mono_threads_wasm_is_browser_thread ())) { + /* can't use memory.atomic.wait32 on the main thread, spin instead */ + /* this lets Emscripten run queued calls on the main thread */ + emscripten_thread_sleep (1); + } else { + r = mono_wasm_atomic_wait_i32 (&q->buf_full, 1, -1); + if (G_UNLIKELY (r == 2)) { + /* timed out with infinite wait?? */ + return -1; + } + /* if r == 0 (blocked and woken) or r == 1 (not equal), go around again and check if buf_full is now 0 */ + } + } } - if (mono_atomic_load_i32 (&q->write_done) != 0) - return -1; if (bytes_written) *bytes_written = buf_size; return 0; @@ -284,6 +318,9 @@ wasm_ipc_stream_read (void *self, uint8_t *buffer, uint32_t bytes_to_read, uint3 static bool wasm_ipc_stream_write (void *self, const uint8_t *buffer, uint32_t bytes_to_write, uint32_t *bytes_written, uint32_t timeout_ms) { + EM_ASM({ + console.log ("wasm_ipc_stream_write"); + }); WasmIpcStream *stream = (WasmIpcStream *)self; g_assert (timeout_ms == EP_INFINITE_WAIT); // pass it down to the queue if the timeout param starts being used int r = queue_push_sync (&stream->queue, buffer, bytes_to_write, bytes_written); @@ -293,6 +330,9 @@ wasm_ipc_stream_write (void *self, const uint8_t *buffer, uint32_t bytes_to_writ static bool wasm_ipc_stream_flush (void *self) { + EM_ASM({ + console.log ("wasm_ipc_stream_flush"); + }); return true; } static bool diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts index 2a2c115fc5b494..105bc5fd50f8c2 100644 --- a/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts @@ -157,6 +157,8 @@ class DiagnosticServerImpl implements DiagnosticServer { if (isEventPipeCommandCollectTracing2(cmd)) { const session = await createEventPipeStreamingSession(ws, cmd); this.postClientReply(ws, "OK", session.sessionID); + console.debug("created session, now streaming: ", session); + cwraps.mono_wasm_event_pipe_session_start_streaming(session.sessionID); } else if (isEventPipeCommandStopTracing(cmd)) { await this.stopEventPipe(cmd.sessionID); } else { @@ -169,8 +171,8 @@ class DiagnosticServerImpl implements DiagnosticServer { } async stopEventPipe(sessionID: EventPipeSessionIDImpl): Promise { - /* TODO: finish me */ console.debug("stopEventPipe", sessionID); + cwraps.mono_wasm_event_pipe_session_disable(sessionID); } // dispatch Process commands received from the diagnostic client diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/stream-queue.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/stream-queue.ts index 2ada6c44482477..40885a3cdc834f 100644 --- a/src/mono/wasm/runtime/diagnostics/server_pthread/stream-queue.ts +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/stream-queue.ts @@ -49,13 +49,13 @@ export class StreamQueue { this.workAvailable.addEventListener("workAvailable", this.onWorkAvailable.bind(this)); } - private get wakeup_write_addr(): VoidPtr { + private get buf_addr(): VoidPtr { return this.queue_addr + BUF_OFFSET; } private get count_addr(): VoidPtr { return this.queue_addr + COUNT_OFFSET; } - private get wakeup_write_done_addr(): VoidPtr { + private get buf_full_addr(): VoidPtr { return this.queue_addr + WRITE_DONE_OFFSET; } @@ -65,19 +65,26 @@ export class StreamQueue { queueMicrotask(this.signalWorkAvailable); } + workAvailableNow(): void { + // process the queue immediately, rather than waiting for the next event loop tick. + this.onWorkAvailable(); + } + private signalWorkAvailableImpl(this: StreamQueue): void { this.workAvailable.dispatchEvent(new Event("workAvailable")); } private onWorkAvailable(this: StreamQueue /*,event: Event */): void { - const buf = Memory.getI32(this.wakeup_write_addr) as unknown as VoidPtr; + const buf = Memory.getI32(this.buf_addr) as unknown as VoidPtr; const count = Memory.getI32(this.count_addr); - Memory.setI32(this.wakeup_write_addr, 0); + Memory.setI32(this.buf_addr, 0); if (count > 0) { this.syncSendBuffer(buf, count); } - Memory.Atomics.storeI32(this.wakeup_write_done_addr, 0); - Memory.Atomics.notifyI32(this.wakeup_write_done_addr, 1); + /* buffer is now not full */ + Memory.Atomics.storeI32(this.buf_full_addr, 0); + /* wake up the writer thread */ + Memory.Atomics.notifyI32(this.buf_full_addr, 1); } } @@ -96,9 +103,13 @@ export function closeQueue(nativeQueueAddr: VoidPtr): void { } // called from native code on the diagnostic thread by queueing a call from the streaming thread. -export function mono_wasm_diagnostic_server_stream_signal_work_available(nativeQueueAddr: VoidPtr): void { +export function mono_wasm_diagnostic_server_stream_signal_work_available(nativeQueueAddr: VoidPtr, current_thread: number): void { const queue = streamQueueMap.get(nativeQueueAddr); if (queue) { - queue.wakeup(); + if (current_thread === 0) { + queue.wakeup(); + } else { + queue.workAvailableNow(); + } } } diff --git a/src/mono/wasm/wasm.proj b/src/mono/wasm/wasm.proj index b9caef72ceb794..2fce384d45e358 100644 --- a/src/mono/wasm/wasm.proj +++ b/src/mono/wasm/wasm.proj @@ -124,7 +124,8 @@ <_EmccCommonFlags Condition="'$(WasmEnableSIMD)' == 'true'" Include="-msimd128" /> <_EmccCommonFlags Condition="'$(MonoWasmThreads)' == 'true'" Include="-s USE_PTHREADS=1" /> <_EmccLinkFlags Condition="'$(MonoWasmThreads)' == 'true'" Include="-Wno-pthreads-mem-growth" /> - <_EmccLinkFlags Condition="'$(MonoWasmThreads)' == 'true'" Include="-s PTHREAD_POOL_SIZE=2" /> + <_EmccLinkFlags Condition="'$(MonoWasmThreads)' == 'true'" Include="-s PTHREAD_POOL_SIZE=4" /> + <_EmccLinkFlags Condition="'$(MonoWasmThreads)' == 'true'" Include="-s PTHREAD_POOL_SIZE_STRICT=2" /> From 50e035c27a5cd54a755374746ee3ac2494d6af5b Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Sun, 10 Jul 2022 00:05:00 -0400 Subject: [PATCH 39/84] cleanup browser-eventpipe sample --- .../sample/wasm/browser-eventpipe/index.html | 3 -- .../sample/wasm/browser-eventpipe/main.js | 28 ------------------- 2 files changed, 31 deletions(-) diff --git a/src/mono/sample/wasm/browser-eventpipe/index.html b/src/mono/sample/wasm/browser-eventpipe/index.html index 607e16650c3dc5..dd13b5fb6c661f 100644 --- a/src/mono/sample/wasm/browser-eventpipe/index.html +++ b/src/mono/sample/wasm/browser-eventpipe/index.html @@ -18,9 +18,6 @@
Computing Fib(N) repeatedly: -
- -
diff --git a/src/mono/sample/wasm/browser-eventpipe/main.js b/src/mono/sample/wasm/browser-eventpipe/main.js index 7500c7ac57a2bd..294542be6775fe 100644 --- a/src/mono/sample/wasm/browser-eventpipe/main.js +++ b/src/mono/sample/wasm/browser-eventpipe/main.js @@ -56,13 +56,6 @@ async function doWork(startWork, stopWork, getIterationsDone) { function getOnClickHandler(startWork, stopWork, getIterationsDone) { return async function () { - // const options = MONO.diagnostics.SessionOptionsBuilder - // .Empty - // .setRundownEnabled(false) - // .addProvider({ name: 'WasmHello', level: MONO.diagnostics.EventLevel.Verbose, args: 'EventCounterIntervalSec=1' }) - // .build(); - // console.log('starting providers', options.providers); - let sessions = MONO.diagnostics.getStartupSessions(); if (typeof (sessions) !== "object" || sessions.length === "undefined" || sessions.length == 0) @@ -73,9 +66,6 @@ function getOnClickHandler(startWork, stopWork, getIterationsDone) { console.debug("eventSession state is ", eventSession._state); // ooh protected member access - // const eventSession = MONO.diagnostics.createEventPipeSession(options); - - // eventSession.start(); const ret = await doWork(startWork, stopWork, getIterationsDone); @@ -90,25 +80,7 @@ function getOnClickHandler(startWork, stopWork, getIterationsDone) { } } -function websocketTestThing() { - console.log("websocketTestThing opening a connection"); - const ws = new WebSocket("ws://localhost:9090/diagnostics"); - ws.onopen = function () { - ws.send("hello from browser"); - ws.onmessage = function (event) { - console.log("got message from server: ", event.data); - ws.close(); - } - } - ws.onerror = function (event) { - console.log("error from server: ", event); - } -} - async function main() { - const wsbtn = document.getElementById("openWS"); - wsbtn.onclick = websocketTestThing; - const { MONO, BINDING, Module, RuntimeBuildInfo } = await createDotnetRuntime(() => { return { disableDotnet6Compatibility: true, From 69d0ca79f14718fa64427fb7a4f25cb0640d0bb0 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Sun, 10 Jul 2022 21:56:30 -0400 Subject: [PATCH 40/84] refactor to simplify and cleanup; rm duplicate code --- .../runtime/diagnostics/browser/controller.ts | 12 +- .../diagnostics/browser/file-session.ts | 109 ++++++++++++++ .../{ => browser}/session-options-builder.ts | 2 +- src/mono/wasm/runtime/diagnostics/index.ts | 133 +----------------- .../diagnostics/server_pthread/index.ts | 71 +--------- .../{event_pipe.ts => socket-connection.ts} | 0 .../server_pthread/streaming-session.ts | 58 ++++++++ .../diagnostics/shared/create-session.ts | 52 +++++++ src/mono/wasm/runtime/dotnet.d.ts | 2 +- 9 files changed, 230 insertions(+), 209 deletions(-) create mode 100644 src/mono/wasm/runtime/diagnostics/browser/file-session.ts rename src/mono/wasm/runtime/diagnostics/{ => browser}/session-options-builder.ts (98%) rename src/mono/wasm/runtime/diagnostics/server_pthread/{event_pipe.ts => socket-connection.ts} (100%) create mode 100644 src/mono/wasm/runtime/diagnostics/server_pthread/streaming-session.ts create mode 100644 src/mono/wasm/runtime/diagnostics/shared/create-session.ts diff --git a/src/mono/wasm/runtime/diagnostics/browser/controller.ts b/src/mono/wasm/runtime/diagnostics/browser/controller.ts index 696d2429847875..ff453bb20bcae0 100644 --- a/src/mono/wasm/runtime/diagnostics/browser/controller.ts +++ b/src/mono/wasm/runtime/diagnostics/browser/controller.ts @@ -3,19 +3,16 @@ import cwraps from "../../cwraps"; import { withStackAlloc, getI32 } from "../../memory"; -import { PromiseController } from "../../promise-utils"; import { Thread, waitForThread } from "../../pthreads/browser"; import { makeDiagnosticServerControlCommand } from "../shared/controller-commands"; import { isDiagnosticMessage } from "../shared/types"; /// An object that can be used to control the diagnostic server. export interface ServerController { - waitForStartupResume(): Promise; postServerAttachToRuntime(): void; } class ServerControllerImpl implements ServerController { - private readonly startupResumePromise: PromiseController = new PromiseController(); constructor(private server: Thread) { server.port.addEventListener("message", this.onServerReply.bind(this)); } @@ -27,9 +24,6 @@ class ServerControllerImpl implements ServerController { console.debug("signaling the diagnostic server to stop"); this.server.postMessageToWorker(makeDiagnosticServerControlCommand("stop")); } - async waitForStartupResume(): Promise { - await this.startupResumePromise.promise; - } postServerAttachToRuntime(): void { console.debug("signal the diagnostic server to attach to the runtime"); this.server.postMessageToWorker(makeDiagnosticServerControlCommand("attach_to_runtime")); @@ -39,12 +33,8 @@ class ServerControllerImpl implements ServerController { const d = event.data; if (isDiagnosticMessage(d)) { switch (d.cmd) { - case "startup_resume": - console.debug("diagnostic server startup resume"); - this.startupResumePromise.resolve(); - break; default: - console.warn("Unknown control command: ", d); + console.warn("Unknown control reply command: ", d); break; } } diff --git a/src/mono/wasm/runtime/diagnostics/browser/file-session.ts b/src/mono/wasm/runtime/diagnostics/browser/file-session.ts new file mode 100644 index 00000000000000..361f27479fb987 --- /dev/null +++ b/src/mono/wasm/runtime/diagnostics/browser/file-session.ts @@ -0,0 +1,109 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// An EventPipe session object represents a single diagnostic tracing session that is collecting +/// events from the runtime and managed libraries. There may be multiple active sessions at the same time. +/// Each session subscribes to a number of providers and will collect events from the time that start() is called, until stop() is called. +/// Upon completion the session saves the events to a file on the VFS. +/// The data can then be retrieved as Blob. +import { EventPipeSessionID, EventPipeSessionOptions } from "../../types"; +import { EventPipeSessionIDImpl } from "../shared/types"; +import { createEventPipeFileSession } from "../shared/create-session"; +import { Module } from "../../imports"; +import cwraps from "../../cwraps"; + +export interface EventPipeSession { + // session ID for debugging logging only + get sessionID(): EventPipeSessionID; + start(): void; + stop(): void; + getTraceBlob(): Blob; +} + +// internal session state of the JS instance +enum State { + Initialized, + Started, + Done, +} + +/// An EventPipe session that saves the event data to a file in the VFS. +class EventPipeFileSession implements EventPipeSession { + protected _state: State; + private _sessionID: EventPipeSessionIDImpl; + private _tracePath: string; // VFS file path to the trace file + + get sessionID(): bigint { return BigInt(this._sessionID); } + + constructor(sessionID: EventPipeSessionIDImpl, tracePath: string) { + this._state = State.Initialized; + this._sessionID = sessionID; + this._tracePath = tracePath; + console.debug(`EventPipe session ${this.sessionID} created`); + } + + start = () => { + if (this._state !== State.Initialized) { + throw new Error(`EventPipe session ${this.sessionID} already started`); + } + this._state = State.Started; + start_streaming(this._sessionID); + console.debug(`EventPipe session ${this.sessionID} started`); + } + + stop = () => { + if (this._state !== State.Started) { + throw new Error(`cannot stop an EventPipe session in state ${this._state}, not 'Started'`); + } + this._state = State.Done; + stop_streaming(this._sessionID); + console.debug(`EventPipe session ${this.sessionID} stopped`); + } + + getTraceBlob = () => { + if (this._state !== State.Done) { + throw new Error(`session is in state ${this._state}, not 'Done'`); + } + const data = Module.FS_readFile(this._tracePath, { encoding: "binary" }) as Uint8Array; + return new Blob([data], { type: "application/octet-stream" }); + } +} + +function start_streaming(sessionID: EventPipeSessionIDImpl): void { + cwraps.mono_wasm_event_pipe_session_start_streaming(sessionID); +} + +function stop_streaming(sessionID: EventPipeSessionIDImpl): void { + cwraps.mono_wasm_event_pipe_session_disable(sessionID); +} + +// a conter for the number of sessions created +let totalSessions = 0; + +export function makeEventPipeSession(options?: EventPipeSessionOptions): EventPipeSession | null { + const defaultRundownRequested = true; + const defaultProviders = ""; // empty string means use the default providers + const defaultBufferSizeInMB = 1; + + const rundown = options?.collectRundownEvents ?? defaultRundownRequested; + const providers = options?.providers ?? defaultProviders; + + // The session trace is saved to a file in the VFS. The file name doesn't matter, + // but we'd like it to be distinct from other traces. + const tracePath = `/trace-${totalSessions++}.nettrace`; + + const sessionOptions = { + rundownRequested: rundown, + providers: providers, + bufferSizeInMB: defaultBufferSizeInMB, + }; + + const success = createEventPipeFileSession(tracePath, sessionOptions); + + if (success === false) + return null; + const sessionID = success; + + return new EventPipeFileSession(sessionID, tracePath); +} + + diff --git a/src/mono/wasm/runtime/diagnostics/session-options-builder.ts b/src/mono/wasm/runtime/diagnostics/browser/session-options-builder.ts similarity index 98% rename from src/mono/wasm/runtime/diagnostics/session-options-builder.ts rename to src/mono/wasm/runtime/diagnostics/browser/session-options-builder.ts index 5092bd57416f82..745c446ac8f3ea 100644 --- a/src/mono/wasm/runtime/diagnostics/session-options-builder.ts +++ b/src/mono/wasm/runtime/diagnostics/browser/session-options-builder.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 { EventPipeSessionOptions } from "../types"; +import { EventPipeSessionOptions } from "../../types"; export const eventLevel = { LogAlways: 0, diff --git a/src/mono/wasm/runtime/diagnostics/index.ts b/src/mono/wasm/runtime/diagnostics/index.ts index a4f2ef9f3f45d2..50f8ef6f4ea618 100644 --- a/src/mono/wasm/runtime/diagnostics/index.ts +++ b/src/mono/wasm/runtime/diagnostics/index.ts @@ -1,126 +1,21 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -import { Module } from "../imports"; -import cwraps from "../cwraps"; import type { DiagnosticOptions, EventPipeSessionOptions, - EventPipeSessionID, } from "../types"; import { is_nullish } from "../types"; import type { VoidPtr } from "../types/emscripten"; import { getController, startDiagnosticServer } from "./browser/controller"; import * as memory from "../memory"; -export type { ProviderConfiguration } from "./session-options-builder"; +export type { ProviderConfiguration } from "./browser/session-options-builder"; import { eventLevel, EventLevel, SessionOptionsBuilder, -} from "./session-options-builder"; - -const sizeOfInt32 = 4; - -type EventPipeSessionIDImpl = number; - -// An EventPipe session object represents a single diagnostic tracing session that is collecting -/// events from the runtime and managed libraries. There may be multiple active sessions at the same time. -/// Each session subscribes to a number of providers and will collect events from the time that start() is called, until stop() is called. -/// Upon completion the session saves the events to a file on the VFS. -/// The data can then be retrieved as Blob. -export interface EventPipeSession { - // session ID for debugging logging only - get sessionID(): EventPipeSessionID; - isIPCStreamingSession(): boolean; - start(): void; - stop(): void; - getTraceBlob(): Blob; -} - - -// internal session state of the JS instance -enum State { - Initialized, - Started, - Done, -} - -function start_streaming(sessionID: EventPipeSessionIDImpl): void { - cwraps.mono_wasm_event_pipe_session_start_streaming(sessionID); -} - -function stop_streaming(sessionID: EventPipeSessionIDImpl): void { - cwraps.mono_wasm_event_pipe_session_disable(sessionID); -} - -abstract class EventPipeSessionBase { - isIPCStreamingSession() { return false; } -} - - -/// An EventPipe session that saves the event data to a file in the VFS. -class EventPipeFileSession extends EventPipeSessionBase implements EventPipeSession { - protected _state: State; - private _sessionID: EventPipeSessionIDImpl; - private _tracePath: string; // VFS file path to the trace file - - get sessionID(): bigint { return BigInt(this._sessionID); } - - constructor(sessionID: EventPipeSessionIDImpl, tracePath: string) { - super(); - this._state = State.Initialized; - this._sessionID = sessionID; - this._tracePath = tracePath; - console.debug(`EventPipe session ${this.sessionID} created`); - } - - start = () => { - if (this._state !== State.Initialized) { - throw new Error(`EventPipe session ${this.sessionID} already started`); - } - this._state = State.Started; - start_streaming(this._sessionID); - console.debug(`EventPipe session ${this.sessionID} started`); - } - - stop = () => { - if (this._state !== State.Started) { - throw new Error(`cannot stop an EventPipe session in state ${this._state}, not 'Started'`); - } - this._state = State.Done; - stop_streaming(this._sessionID); - console.debug(`EventPipe session ${this.sessionID} stopped`); - } - - getTraceBlob = () => { - if (this._state !== State.Done) { - throw new Error(`session is in state ${this._state}, not 'Done'`); - } - const data = Module.FS_readFile(this._tracePath, { encoding: "binary" }) as Uint8Array; - return new Blob([data], { type: "application/octet-stream" }); - } -} - -// a conter for the number of sessions created -let totalSessions = 0; - -function createSessionWithPtrCB(sessionIdOutPtr: VoidPtr, options: EventPipeSessionOptions, tracePath: string): false | number { - const defaultRundownRequested = true; - const defaultProviders = ""; // empty string means use the default providers - const defaultBufferSizeInMB = 1; - - const rundown = options?.collectRundownEvents ?? defaultRundownRequested; - const providers = options?.providers ?? defaultProviders; - - // TODO: if options.message_port, create a streaming session instead of a file session - - memory.setI32(sessionIdOutPtr, 0); - if (!cwraps.mono_wasm_event_pipe_enable(tracePath, 0 as unknown as VoidPtr, defaultBufferSizeInMB, providers, rundown, sessionIdOutPtr)) { - return false; - } else { - return memory.getI32(sessionIdOutPtr); - } -} +} from "./browser/session-options-builder"; +import { EventPipeSession, makeEventPipeSession } from "./browser/file-session"; export interface Diagnostics { EventLevel: EventLevel; @@ -130,7 +25,6 @@ export interface Diagnostics { getStartupSessions(): (EventPipeSession | null)[]; } - let startup_session_configs: EventPipeSessionOptions[] = []; let startup_sessions: (EventPipeSession | null)[] | null = null; @@ -147,7 +41,7 @@ export function mono_wasm_event_pipe_early_startup_callback(): void { function createAndStartEventPipeSession(options: (EventPipeSessionOptions)): EventPipeSession | null { - const session = createEventPipeSession(options); + const session = makeEventPipeSession(options); if (session === null) { return null; } @@ -156,20 +50,6 @@ function createAndStartEventPipeSession(options: (EventPipeSessionOptions)): Eve return session; } -function createEventPipeSession(options?: EventPipeSessionOptions): EventPipeSession | null { - // The session trace is saved to a file in the VFS. The file name doesn't matter, - // but we'd like it to be distinct from other traces. - const tracePath = `/trace-${totalSessions++}.nettrace`; - - const success = memory.withStackAlloc(sizeOfInt32, createSessionWithPtrCB, options, tracePath); - - if (success === false) - return null; - const sessionID = success; - - return new EventPipeFileSession(sessionID, tracePath); -} - /// APIs for working with .NET diagnostics from JavaScript. export const diagnostics: Diagnostics = { /// An enumeration of the level (higher value means more detail): @@ -185,7 +65,7 @@ export const diagnostics: Diagnostics = { /// Creates a new EventPipe session that will collect trace events from the runtime and managed libraries. /// Use the options to control the kinds of events to be collected. /// Multiple sessions may be created and started at the same time. - createEventPipeSession: createEventPipeSession, + createEventPipeSession: makeEventPipeSession, getStartupSessions(): (EventPipeSession | null)[] { return Array.from(startup_sessions || []); }, @@ -214,9 +94,6 @@ export async function mono_wasm_init_diagnostics(options: DiagnosticOptions): Pr if (controller) { if (suspend) { suspendOnStartup = true; - //console.debug("waiting for the diagnostic server to resume us"); - //const response = await controller.waitForStartupResume(); - //console.debug("diagnostic server resumed us", response); } } } diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts index 105bc5fd50f8c2..67fface4e3e65c 100644 --- a/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts @@ -5,11 +5,10 @@ import { assertNever } from "../../types"; import { pthread_self } from "../../pthreads/worker"; -import * as memory from "../../memory"; import { Module } from "../../imports"; import cwraps from "../../cwraps"; import { EventPipeSessionIDImpl, isDiagnosticMessage } from "../shared/types"; -import { CharPtr, VoidPtr } from "../../types/emscripten"; +import { CharPtr } from "../../types/emscripten"; import { DiagnosticServerControlCommand, } from "../shared/controller-commands"; @@ -17,8 +16,6 @@ import { import { mockScript } from "./mock-remote"; import type { MockRemoteSocket } from "../mock"; import { PromiseController } from "../../promise-utils"; -import { EventPipeSocketConnection, takeOverSocket } from "./event_pipe"; -import { StreamQueue, allocateQueue } from "./stream-queue"; import { isEventPipeCommand, isProcessCommand, @@ -28,9 +25,8 @@ import { isEventPipeCommandCollectTracing2, isEventPipeCommandStopTracing, isProcessCommandResumeRuntime, - EventPipeCommandCollectTracing2, - EventPipeCollectTracingCommandProvider, } from "./protocol-client-commands"; +import { makeEventPipeStreamingSession } from "./streaming-session"; import parseMockCommand from "./mock-command-parser"; function addOneShotMessageEventListener(src: EventTarget): Promise> { @@ -155,7 +151,7 @@ class DiagnosticServerImpl implements DiagnosticServer { // dispatch EventPipe commands received from the diagnostic client async dispatchEventPipeCommand(ws: WebSocket | MockRemoteSocket, cmd: EventPipeClientCommandBase): Promise { if (isEventPipeCommandCollectTracing2(cmd)) { - const session = await createEventPipeStreamingSession(ws, cmd); + const session = await makeEventPipeStreamingSession(ws, cmd); this.postClientReply(ws, "OK", session.sessionID); console.debug("created session, now streaming: ", session); cwraps.mono_wasm_event_pipe_session_start_streaming(session.sessionID); @@ -193,67 +189,6 @@ class DiagnosticServerImpl implements DiagnosticServer { } } -class EventPipeStreamingSession { - - constructor(readonly sessionID: EventPipeSessionIDImpl, readonly ws: WebSocket | MockRemoteSocket, - readonly queue: StreamQueue, readonly connection: EventPipeSocketConnection) { } -} - -async function createEventPipeStreamingSession(ws: WebSocket | MockRemoteSocket, cmd: EventPipeCommandCollectTracing2): Promise { - // First, create the native IPC stream and get its queue. - const ipcStreamAddr = cwraps.mono_wasm_diagnostic_server_create_stream(); // FIXME: this should be a wrapped in a JS object so we can free it when we're done. - const queueAddr = mono_wasm_diagnostic_server_get_stream_queue(ipcStreamAddr); - // then take over the websocket connection - const conn = takeOverSocket(ws); - // and set up queue notifications - const queue = allocateQueue(queueAddr, conn.write.bind(conn)); - // create the event pipe session - const sessionID = createEventPipeStreamingSessionNative(ipcStreamAddr, cmd); - if (sessionID === null) - throw new Error("failed to create event pipe session"); - return new EventPipeStreamingSession(sessionID, ws, queue, conn); -} - -function createEventPipeStreamingSessionNative(ipcStreamAddr: VoidPtr, options: EventPipeCommandCollectTracing2): EventPipeSessionIDImpl | null { - - const sizeOfInt32 = 4; - - const success = memory.withStackAlloc(sizeOfInt32, createStreamingSessionWithPtrCB, options, ipcStreamAddr); - - if (success === false) - return null; - const sessionID = success; - - return sessionID; -} - -function createStreamingSessionWithPtrCB(sessionIdOutPtr: VoidPtr, options: EventPipeCommandCollectTracing2, ipcStreamAddr: VoidPtr): false | number { - const providers = providersStringFromObject(options.providers); - memory.setI32(sessionIdOutPtr, 0); - if (!cwraps.mono_wasm_event_pipe_enable(null, ipcStreamAddr, options.circularBufferMB, providers, options.requestRundown, sessionIdOutPtr)) { - return false; - } else { - return memory.getI32(sessionIdOutPtr); - } -} - -function providersStringFromObject(providers: EventPipeCollectTracingCommandProvider[]) { - const providersString = providers.map(providerToString).join(","); - return providersString; - - function providerToString(provider: EventPipeCollectTracingCommandProvider): string { - const keyword_str = provider.keywords === 0 ? "" : provider.keywords.toString(); - const args_str = provider.filter_data === "" ? "" : ":" + provider.filter_data; - return provider.provider_name + ":" + keyword_str + ":" + provider.logLevel + args_str; - } -} - -const IPC_STREAM_QUEUE_OFFSET = 4; /* keep in sync with mono_wasm_diagnostic_server_create_stream() in C */ -function mono_wasm_diagnostic_server_get_stream_queue(streamAddr: VoidPtr): VoidPtr { - // TODO: this can probably be in JS if we put the queue at a known address in the stream. (probably offset 4); - return streamAddr + IPC_STREAM_QUEUE_OFFSET; -} - /// Called by the runtime to initialize the diagnostic server workers export function mono_wasm_diagnostic_server_on_server_thread_created(websocketUrlPtr: CharPtr): void { const websocketUrl = Module.UTF8ToString(websocketUrlPtr); diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/event_pipe.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/socket-connection.ts similarity index 100% rename from src/mono/wasm/runtime/diagnostics/server_pthread/event_pipe.ts rename to src/mono/wasm/runtime/diagnostics/server_pthread/socket-connection.ts diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/streaming-session.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/streaming-session.ts new file mode 100644 index 00000000000000..72c503cdab2089 --- /dev/null +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/streaming-session.ts @@ -0,0 +1,58 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +import { + EventPipeSessionIDImpl +} from "../shared/types"; +import { EventPipeSocketConnection, takeOverSocket } from "./socket-connection"; +import { StreamQueue, allocateQueue } from "./stream-queue"; +import type { MockRemoteSocket } from "../mock"; +import type { VoidPtr } from "../../types/emscripten"; +import cwraps from "../../cwraps"; +import { + EventPipeCommandCollectTracing2, + EventPipeCollectTracingCommandProvider, +} from "./protocol-client-commands"; +import { createEventPipeStreamingSession } from "../shared/create-session"; + +export class EventPipeStreamingSession { + constructor(readonly sessionID: EventPipeSessionIDImpl, readonly ws: WebSocket | MockRemoteSocket, + readonly queue: StreamQueue, readonly connection: EventPipeSocketConnection) { } +} + +export async function makeEventPipeStreamingSession(ws: WebSocket | MockRemoteSocket, cmd: EventPipeCommandCollectTracing2): Promise { + // First, create the native IPC stream and get its queue. + const ipcStreamAddr = cwraps.mono_wasm_diagnostic_server_create_stream(); // FIXME: this should be a wrapped in a JS object so we can free it when we're done. + const queueAddr = mono_wasm_diagnostic_server_get_stream_queue(ipcStreamAddr); + // then take over the websocket connection + const conn = takeOverSocket(ws); + // and set up queue notifications + const queue = allocateQueue(queueAddr, conn.write.bind(conn)); + const options = { + rundownRequested: cmd.requestRundown, + bufferSizeInMB: cmd.circularBufferMB, + providers: providersStringFromObject(cmd.providers), + }; + // create the event pipe session + const sessionID = createEventPipeStreamingSession(ipcStreamAddr, options); + if (sessionID === false) + throw new Error("failed to create event pipe session"); + return new EventPipeStreamingSession(sessionID, ws, queue, conn); +} + + +function providersStringFromObject(providers: EventPipeCollectTracingCommandProvider[]) { + const providersString = providers.map(providerToString).join(","); + return providersString; + + function providerToString(provider: EventPipeCollectTracingCommandProvider): string { + const keyword_str = provider.keywords === 0 ? "" : provider.keywords.toString(); + const args_str = provider.filter_data === "" ? "" : ":" + provider.filter_data; + return provider.provider_name + ":" + keyword_str + ":" + provider.logLevel + args_str; + } +} + +const IPC_STREAM_QUEUE_OFFSET = 4; /* keep in sync with mono_wasm_diagnostic_server_create_stream() in C */ +function mono_wasm_diagnostic_server_get_stream_queue(streamAddr: VoidPtr): VoidPtr { + // TODO: this can probably be in JS if we put the queue at a known address in the stream. (probably offset 4); + return streamAddr + IPC_STREAM_QUEUE_OFFSET; +} diff --git a/src/mono/wasm/runtime/diagnostics/shared/create-session.ts b/src/mono/wasm/runtime/diagnostics/shared/create-session.ts new file mode 100644 index 00000000000000..7e9fa66ab7929c --- /dev/null +++ b/src/mono/wasm/runtime/diagnostics/shared/create-session.ts @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import * as memory from "../../memory"; +import { VoidPtr } from "../../types/emscripten"; +import cwraps from "../../cwraps"; +import type { EventPipeSessionIDImpl } from "./types"; + +const sizeOfInt32 = 4; + +export interface EventPipeCreateSessionOptions { + rundownRequested: boolean; + bufferSizeInMB: number; + providers: string; +} + +type SessionType = + { + type: "file"; + filePath: string + } + | { + type: "stream"; + stream: VoidPtr + }; + + +function createSessionWithPtrCB(sessionIdOutPtr: VoidPtr, options: EventPipeCreateSessionOptions, sessionType: SessionType): false | EventPipeSessionIDImpl { + memory.setI32(sessionIdOutPtr, 0); + let tracePath: string | null; + let ipcStreamAddr: VoidPtr; + if (sessionType.type === "file") { + tracePath = sessionType.filePath; + ipcStreamAddr = 0 as unknown as VoidPtr; + } else { + tracePath = null; + ipcStreamAddr = sessionType.stream; + } + if (!cwraps.mono_wasm_event_pipe_enable(tracePath, ipcStreamAddr, options.bufferSizeInMB, options.providers, options.rundownRequested, sessionIdOutPtr)) { + return false; + } else { + return memory.getI32(sessionIdOutPtr); + } +} + +export function createEventPipeStreamingSession(ipcStreamAddr: VoidPtr, options: EventPipeCreateSessionOptions): EventPipeSessionIDImpl | false { + return memory.withStackAlloc(sizeOfInt32, createSessionWithPtrCB, options, { type: "stream", stream: ipcStreamAddr }); +} + +export function createEventPipeFileSession(tracePath: string, options: EventPipeCreateSessionOptions): EventPipeSessionIDImpl | false { + return memory.withStackAlloc(sizeOfInt32, createSessionWithPtrCB, options, { type: "file", filePath: tracePath }); +} diff --git a/src/mono/wasm/runtime/dotnet.d.ts b/src/mono/wasm/runtime/dotnet.d.ts index 5f00a7b9b00fd5..80ef5ec1d7999a 100644 --- a/src/mono/wasm/runtime/dotnet.d.ts +++ b/src/mono/wasm/runtime/dotnet.d.ts @@ -286,11 +286,11 @@ declare class SessionOptionsBuilder { interface EventPipeSession { get sessionID(): EventPipeSessionID; - isIPCStreamingSession(): boolean; start(): void; stop(): void; getTraceBlob(): Blob; } + interface Diagnostics { EventLevel: EventLevel; SessionOptionsBuilder: typeof SessionOptionsBuilder; From 032e41ee5677f80bac1846b4176b066d524d389c Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Sun, 10 Jul 2022 22:30:13 -0400 Subject: [PATCH 41/84] call mono_wasm_event_pipe_early_startup_callback from event_pipe init instead of from the rundown_execution_checkpoint_2 function --- src/mono/mono/component/event_pipe.c | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/mono/mono/component/event_pipe.c b/src/mono/mono/component/event_pipe.c index de76dcaa45aeb3..9738f7d72255f7 100644 --- a/src/mono/mono/component/event_pipe.c +++ b/src/mono/mono/component/event_pipe.c @@ -100,12 +100,16 @@ event_pipe_wait_for_session_signal ( #ifdef HOST_WASM static void -invoke_wasm_early_startup_callback (void); +mono_wasm_event_pipe_init (void); #endif static MonoComponentEventPipe fn_table = { { MONO_COMPONENT_ITF_VERSION, &event_pipe_available }, +#ifndef HOST_WASM &ep_init, +#else + &mono_wasm_event_pipe_init, +#endif &ep_finish_init, &ep_shutdown, &event_pipe_enable, @@ -233,10 +237,6 @@ event_pipe_add_rundown_execution_checkpoint_2 ( const ep_char8_t *name, ep_timestamp_t timestamp) { -#ifdef HOST_WASM - // If WASM installed any startup session provider configs, start those sessions and record the session IDs - invoke_wasm_early_startup_callback (); -#endif return ep_add_rundown_execution_checkpoint (name, timestamp); } @@ -434,4 +434,12 @@ invoke_wasm_early_startup_callback (void) wasm_early_startup_callback (); } + +static void +mono_wasm_event_pipe_init (void) +{ + ep_init (); + invoke_wasm_early_startup_callback (); +} + #endif /* HOST_WASM */ From 637f88a2ad9130ed5b2c433d6c7cbed4d4ced73a Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Sun, 10 Jul 2022 22:50:54 -0400 Subject: [PATCH 42/84] if diagnostics server isn't enabled, don't try to initialize it --- src/mono/wasm/runtime/diagnostics/index.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/mono/wasm/runtime/diagnostics/index.ts b/src/mono/wasm/runtime/diagnostics/index.ts index 50f8ef6f4ea618..374b55aa5ba6ba 100644 --- a/src/mono/wasm/runtime/diagnostics/index.ts +++ b/src/mono/wasm/runtime/diagnostics/index.ts @@ -82,6 +82,7 @@ export const diagnostics: Diagnostics = { //// * If the diagnostic server gets more commands it will send us a message through the serverController and we will start additional sessions let suspendOnStartup = false; +let diagnosticsServerEnabled = false; export async function mono_wasm_init_diagnostics(options: DiagnosticOptions): Promise { if (!is_nullish(options.server)) { @@ -92,6 +93,7 @@ export async function mono_wasm_init_diagnostics(options: DiagnosticOptions): Pr const suspend = boolsyOption(options.server.suspend); const controller = await startDiagnosticServer(url); if (controller) { + diagnosticsServerEnabled = true; if (suspend) { suspendOnStartup = true; } @@ -114,11 +116,13 @@ function boolsyOption(x: string | boolean): boolean { } export function mono_wasm_diagnostic_server_on_runtime_server_init(out_options: VoidPtr): void { - /* called on the main thread when the runtime is sufficiently initialized */ - const controller = getController(); - controller.postServerAttachToRuntime(); - // FIXME: is this really the best place to do this? - memory.setI32(out_options, suspendOnStartup ? 1 : 0); + if (diagnosticsServerEnabled) { + /* called on the main thread when the runtime is sufficiently initialized */ + const controller = getController(); + controller.postServerAttachToRuntime(); + // FIXME: is this really the best place to do this? + memory.setI32(out_options, suspendOnStartup ? 1 : 0); + } } export default diagnostics; From 33c7d3906c37f09acc792f2ea330ccac1f82d48b Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Mon, 11 Jul 2022 16:00:38 -0400 Subject: [PATCH 43/84] WIP: start parsing binary commands --- .../wasm/runtime/diagnostics/mock/index.ts | 4 +- .../server_pthread/common-socket.ts | 27 ++ .../server_pthread/protocol-socket.ts | 307 ++++++++++++++++++ .../server_pthread/socket-connection.ts | 25 +- 4 files changed, 337 insertions(+), 26 deletions(-) create mode 100644 src/mono/wasm/runtime/diagnostics/server_pthread/common-socket.ts create mode 100644 src/mono/wasm/runtime/diagnostics/server_pthread/protocol-socket.ts diff --git a/src/mono/wasm/runtime/diagnostics/mock/index.ts b/src/mono/wasm/runtime/diagnostics/mock/index.ts index d77cfe9d038b11..b468dd35a67c50 100644 --- a/src/mono/wasm/runtime/diagnostics/mock/index.ts +++ b/src/mono/wasm/runtime/diagnostics/mock/index.ts @@ -50,8 +50,8 @@ class MockScriptEngineSocketImpl implements MockRemoteSocket { } this.engine.mockReplyEventTarget.dispatchEvent(new CloseEvent("close")); } - dispatchEvent(): boolean { - throw new Error("don't call dispatchEvent on a MockRemoteSocket"); + dispatchEvent(ev: Event): boolean { + return this.engine.eventTarget.dispatchEvent(ev); } } diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/common-socket.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/common-socket.ts new file mode 100644 index 00000000000000..c99bf59e265257 --- /dev/null +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/common-socket.ts @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +import { MockRemoteSocket } from "../mock"; + +// the common bits that we depend on from a real WebSocket or a MockRemoteSocket used for testing +export interface CommonSocket { + addEventListener(type: T, listener: (this: CommonSocket, ev: WebSocketEventMap[T]) => any, options?: boolean | AddEventListenerOptions): void; + addEventListener(event: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void; + removeEventListener(type: T, listener: (this: CommonSocket, ev: WebSocketEventMap[T]) => any): void; + removeEventListener(event: string, listener: EventListenerOrEventListenerObject): void; + dispatchEvent(evt: Event): boolean; + send(data: string | ArrayBuffer | Uint8Array | Blob | DataView): void; + close(): void; +} + + +type AssignableTo = Q extends T ? true : false; + +function static_assert(x: Cond): asserts x is Cond { /*empty*/ } + +{ + static_assert>(true); + static_assert>(true); + + static_assert>(false); // sanity check that static_assert works +} + diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/protocol-socket.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/protocol-socket.ts new file mode 100644 index 00000000000000..3df748a5c90046 --- /dev/null +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/protocol-socket.ts @@ -0,0 +1,307 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import type { CommonSocket } from "./common-socket"; + +export const dotnetDiagnosticsServerProtocolCommandEvent = "dotnet:diagnostics:protocolCommand" as const; + +// Just the minimal info we can pull from the +export interface BinaryProtocolCommand { + commandSet: number; + command: number; + payload: Uint8Array; +} + + +export interface ProtcolCommandEvent extends Event { + type: typeof dotnetDiagnosticsServerProtocolCommandEvent; + data: BinaryProtocolCommand; +} + +export interface ProtocolSocketEventMap extends WebSocketEventMap { + [dotnetDiagnosticsServerProtocolCommandEvent]: ProtcolCommandEvent; +} + +/// An adapter that takes a websocket connection and converts MessageEvent into ProtocolCommandEvent by +/// parsing the command. +interface ProtocolSocket { + addEventListener(type: K, listener: (this: ProtocolSocket, ev: ProtocolSocketEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; + addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void; + removeEventListener(type: K, listener: (this: ProtocolSocket, ev: ProtocolSocketEventMap[K]) => any, options?: boolean | EventListenerOptions): void; + removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void; + send(buf: Uint8Array): void; +} + +enum InState { + Idle, + PartialCommand, // we received part of a command, but not the complete thing + Error, // something went wrong, we won't dispatch any more ProtocolCommandEvents +} + +type State = { state: InState.Idle | InState.Error; } | PartialCommandState; + +interface PartialCommandState { + state: InState.PartialCommand; + buf: Uint8Array; /* partially received command */ + size: number; /* number of bytes of partial command */ +} + + +interface ParseResultBase { + success: boolean; +} + + +interface ParseResultOk extends ParseResultBase { + success: true + command: BinaryProtocolCommand | undefined; + newState: State; +} + +interface ParseResultFail extends ParseResultBase { + success: false; + error: string; +} + +type ParseResult = ParseResultOk | ParseResultFail; + +class ProtocolSocketImpl implements ProtocolSocket { + private state: State = { state: InState.Idle }; + private protocolListeners = 0; + private readonly messageListener: (this: CommonSocket, ev: MessageEvent) => void = this.onMessage.bind(this); + constructor(private readonly sock: CommonSocket) { + } + onMessage(this: ProtocolSocketImpl, ev: MessageEvent): void { + console.debug("socket received message", ev.data); + if (typeof ev.data === "object" && ev.data instanceof ArrayBuffer) { + this.onArrayBuffer(ev.data); + } else if (typeof ev.data === "object" && ev.data instanceof Blob) { + ev.data.arrayBuffer().then(this.onArrayBuffer.bind(this)); + } + // otherwise it's string, ignore it. + } + + onArrayBuffer(this: ProtocolSocketImpl, buf: ArrayBuffer) { + if (this.state.state == InState.Error) { + return; + } + let result: ParseResult; + if (this.state.state === InState.Idle) { + result = this.tryParseHeader(new Uint8Array(buf)); + } else { + result = this.tryAppendBuffer(new Uint8Array(buf)); + } + if (result.success) { + this.setState(result.newState); + if (result.command) { + const command = result.command; + queueMicrotask(() => { + this.dispatchProtocolCommandEvent(command); + }); + } + } else { + console.warn("socket received invalid command header", buf, result.error); + // FIXME: dispatch error event? + this.setState({ state: InState.Error }); + } + } + + tryParseHeader(buf: Uint8Array): ParseResult { + const pos = { pos: 0 }; + if (buf.byteLength < Parser.MinimalHeaderSize) { + // TODO: we need to see the magic and the size to make a partial commmand + return { success: false, error: "not enough data" }; + } + if (!Parser.tryParseHeader(buf, pos)) { + return { success: false, error: "invalid header" }; + } + const size = Parser.tryParseSize(buf, pos); + if (!size) { + return { success: false, error: "invalid size" }; + } + // make a "partially completed" state with a buffer of the right size and just the header upto the size + // field filled in. + const partialBuf = new ArrayBuffer(size); + const partialBufView = new Uint8Array(partialBuf); + partialBufView.set(buf.subarray(0, pos.pos)); + const partialState: PartialCommandState = { state: InState.PartialCommand, buf: partialBufView, size: 0 }; + return this.continueWithBuffer(partialState, buf.subarray(pos.pos)); + } + + tryAppendBuffer(moreBuf: Uint8Array): ParseResult { + if (this.state.state !== InState.PartialCommand) { + return { success: false, error: "not in partial command state" }; + } + return this.continueWithBuffer(this.state, moreBuf); + } + + continueWithBuffer(state: PartialCommandState, moreBuf: Uint8Array): ParseResult { + const buf = state.buf; + let partialSize = state.size; + let overflow: Uint8Array | null = null; + if (partialSize + moreBuf.byteLength <= buf.byteLength) { + buf.set(moreBuf, partialSize); + partialSize += moreBuf.byteLength; + } else { + const overflowSize = partialSize + moreBuf.byteLength - buf.byteLength; + const overflowOffset = moreBuf.byteLength - overflowSize; + buf.set(moreBuf.subarray(0, buf.byteLength - partialSize), partialSize); + partialSize = buf.byteLength; + const overflowBuf = new ArrayBuffer(overflowSize); + overflow = new Uint8Array(overflowBuf); + overflow.set(moreBuf.subarray(overflowOffset)); + } + if (partialSize < buf.byteLength) { + const newState = { state: InState.PartialCommand, buf, size: partialSize }; + return { success: true, command: undefined, newState }; + } else { + const pos = { pos: Parser.MinimalHeaderSize }; + const result = this.tryParseCompletedBuffer(buf, pos); + if (overflow) { + console.warn("additional bytes past command payload", overflow); + if (result.success) { + result.newState = { state: InState.Error }; + } + } + return result; + } + } + + tryParseCompletedBuffer(buf: Uint8Array, pos: { pos: number }): ParseResult { + const command = Parser.tryParseCommand(buf, pos); + if (!command) { + this.setState({ state: InState.Error }); + return { success: false, error: "invalid command" }; + } + return { success: true, command, newState: { state: InState.Idle } }; + } + + + dispatchProtocolCommandEvent(cmd: BinaryProtocolCommand): void { + const ev = new Event(dotnetDiagnosticsServerProtocolCommandEvent); + (ev).data = cmd; // FIXME: use a proper event subclass + this.sock.dispatchEvent(ev); + } + + addEventListener(type: K, listener: (this: ProtocolSocket, ev: ProtocolSocketEventMap[K]) => any, options?: boolean | AddEventListenerOptions | undefined): void; + addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions | undefined): void { + this.sock.addEventListener(type, listener, options); + if (type === dotnetDiagnosticsServerProtocolCommandEvent) { + if (this.protocolListeners === 0) { + this.sock.addEventListener("message", this.messageListener); + } + this.protocolListeners++; + } + } + + removeEventListener(type: K, listener: (this: ProtocolSocket, ev: ProtocolSocketEventMap[K]) => any): void; + removeEventListener(type: string, listener: EventListenerOrEventListenerObject): void { + if (type === dotnetDiagnosticsServerProtocolCommandEvent) { + this.protocolListeners--; + if (this.protocolListeners === 0) { + this.sock.removeEventListener("message", this.messageListener); + this.setState({ state: InState.Idle }); + } + } + this.sock.removeEventListener(type, listener); + } + + send(buf: Uint8Array) { + this.sock.send(buf); + } + + close() { + this.sock.close(); + } + + private setState(state: State) { + this.state = state; + } +} + +export function createProtocolSocket(socket: CommonSocket): ProtocolSocket { + return new ProtocolSocketImpl(socket); +} + +const Parser = { + magic_buf: null as Uint8Array | null, + get DOTNET_IPC_V1(): Uint8Array { + if (Parser.magic_buf === null) { + const magic = "DOTNET_IPC_V1"; + const magic_len = magic.length + 1; // nul terminated + Parser.magic_buf = new Uint8Array(magic_len); + for (let i = 0; i < magic_len; i++) { + Parser.magic_buf[i] = magic.charCodeAt(i); + } + Parser.magic_buf[magic_len - 1] = 0; + } + return Parser.magic_buf; + }, + get MinimalHeaderSize(): number { + // we just need to see the magic and the size + const sizeOfSize = 2; + return Parser.DOTNET_IPC_V1.byteLength + sizeOfSize; + }, + advancePos(pos: { pos: number }, offset: number) { + pos.pos += offset; + }, + tryParseHeader(buf: Uint8Array, pos: { pos: number }): boolean { + const j = pos.pos; + for (let i = 0; i < Parser.DOTNET_IPC_V1.length; i++) { + if (buf[j] !== Parser.DOTNET_IPC_V1[i]) { + return false; + } + } + Parser.advancePos(pos, Parser.DOTNET_IPC_V1.length); + return true; + }, + tryParseSize(buf: Uint8Array, pos: { pos: number }): number | undefined { + return Parser.tryParseUint16(buf, pos); + }, + tryParseCommand(buf: Uint8Array, pos: { pos: number }): BinaryProtocolCommand | undefined { + const commandSet = Parser.tryParseUint8(buf, pos); + if (commandSet === undefined) + return undefined; + const command = Parser.tryParseUint8(buf, pos); + if (command === undefined) + return undefined; + if (Parser.tryParseReserved(buf, pos) === undefined) + return undefined; + const payload = buf.slice(pos.pos); + const result = { + commandSet, + command, + payload + }; + return result; + }, + tryParseReserved(buf: Uint8Array, pos: { pos: number }): true | undefined { + const reservedLength = 2; // 2 bytes reserved, must be 0 + for (let i = 0; i < reservedLength; i++) { + const reserved = Parser.tryParseUint8(buf, pos); + if (reserved === undefined || reserved !== 0) { + return undefined; + } + } + return true; + }, + tryParseUint8(buf: Uint8Array, pos: { pos: number }): number | undefined { + const j = pos.pos; + if (j >= buf.byteLength) { + return undefined; + } + const size = buf[j]; + Parser.advancePos(pos, 1); + return size; + }, + tryParseUint16(buf: Uint8Array, pos: { pos: number }): number | undefined { + const j = pos.pos; + if (j + 1 >= buf.byteLength) { + return undefined; + } + const size = (buf[j + 1] << 8) | buf[j]; + Parser.advancePos(pos, 2); + return size; + }, + +}; diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/socket-connection.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/socket-connection.ts index 5f48733af5df20..1a508b8b6bfb0c 100644 --- a/src/mono/wasm/runtime/diagnostics/server_pthread/socket-connection.ts +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/socket-connection.ts @@ -2,38 +2,15 @@ // The .NET Foundation licenses this file to you under the MIT license. import { assertNever } from "../../types"; -import { MockRemoteSocket } from "../mock"; import { VoidPtr } from "../../types/emscripten"; import { Module } from "../../imports"; - +import type { CommonSocket } from "./common-socket"; enum ListenerState { SendingTrailingData, Closed, Error } - -// the common bits that we depend on from a real WebSocket or a MockRemoteSocket used for testing -interface CommonSocket { - addEventListener(type: T, listener: (this: CommonSocket, ev: WebSocketEventMap[T]) => any, options?: boolean | AddEventListenerOptions): void; - addEventListener(event: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void; - removeEventListener(event: string, listener: EventListenerOrEventListenerObject): void; - send(data: string | ArrayBuffer | Uint8Array | Blob | DataView): void; - close(): void; -} - -type AssignableTo = Q extends T ? true : false; - -function static_assert(x: Cond): asserts x is Cond { /*empty*/ } - -{ - static_assert>(true); - static_assert>(true); - - static_assert>(false); // sanity check that static_assert works -} - - class SocketGuts { constructor(private readonly ws: CommonSocket) { } close(): void { From 7c2d7a157d6aac9a2067851996f069199be97120 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Wed, 13 Jul 2022 16:54:27 -0400 Subject: [PATCH 44/84] WIP: start wiring up binary protocol parsing to the websocket --- .../Wasm.Browser.EventPipe.Sample.csproj | 2 +- .../diagnostics/server_pthread/index.ts | 81 +++++++++++++++++-- .../server_pthread/protocol-socket.ts | 9 ++- 3 files changed, 81 insertions(+), 11 deletions(-) diff --git a/src/mono/sample/wasm/browser-eventpipe/Wasm.Browser.EventPipe.Sample.csproj b/src/mono/sample/wasm/browser-eventpipe/Wasm.Browser.EventPipe.Sample.csproj index b723b907b560da..6f3d4453814497 100644 --- a/src/mono/sample/wasm/browser-eventpipe/Wasm.Browser.EventPipe.Sample.csproj +++ b/src/mono/sample/wasm/browser-eventpipe/Wasm.Browser.EventPipe.Sample.csproj @@ -22,7 +22,7 @@ }' /> diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts index 67fface4e3e65c..e40cb51d097508 100644 --- a/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts @@ -3,7 +3,7 @@ /// -import { assertNever } from "../../types"; +import { assertNever, mono_assert } from "../../types"; import { pthread_self } from "../../pthreads/worker"; import { Module } from "../../imports"; import cwraps from "../../cwraps"; @@ -28,6 +28,8 @@ import { } from "./protocol-client-commands"; import { makeEventPipeStreamingSession } from "./streaming-session"; import parseMockCommand from "./mock-command-parser"; +import { CommonSocket } from "./common-socket"; +import { createProtocolSocket, dotnetDiagnosticsServerProtocolCommandEvent, BinaryProtocolCommand, ProtocolCommandEvent } from "./protocol-socket"; function addOneShotMessageEventListener(src: EventTarget): Promise> { return new Promise((resolve) => { @@ -36,17 +38,33 @@ function addOneShotMessageEventListener(src: EventTarget): Promise { + return new Promise((resolve) => { + const listener = (event: Event) => { resolve(event as ProtocolCommandEvent); }; + src.addEventListener(dotnetDiagnosticsServerProtocolCommandEvent, listener, { once: true }); + }); +} + +function addOneShotOpenEventListenr(src: EventTarget): Promise { + return new Promise((resolve) => { + const listener = (event: Event) => { resolve(event); }; + src.addEventListener("open", listener, { once: true }); + }); +} + export interface DiagnosticServer { stop(): void; } class DiagnosticServerImpl implements DiagnosticServer { readonly websocketUrl: string; + readonly mocked: boolean; runtimeResumed = false; constructor(websocketUrl: string) { this.websocketUrl = websocketUrl; pthread_self.addEventListenerFromBrowser(this.onMessageFromMainThread.bind(this)); + this.mocked = websocketUrl.startsWith("mock:"); } private startRequestedController = new PromiseController(); @@ -88,12 +106,26 @@ class DiagnosticServerImpl implements DiagnosticServer { } } + async openSocket(): Promise { + if (this.mocked) { + return mockScript.open(); + } else { + const sock = new WebSocket(this.websocketUrl); + await addOneShotOpenEventListenr(sock); + return sock; + } + } async advertiseAndWaitForClient(): Promise { try { - const ws = mockScript.open(); - const p = addOneShotMessageEventListener(ws); - ws.send("ADVR_V1"); + const ws = await this.openSocket(); + let p: Promise> | Promise; + if (this.mocked) { + p = addOneShotMessageEventListener(ws); + } else { + p = addOneShotProtocolCommandEventListener(createProtocolSocket(ws)); + } + this.sendAdvertise(ws); const message = await p; console.debug("received advertising response: ", message); const cmd = this.parseCommand(message); @@ -113,8 +145,39 @@ class DiagnosticServerImpl implements DiagnosticServer { } } + sendAdvertise(ws: CommonSocket) { + const BUF_LENGTH = 34; + const buf = new ArrayBuffer(BUF_LENGTH); + const view = new Uint8Array(buf); + let pos = 0; + const text = "ADVR_V1"; + for (let i = 0; i < text.length; i++) { + view[pos++] = text.charCodeAt(i); + } + view[pos++] = 0; // nul terminator + const guid = "C979E170-B538-475C-BCF1-B04A30DA1430"; + guid.split("-").forEach((part) => { + // FIXME: I'm sure the endianness is wrong here + for (let i = 0; i < part.length; i += 2) { + view[pos++] = parseInt(part.substring(i, 2), 16); + } + }); + // "process ID" in 2 32-bit parts + const pid = [0, 1234]; + for (let i = 0; i < pid.length; i++) { + view[pos++] = pid[i] & 0xFF; + view[pos++] = (pid[i] >> 8) & 0xFF; + view[pos++] = (pid[i] >> 16) & 0xFF; + view[pos++] = (pid[i] >> 24) & 0xFF; + } + view[pos++] = 0; + view[pos++] = 0; // two reserved zero bytes + mono_assert(pos == BUF_LENGTH, "did not format ADVR_V1 correctly"); + ws.send(buf); + + } - parseCommand(message: MessageEvent): ProtocolClientCommandBase | null { + parseCommand(message: MessageEvent | ProtocolCommandEvent): ProtocolClientCommandBase | null { if (typeof message.data === "string") { return parseMockCommand(message.data); } else { @@ -194,9 +257,11 @@ export function mono_wasm_diagnostic_server_on_server_thread_created(websocketUr const websocketUrl = Module.UTF8ToString(websocketUrlPtr); console.debug(`mono_wasm_diagnostic_server_on_server_thread_created, url ${websocketUrl}`); const server = new DiagnosticServerImpl(websocketUrl); - queueMicrotask(() => { - mockScript.run(); - }); + if (websocketUrl.startsWith("mock:")) { + queueMicrotask(() => { + mockScript.run(); + }); + } queueMicrotask(() => { server.serverLoop(); }); diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/protocol-socket.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/protocol-socket.ts index 3df748a5c90046..854d09e82c6273 100644 --- a/src/mono/wasm/runtime/diagnostics/server_pthread/protocol-socket.ts +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/protocol-socket.ts @@ -13,13 +13,13 @@ export interface BinaryProtocolCommand { } -export interface ProtcolCommandEvent extends Event { +export interface ProtocolCommandEvent extends Event { type: typeof dotnetDiagnosticsServerProtocolCommandEvent; data: BinaryProtocolCommand; } export interface ProtocolSocketEventMap extends WebSocketEventMap { - [dotnetDiagnosticsServerProtocolCommandEvent]: ProtcolCommandEvent; + [dotnetDiagnosticsServerProtocolCommandEvent]: ProtocolCommandEvent; } /// An adapter that takes a websocket connection and converts MessageEvent into ProtocolCommandEvent by @@ -30,6 +30,7 @@ interface ProtocolSocket { removeEventListener(type: K, listener: (this: ProtocolSocket, ev: ProtocolSocketEventMap[K]) => any, options?: boolean | EventListenerOptions): void; removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void; send(buf: Uint8Array): void; + dispatchEvent(evt: Event): boolean; } enum InState { @@ -81,6 +82,10 @@ class ProtocolSocketImpl implements ProtocolSocket { // otherwise it's string, ignore it. } + dispatchEvent(evt: Event): boolean { + return this.sock.dispatchEvent(evt); + } + onArrayBuffer(this: ProtocolSocketImpl, buf: ArrayBuffer) { if (this.state.state == InState.Error) { return; From e0159407a143e77158ca351bf0d89f9a297ec105 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Thu, 14 Jul 2022 16:41:08 -0400 Subject: [PATCH 45/84] WIP: Can parse a CollectTracing2 command and attempt to create a session! --- .../diagnostics/server_pthread/index.ts | 33 +++- .../diagnostics/server_pthread/mock-remote.ts | 2 +- .../protocol-client-commands.ts | 2 +- .../server_pthread/protocol-socket.ts | 177 +++++++++++++++++- .../server_pthread/streaming-session.ts | 11 +- 5 files changed, 206 insertions(+), 19 deletions(-) diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts index e40cb51d097508..de34e559f08556 100644 --- a/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts @@ -29,7 +29,13 @@ import { import { makeEventPipeStreamingSession } from "./streaming-session"; import parseMockCommand from "./mock-command-parser"; import { CommonSocket } from "./common-socket"; -import { createProtocolSocket, dotnetDiagnosticsServerProtocolCommandEvent, BinaryProtocolCommand, ProtocolCommandEvent } from "./protocol-socket"; +import { + createProtocolSocket, dotnetDiagnosticsServerProtocolCommandEvent, + BinaryProtocolCommand, + ProtocolCommandEvent, + isBinaryProtocolCommand, + parseBinaryProtocolCommand, +} from "./protocol-socket"; function addOneShotMessageEventListener(src: EventTarget): Promise> { return new Promise((resolve) => { @@ -91,6 +97,7 @@ class DiagnosticServerImpl implements DiagnosticServer { await this.startRequestedController.promise; await this.attachToRuntimeController.promise; // can't start tracing until we've attached to the runtime while (!this.stopRequested) { + console.debug("diagnostic server: advertising and waiting for client"); const p1: Promise<"first" | "second"> = this.advertiseAndWaitForClient().then(() => "first"); const p2: Promise<"first" | "second"> = this.stopRequestedController.promise.then(() => "second"); const result = await Promise.race([p1, p2]); @@ -159,16 +166,18 @@ class DiagnosticServerImpl implements DiagnosticServer { guid.split("-").forEach((part) => { // FIXME: I'm sure the endianness is wrong here for (let i = 0; i < part.length; i += 2) { - view[pos++] = parseInt(part.substring(i, 2), 16); + const idx = part.length - i - 2; // go through the pieces backwards + view[pos++] = parseInt(part.substring(idx, idx + 2), 16); } }); // "process ID" in 2 32-bit parts - const pid = [0, 1234]; + const pid = [0, 1234]; // hi, lo for (let i = 0; i < pid.length; i++) { - view[pos++] = pid[i] & 0xFF; - view[pos++] = (pid[i] >> 8) & 0xFF; - view[pos++] = (pid[i] >> 16) & 0xFF; - view[pos++] = (pid[i] >> 24) & 0xFF; + const j = pid[pid.length - i - 1]; //lo, hi + view[pos++] = j & 0xFF; + view[pos++] = (j >> 8) & 0xFF; + view[pos++] = (j >> 16) & 0xFF; + view[pos++] = (j >> 24) & 0xFF; } view[pos++] = 0; view[pos++] = 0; // two reserved zero bytes @@ -182,7 +191,7 @@ class DiagnosticServerImpl implements DiagnosticServer { return parseMockCommand(message.data); } else { console.debug("parsing byte command: ", message.data); - throw new Error("TODO"); + return parseProtocolCommand(message.data); } } @@ -252,6 +261,14 @@ class DiagnosticServerImpl implements DiagnosticServer { } } +function parseProtocolCommand(data: ArrayBuffer | BinaryProtocolCommand): ProtocolClientCommandBase | null { + if (isBinaryProtocolCommand(data)) { + return parseBinaryProtocolCommand(data); + } else { + throw new Error("binary blob from mock is not implemented"); + } +} + /// Called by the runtime to initialize the diagnostic server workers export function mono_wasm_diagnostic_server_on_server_thread_created(websocketUrlPtr: CharPtr): void { const websocketUrl = Module.UTF8ToString(websocketUrlPtr); diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/mock-remote.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/mock-remote.ts index 80cbf271b463ab..8aa3e5d2af2e14 100644 --- a/src/mono/wasm/runtime/diagnostics/server_pthread/mock-remote.ts +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/mock-remote.ts @@ -19,7 +19,7 @@ const script: ((engine: MockScriptEngine) => Promise)[] = [ requestRundown: true, providers: [ { - keywords: 0, + keywords: [0, 0], logLevel: 5, provider_name: "WasmHello", filter_data: "EventCounterIntervalSec=1" diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/protocol-client-commands.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/protocol-client-commands.ts index cc99b9b9e6b908..9450bf63588803 100644 --- a/src/mono/wasm/runtime/diagnostics/server_pthread/protocol-client-commands.ts +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/protocol-client-commands.ts @@ -41,7 +41,7 @@ export interface EventPipeCommandStopTracing extends EventPipeClientCommandBase } export interface EventPipeCollectTracingCommandProvider { - keywords: number; + keywords: [number, number]; // lo,hi. FIXME: this is ugly logLevel: number; provider_name: string; filter_data: string; diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/protocol-socket.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/protocol-socket.ts index 854d09e82c6273..ac87d15a0e481f 100644 --- a/src/mono/wasm/runtime/diagnostics/server_pthread/protocol-socket.ts +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/protocol-socket.ts @@ -2,6 +2,12 @@ // The .NET Foundation licenses this file to you under the MIT license. import type { CommonSocket } from "./common-socket"; +import type { + ProtocolClientCommandBase, + EventPipeClientCommandBase, + EventPipeCommandCollectTracing2, + EventPipeCollectTracingCommandProvider +} from "./protocol-client-commands"; export const dotnetDiagnosticsServerProtocolCommandEvent = "dotnet:diagnostics:protocolCommand" as const; @@ -12,6 +18,9 @@ export interface BinaryProtocolCommand { payload: Uint8Array; } +export function isBinaryProtocolCommand(x: object): x is BinaryProtocolCommand { + return "commandSet" in x && "command" in x && "payload" in x; +} export interface ProtocolCommandEvent extends Event { type: typeof dotnetDiagnosticsServerProtocolCommandEvent; @@ -73,7 +82,7 @@ class ProtocolSocketImpl implements ProtocolSocket { constructor(private readonly sock: CommonSocket) { } onMessage(this: ProtocolSocketImpl, ev: MessageEvent): void { - console.debug("socket received message", ev.data); + console.debug("protocol socket received message", ev.data); if (typeof ev.data === "object" && ev.data instanceof ArrayBuffer) { this.onArrayBuffer(ev.data); } else if (typeof ev.data === "object" && ev.data instanceof Blob) { @@ -87,6 +96,7 @@ class ProtocolSocketImpl implements ProtocolSocket { } onArrayBuffer(this: ProtocolSocketImpl, buf: ArrayBuffer) { + console.debug("protocol-socket: parsing array buffer", buf); if (this.state.state == InState.Error) { return; } @@ -97,10 +107,13 @@ class ProtocolSocketImpl implements ProtocolSocket { result = this.tryAppendBuffer(new Uint8Array(buf)); } if (result.success) { + console.debug("protocol-socket: got result", result); this.setState(result.newState); if (result.command) { const command = result.command; + console.debug("protocol-socket: queueing command", command); queueMicrotask(() => { + console.debug("dispatching protocol event with command", command); this.dispatchProtocolCommandEvent(command); }); } @@ -121,16 +134,17 @@ class ProtocolSocketImpl implements ProtocolSocket { return { success: false, error: "invalid header" }; } const size = Parser.tryParseSize(buf, pos); - if (!size) { + if (size === undefined || size < Parser.MinimalHeaderSize) { return { success: false, error: "invalid size" }; } // make a "partially completed" state with a buffer of the right size and just the header upto the size // field filled in. + const parsedSize = pos.pos; const partialBuf = new ArrayBuffer(size); const partialBufView = new Uint8Array(partialBuf); - partialBufView.set(buf.subarray(0, pos.pos)); - const partialState: PartialCommandState = { state: InState.PartialCommand, buf: partialBufView, size: 0 }; - return this.continueWithBuffer(partialState, buf.subarray(pos.pos)); + partialBufView.set(buf.subarray(0, parsedSize)); + const partialState: PartialCommandState = { state: InState.PartialCommand, buf: partialBufView, size: parsedSize }; + return this.continueWithBuffer(partialState, buf.subarray(parsedSize)); } tryAppendBuffer(moreBuf: Uint8Array): ParseResult { @@ -193,6 +207,7 @@ class ProtocolSocketImpl implements ProtocolSocket { this.sock.addEventListener(type, listener, options); if (type === dotnetDiagnosticsServerProtocolCommandEvent) { if (this.protocolListeners === 0) { + console.debug("adding protocol listener, with a message chaser"); this.sock.addEventListener("message", this.messageListener); } this.protocolListeners++; @@ -202,6 +217,7 @@ class ProtocolSocketImpl implements ProtocolSocket { removeEventListener(type: K, listener: (this: ProtocolSocket, ev: ProtocolSocketEventMap[K]) => any): void; removeEventListener(type: string, listener: EventListenerOrEventListenerObject): void { if (type === dotnetDiagnosticsServerProtocolCommandEvent) { + console.debug("removing protocol listener and message chaser"); this.protocolListeners--; if (this.protocolListeners === 0) { this.sock.removeEventListener("message", this.messageListener); @@ -251,9 +267,9 @@ const Parser = { pos.pos += offset; }, tryParseHeader(buf: Uint8Array, pos: { pos: number }): boolean { - const j = pos.pos; + let j = pos.pos; for (let i = 0; i < Parser.DOTNET_IPC_V1.length; i++) { - if (buf[j] !== Parser.DOTNET_IPC_V1[i]) { + if (buf[j++] !== Parser.DOTNET_IPC_V1[i]) { return false; } } @@ -308,5 +324,150 @@ const Parser = { Parser.advancePos(pos, 2); return size; }, - + tryParseUint32(buf: Uint8Array, pos: { pos: number }): number | undefined { + const j = pos.pos; + if (j + 3 >= buf.byteLength) { + return undefined; + } + const size = (buf[j + 3] << 24) | (buf[j + 2] << 16) | (buf[j + 1] << 8) | buf[j]; + Parser.advancePos(pos, 4); + return size; + }, + tryParseUint64(buf: Uint8Array, pos: { pos: number }): [number, number] | undefined { + const lo = Parser.tryParseUint32(buf, pos); + if (lo === undefined) + return undefined; + const hi = Parser.tryParseUint32(buf, pos); + if (hi === undefined) + return undefined; + return [lo, hi]; + }, + tryParseBool(buf: Uint8Array, pos: { pos: number }): boolean | undefined { + const r = Parser.tryParseUint8(buf, pos); + if (r === undefined) + return undefined; + return r !== 0; + }, + tryParseArraySize(buf: Uint8Array, pos: { pos: number }): number | undefined { + const r = Parser.tryParseUint32(buf, pos); + if (r === undefined) + return undefined; + return r; + }, + tryParseStringLength(buf: Uint8Array, pos: { pos: number }): number | undefined { + return Parser.tryParseArraySize(buf, pos); + }, + tryParseUtf16String(buf: Uint8Array, pos: { pos: number }): string | undefined { + const length = Parser.tryParseStringLength(buf, pos); + if (length === undefined) + return undefined; + const j = pos.pos; + if (j + length * 2 > buf.byteLength) { + return undefined; + } + const result = new Array(length); + for (let i = 0; i < length; i++) { + result[i] = (buf[j + 2 * i + 1] << 8) | buf[j + 2 * i]; + } + Parser.advancePos(pos, length * 2); + return String.fromCharCode.apply(null, result); + } }; + + +const enum CommandSet { + Reserved = 0, + Dump = 1, + EventPipe = 2, + Profiler = 3, + Process = 4, + /* future*/ + + // replies + Server = 0xFF, +} + +export function parseBinaryProtocolCommand(cmd: BinaryProtocolCommand): ProtocolClientCommandBase | null { + switch (cmd.commandSet) { + case CommandSet.Reserved: + throw new Error("unexpected reserved command_set command"); + case CommandSet.Dump: + throw new Error("TODO"); + case CommandSet.EventPipe: + return parseEventPipeCommand(cmd); + case CommandSet.Profiler: + throw new Error("TODO"); + case CommandSet.Process: + throw new Error("TODO"); + default: + console.warn("unexpected command_set command: " + cmd.commandSet); + return null; + } +} + +const enum EventPipeCommand { + StopTracing = 1, + CollectTracing = 2, + CollectTracing2 = 3, +} + +function parseEventPipeCommand(cmd: BinaryProtocolCommand & { commandSet: CommandSet.EventPipe }): EventPipeClientCommandBase | null { + switch (cmd.command) { + case EventPipeCommand.StopTracing: + throw new Error("TODO"); + case EventPipeCommand.CollectTracing: + throw new Error("TODO"); + case EventPipeCommand.CollectTracing2: + return parseEventPipeCollectTracing2(cmd); + default: + console.warn("unexpected EventPipie command: " + cmd.command); + return null; + } +} + +function parseEventPipeCollectTracing2(cmd: BinaryProtocolCommand & { commandSet: CommandSet.EventPipe, command: EventPipeCommand.CollectTracing2 }): EventPipeCommandCollectTracing2 | null { + const pos = { pos: 0 }; + const buf = cmd.payload; + const circularBufferMB = Parser.tryParseUint32(buf, pos); + if (circularBufferMB === undefined) { + return null; + } + const format = Parser.tryParseUint32(buf, pos); + if (format === undefined) { + return null; + } + const requestRundown = Parser.tryParseBool(buf, pos); + if (requestRundown === undefined) { + return null; + } + const numProviders = Parser.tryParseArraySize(buf, pos); + if (numProviders === undefined) { + return null; + } + const providers = new Array(numProviders); + for (let i = 0; i < numProviders; i++) { + const provider = parseEventPipeCollectTracingCommandProvider(buf, pos); + if (provider === null) { + return null; + } + providers[i] = provider; + } + return { command_set: "EventPipe", command: "CollectTracing2", circularBufferMB, format, requestRundown, providers }; +} + +function parseEventPipeCollectTracingCommandProvider(buf: Uint8Array, pos: { pos: number }): EventPipeCollectTracingCommandProvider | null { + const keywords = Parser.tryParseUint64(buf, pos); + if (keywords === undefined) { + return null; + } + const logLevel = Parser.tryParseUint32(buf, pos); + if (logLevel === undefined) + return null; + const providerName = Parser.tryParseUtf16String(buf, pos); + if (providerName === undefined) + return null; + const filterData = Parser.tryParseUtf16String(buf, pos); + if (filterData === undefined) + return null; + return { keywords, logLevel, provider_name: providerName, filter_data: filterData }; +} diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/streaming-session.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/streaming-session.ts index 72c503cdab2089..845303813bb4c2 100644 --- a/src/mono/wasm/runtime/diagnostics/server_pthread/streaming-session.ts +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/streaming-session.ts @@ -45,12 +45,21 @@ function providersStringFromObject(providers: EventPipeCollectTracingCommandProv return providersString; function providerToString(provider: EventPipeCollectTracingCommandProvider): string { - const keyword_str = provider.keywords === 0 ? "" : provider.keywords.toString(); + const keyword_str = provider.keywords[0] === 0 && provider.keywords[1] === 0 ? "" : keywordsToHexString(provider.keywords); const args_str = provider.filter_data === "" ? "" : ":" + provider.filter_data; return provider.provider_name + ":" + keyword_str + ":" + provider.logLevel + args_str; } + + function keywordsToHexString(k: [number, number]): string { + const lo = k[0]; + const hi = k[1]; + const lo_hex = lo.toString(16); + const hi_hex = hi.toString(16); + return hi_hex + lo_hex; + } } + const IPC_STREAM_QUEUE_OFFSET = 4; /* keep in sync with mono_wasm_diagnostic_server_create_stream() in C */ function mono_wasm_diagnostic_server_get_stream_queue(streamAddr: VoidPtr): VoidPtr { // TODO: this can probably be in JS if we put the queue at a known address in the stream. (probably offset 4); From 182872cf7572397e9a050bb6db336457c5b75e4f Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Fri, 15 Jul 2022 09:38:07 -0400 Subject: [PATCH 46/84] [wasm-ep] use the new PromiseController --- .../runtime/diagnostics/server_pthread/index.ts | 8 ++++---- .../diagnostics/server_pthread/mock-remote.ts | 6 +++--- src/mono/wasm/runtime/promise-utils.ts | 17 ----------------- 3 files changed, 7 insertions(+), 24 deletions(-) diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts index de34e559f08556..72eaeecd643094 100644 --- a/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts @@ -15,7 +15,7 @@ import { import { mockScript } from "./mock-remote"; import type { MockRemoteSocket } from "../mock"; -import { PromiseController } from "../../promise-utils"; +import { createPromiseController } from "../../promise-controller"; import { isEventPipeCommand, isProcessCommand, @@ -73,11 +73,11 @@ class DiagnosticServerImpl implements DiagnosticServer { this.mocked = websocketUrl.startsWith("mock:"); } - private startRequestedController = new PromiseController(); + private startRequestedController = createPromiseController().promise_control; private stopRequested = false; - private stopRequestedController = new PromiseController(); + private stopRequestedController = createPromiseController().promise_control; - private attachToRuntimeController = new PromiseController(); + private attachToRuntimeController = createPromiseController().promise_control; start(): void { console.log(`starting diagnostic server with url: ${this.websocketUrl}`); diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/mock-remote.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/mock-remote.ts index 8aa3e5d2af2e14..1e0f1ee7d7b37b 100644 --- a/src/mono/wasm/runtime/diagnostics/server_pthread/mock-remote.ts +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/mock-remote.ts @@ -2,12 +2,12 @@ // The .NET Foundation licenses this file to you under the MIT license. import { mock, MockScriptEngine } from "../mock"; -import { PromiseController } from "../../promise-utils"; +import { createPromiseController } from "../../promise-controller"; function expectAdvertise(data: string | ArrayBuffer) { return data === "ADVR_V1"; } -const scriptPC = new PromiseController(); -const scriptPCunfulfilled = new PromiseController(); +const scriptPC = createPromiseController().promise_control; +const scriptPCunfulfilled = createPromiseController().promise_control; const script: ((engine: MockScriptEngine) => Promise)[] = [ async (engine) => { diff --git a/src/mono/wasm/runtime/promise-utils.ts b/src/mono/wasm/runtime/promise-utils.ts index 78ba0f4bf9201d..6b1e57ee737383 100644 --- a/src/mono/wasm/runtime/promise-utils.ts +++ b/src/mono/wasm/runtime/promise-utils.ts @@ -6,20 +6,3 @@ export function delay(ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms)); } -/// A PromiseController encapsulates a Promise together with easy access to its resolve and reject functions. -/// It's a bit like a CancelationTokenSource in .NET -export class PromiseController { - readonly promise: Promise; - readonly resolve: (value: T | PromiseLike) => void; - readonly reject: (reason: any) => void; - constructor() { - let rs: (value: T | PromiseLike) => void = undefined as any; - let rj: (reason: any) => void = undefined as any; - this.promise = new Promise((resolve, reject) => { - rs = resolve; - rj = reject; - }); - this.resolve = rs; - this.reject = rj; - } -} From 0e7a6baf205b3930b57e2b9e60aa47a7eeaf702c Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Fri, 15 Jul 2022 12:13:52 -0400 Subject: [PATCH 47/84] get back to the server loop quicker by queueing the parsing in the microtask --- .../wasm/runtime/diagnostics/server_pthread/index.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts index 72eaeecd643094..da53e9cf039cfd 100644 --- a/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts @@ -135,6 +135,15 @@ class DiagnosticServerImpl implements DiagnosticServer { this.sendAdvertise(ws); const message = await p; console.debug("received advertising response: ", message); + queueMicrotask(() => this.parseAndDispatchMessage(ws, message)); + } finally { + // if there were errors, resume the runtime anyway + this.resumeRuntime(); + } + } + + async parseAndDispatchMessage(ws: CommonSocket, message: MessageEvent | ProtocolCommandEvent): Promise { + try { const cmd = this.parseCommand(message); if (cmd === null) { console.error("unexpected message from client", message); From 6ef49c30fd3d1372a7615166ea618ca5ac1384ee Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Fri, 15 Jul 2022 12:43:32 -0400 Subject: [PATCH 48/84] update mock for binary ADVR_V1 message --- .../runtime/diagnostics/server_pthread/mock-remote.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/mock-remote.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/mock-remote.ts index 1e0f1ee7d7b37b..67b61393998563 100644 --- a/src/mono/wasm/runtime/diagnostics/server_pthread/mock-remote.ts +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/mock-remote.ts @@ -4,7 +4,15 @@ import { mock, MockScriptEngine } from "../mock"; import { createPromiseController } from "../../promise-controller"; -function expectAdvertise(data: string | ArrayBuffer) { return data === "ADVR_V1"; } +function expectAdvertise(data: string | ArrayBuffer) { + if (typeof (data) === "string") { + return data === "ADVR_V1"; + } else { + const view = new Uint8Array(data); + const ADVR_V1 = Array.from("ADVR_V1\0").map((c) => c.charCodeAt(0)); + return view.length >= ADVR_V1.length && ADVR_V1.every((v, i) => v === view[i]); + } +} const scriptPC = createPromiseController().promise_control; const scriptPCunfulfilled = createPromiseController().promise_control; From 76abc4dccb53ebe275715c58b71880727de16ddd Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Fri, 15 Jul 2022 12:43:55 -0400 Subject: [PATCH 49/84] sample: don't suspend, and use a mock url --- .../wasm/browser-eventpipe/Wasm.Browser.EventPipe.Sample.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mono/sample/wasm/browser-eventpipe/Wasm.Browser.EventPipe.Sample.csproj b/src/mono/sample/wasm/browser-eventpipe/Wasm.Browser.EventPipe.Sample.csproj index 6f3d4453814497..12277de07977cf 100644 --- a/src/mono/sample/wasm/browser-eventpipe/Wasm.Browser.EventPipe.Sample.csproj +++ b/src/mono/sample/wasm/browser-eventpipe/Wasm.Browser.EventPipe.Sample.csproj @@ -22,7 +22,7 @@ }' /> From 32dbdd9662c373b8f23315a49a943b112e7facdc Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Sat, 16 Jul 2022 14:20:42 -0400 Subject: [PATCH 50/84] use better values for parse results --- .../diagnostics/server_pthread/index.ts | 11 +++- .../server_pthread/protocol-socket.ts | 62 +++++++++++-------- 2 files changed, 45 insertions(+), 28 deletions(-) diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts index da53e9cf039cfd..513c4d69f0ed25 100644 --- a/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts @@ -35,6 +35,7 @@ import { ProtocolCommandEvent, isBinaryProtocolCommand, parseBinaryProtocolCommand, + ParseClientCommandResult, } from "./protocol-socket"; function addOneShotMessageEventListener(src: EventTarget): Promise> { @@ -200,7 +201,13 @@ class DiagnosticServerImpl implements DiagnosticServer { return parseMockCommand(message.data); } else { console.debug("parsing byte command: ", message.data); - return parseProtocolCommand(message.data); + const result = parseProtocolCommand(message.data); + if (result.success) { + return result.result; + } else { + console.warn("failed to parse command: ", result.error); + return null; + } } } @@ -270,7 +277,7 @@ class DiagnosticServerImpl implements DiagnosticServer { } } -function parseProtocolCommand(data: ArrayBuffer | BinaryProtocolCommand): ProtocolClientCommandBase | null { +function parseProtocolCommand(data: ArrayBuffer | BinaryProtocolCommand): ParseClientCommandResult { if (isBinaryProtocolCommand(data)) { return parseBinaryProtocolCommand(data); } else { diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/protocol-socket.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/protocol-socket.ts index ac87d15a0e481f..859c4c78ff1129 100644 --- a/src/mono/wasm/runtime/diagnostics/server_pthread/protocol-socket.ts +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/protocol-socket.ts @@ -6,7 +6,8 @@ import type { ProtocolClientCommandBase, EventPipeClientCommandBase, EventPipeCommandCollectTracing2, - EventPipeCollectTracingCommandProvider + EventPipeCollectTracingCommandProvider, + ProcessClientCommandBase } from "./protocol-client-commands"; export const dotnetDiagnosticsServerProtocolCommandEvent = "dotnet:diagnostics:protocolCommand" as const; @@ -61,9 +62,11 @@ interface ParseResultBase { success: boolean; } - interface ParseResultOk extends ParseResultBase { - success: true + success: true; +} + +interface ParseResultBinaryCommandOk extends ParseResultOk { command: BinaryProtocolCommand | undefined; newState: State; } @@ -73,7 +76,7 @@ interface ParseResultFail extends ParseResultBase { error: string; } -type ParseResult = ParseResultOk | ParseResultFail; +type ParseResult = ParseResultBinaryCommandOk | ParseResultFail; class ProtocolSocketImpl implements ProtocolSocket { private state: State = { state: InState.Idle }; @@ -387,7 +390,13 @@ const enum CommandSet { Server = 0xFF, } -export function parseBinaryProtocolCommand(cmd: BinaryProtocolCommand): ProtocolClientCommandBase | null { +interface ParseClientCommandResultOk extends ParseResultOk { + result: C; +} + +export type ParseClientCommandResult = ParseClientCommandResultOk | ParseResultFail; + +export function parseBinaryProtocolCommand(cmd: BinaryProtocolCommand): ParseClientCommandResult { switch (cmd.commandSet) { case CommandSet.Reserved: throw new Error("unexpected reserved command_set command"); @@ -400,8 +409,7 @@ export function parseBinaryProtocolCommand(cmd: BinaryProtocolCommand): Protocol case CommandSet.Process: throw new Error("TODO"); default: - console.warn("unexpected command_set command: " + cmd.commandSet); - return null; + return { success: false, error: `unexpected command_set ${cmd.commandSet} command` }; } } @@ -411,7 +419,7 @@ const enum EventPipeCommand { CollectTracing2 = 3, } -function parseEventPipeCommand(cmd: BinaryProtocolCommand & { commandSet: CommandSet.EventPipe }): EventPipeClientCommandBase | null { +function parseEventPipeCommand(cmd: BinaryProtocolCommand & { commandSet: CommandSet.EventPipe }): ParseClientCommandResult { switch (cmd.command) { case EventPipeCommand.StopTracing: throw new Error("TODO"); @@ -420,54 +428,56 @@ function parseEventPipeCommand(cmd: BinaryProtocolCommand & { commandSet: Comman case EventPipeCommand.CollectTracing2: return parseEventPipeCollectTracing2(cmd); default: - console.warn("unexpected EventPipie command: " + cmd.command); - return null; + console.warn("unexpected EventPipe command: " + cmd.command); + return { success: false, error: `unexpected EventPipe command ${cmd.command}` }; } } -function parseEventPipeCollectTracing2(cmd: BinaryProtocolCommand & { commandSet: CommandSet.EventPipe, command: EventPipeCommand.CollectTracing2 }): EventPipeCommandCollectTracing2 | null { +function parseEventPipeCollectTracing2(cmd: BinaryProtocolCommand & { commandSet: CommandSet.EventPipe, command: EventPipeCommand.CollectTracing2 }): ParseClientCommandResult { const pos = { pos: 0 }; const buf = cmd.payload; const circularBufferMB = Parser.tryParseUint32(buf, pos); if (circularBufferMB === undefined) { - return null; + return { success: false, error: "failed to parse circularBufferMB in EventPipe CollectTracing2 command" }; } const format = Parser.tryParseUint32(buf, pos); if (format === undefined) { - return null; + return { success: false, error: "failed to parse format in EventPipe CollectTracing2 command" }; } const requestRundown = Parser.tryParseBool(buf, pos); if (requestRundown === undefined) { - return null; + return { success: false, error: "failed to parse requestRundown in EventPipe CollectTracing2 command" }; } const numProviders = Parser.tryParseArraySize(buf, pos); if (numProviders === undefined) { - return null; + return { success: false, error: "failed to parse numProviders in EventPipe CollectTracing2 command" }; } const providers = new Array(numProviders); for (let i = 0; i < numProviders; i++) { - const provider = parseEventPipeCollectTracingCommandProvider(buf, pos); - if (provider === null) { - return null; + const result = parseEventPipeCollectTracingCommandProvider(buf, pos); + if (!result.success) { + return result; } - providers[i] = provider; + providers[i] = result.result; } - return { command_set: "EventPipe", command: "CollectTracing2", circularBufferMB, format, requestRundown, providers }; + const command: EventPipeCommandCollectTracing2 = { command_set: "EventPipe", command: "CollectTracing2", circularBufferMB, format, requestRundown, providers }; + return { success: true, result: command }; } -function parseEventPipeCollectTracingCommandProvider(buf: Uint8Array, pos: { pos: number }): EventPipeCollectTracingCommandProvider | null { +function parseEventPipeCollectTracingCommandProvider(buf: Uint8Array, pos: { pos: number }): ParseClientCommandResult { const keywords = Parser.tryParseUint64(buf, pos); if (keywords === undefined) { - return null; + return { success: false, error: "failed to parse keywords in EventPipe CollectTracing provider" }; } const logLevel = Parser.tryParseUint32(buf, pos); if (logLevel === undefined) - return null; + return { success: false, error: "failed to parse logLevel in EventPipe CollectTracing provider" }; const providerName = Parser.tryParseUtf16String(buf, pos); if (providerName === undefined) - return null; + return { success: false, error: "failed to parse providerName in EventPipe CollectTracing provider" }; const filterData = Parser.tryParseUtf16String(buf, pos); if (filterData === undefined) - return null; - return { keywords, logLevel, provider_name: providerName, filter_data: filterData }; + return { success: false, error: "failed to parse filterData in EventPipe CollectTracing provider" }; + const provider: EventPipeCollectTracingCommandProvider = { keywords, logLevel, provider_name: providerName, filter_data: filterData }; + return { success: true, result: provider }; } From c41720001805dddcbd80df256fcc325ca906091b Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Sat, 16 Jul 2022 19:47:11 -0400 Subject: [PATCH 51/84] parse a few more binary protocol commands --- .../server_pthread/protocol-socket.ts | 98 ++++++++++++++----- 1 file changed, 74 insertions(+), 24 deletions(-) diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/protocol-socket.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/protocol-socket.ts index 859c4c78ff1129..1108160856df9b 100644 --- a/src/mono/wasm/runtime/diagnostics/server_pthread/protocol-socket.ts +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/protocol-socket.ts @@ -7,7 +7,9 @@ import type { EventPipeClientCommandBase, EventPipeCommandCollectTracing2, EventPipeCollectTracingCommandProvider, - ProcessClientCommandBase + EventPipeCommandStopTracing, + ProcessClientCommandBase, + ProcessCommandResumeRuntime, } from "./protocol-client-commands"; export const dotnetDiagnosticsServerProtocolCommandEvent = "dotnet:diagnostics:protocolCommand" as const; @@ -59,21 +61,21 @@ interface PartialCommandState { interface ParseResultBase { - success: boolean; + readonly success: boolean; } interface ParseResultOk extends ParseResultBase { - success: true; + readonly success: true; } interface ParseResultBinaryCommandOk extends ParseResultOk { - command: BinaryProtocolCommand | undefined; - newState: State; + readonly command: BinaryProtocolCommand | undefined; + readonly newState: State; } interface ParseResultFail extends ParseResultBase { - success: false; - error: string; + readonly success: false; + readonly error: string; } type ParseResult = ParseResultBinaryCommandOk | ParseResultFail; @@ -178,11 +180,12 @@ class ProtocolSocketImpl implements ProtocolSocket { return { success: true, command: undefined, newState }; } else { const pos = { pos: Parser.MinimalHeaderSize }; - const result = this.tryParseCompletedBuffer(buf, pos); + let result = this.tryParseCompletedBuffer(buf, pos); if (overflow) { console.warn("additional bytes past command payload", overflow); if (result.success) { - result.newState = { state: InState.Error }; + const newResult: ParseResultBinaryCommandOk = { success: true, command: result.command, newState: { state: InState.Error } }; + result = newResult; } } return result; @@ -378,7 +381,7 @@ const Parser = { }; -const enum CommandSet { +const enum CommandSetId { Reserved = 0, Dump = 1, EventPipe = 2, @@ -391,41 +394,41 @@ const enum CommandSet { } interface ParseClientCommandResultOk extends ParseResultOk { - result: C; + readonly result: C; } export type ParseClientCommandResult = ParseClientCommandResultOk | ParseResultFail; export function parseBinaryProtocolCommand(cmd: BinaryProtocolCommand): ParseClientCommandResult { switch (cmd.commandSet) { - case CommandSet.Reserved: + case CommandSetId.Reserved: throw new Error("unexpected reserved command_set command"); - case CommandSet.Dump: + case CommandSetId.Dump: throw new Error("TODO"); - case CommandSet.EventPipe: + case CommandSetId.EventPipe: return parseEventPipeCommand(cmd); - case CommandSet.Profiler: - throw new Error("TODO"); - case CommandSet.Process: + case CommandSetId.Profiler: throw new Error("TODO"); + case CommandSetId.Process: + return parseProcessCommand(cmd); default: return { success: false, error: `unexpected command_set ${cmd.commandSet} command` }; } } -const enum EventPipeCommand { +const enum EventPipeCommandId { StopTracing = 1, CollectTracing = 2, CollectTracing2 = 3, } -function parseEventPipeCommand(cmd: BinaryProtocolCommand & { commandSet: CommandSet.EventPipe }): ParseClientCommandResult { +function parseEventPipeCommand(cmd: BinaryProtocolCommand & { commandSet: CommandSetId.EventPipe }): ParseClientCommandResult { switch (cmd.command) { - case EventPipeCommand.StopTracing: - throw new Error("TODO"); - case EventPipeCommand.CollectTracing: + case EventPipeCommandId.StopTracing: + return parseEventPipeStopTracing(cmd); + case EventPipeCommandId.CollectTracing: throw new Error("TODO"); - case EventPipeCommand.CollectTracing2: + case EventPipeCommandId.CollectTracing2: return parseEventPipeCollectTracing2(cmd); default: console.warn("unexpected EventPipe command: " + cmd.command); @@ -433,7 +436,7 @@ function parseEventPipeCommand(cmd: BinaryProtocolCommand & { commandSet: Comman } } -function parseEventPipeCollectTracing2(cmd: BinaryProtocolCommand & { commandSet: CommandSet.EventPipe, command: EventPipeCommand.CollectTracing2 }): ParseClientCommandResult { +function parseEventPipeCollectTracing2(cmd: BinaryProtocolCommand & { commandSet: CommandSetId.EventPipe, command: EventPipeCommandId.CollectTracing2 }): ParseClientCommandResult { const pos = { pos: 0 }; const buf = cmd.payload; const circularBufferMB = Parser.tryParseUint32(buf, pos); @@ -481,3 +484,50 @@ function parseEventPipeCollectTracingCommandProvider(buf: Uint8Array, pos: { pos const provider: EventPipeCollectTracingCommandProvider = { keywords, logLevel, provider_name: providerName, filter_data: filterData }; return { success: true, result: provider }; } + +function parseEventPipeStopTracing(cmd: BinaryProtocolCommand & { commandSet: CommandSetId.EventPipe, command: EventPipeCommandId.StopTracing }): ParseClientCommandResult { + const pos = { pos: 0 }; + const buf = cmd.payload; + const sessionID = Parser.tryParseUint64(buf, pos); + if (sessionID === undefined) { + return { success: false, error: "failed to parse sessionID in EventPipe StopTracing command" }; + } + const [lo, hi] = sessionID; + if (hi !== 0) { + return { success: false, error: "sessionID is too large in EventPipe StopTracing command" }; + } + const command: EventPipeCommandStopTracing = { command_set: "EventPipe", command: "StopTracing", sessionID: lo }; + return { success: true, result: command }; +} + +const enum ProcessCommandId { + ProcessInfo = 0, + ResumeRuntime = 1, + ProcessEnvironment = 2, + ProcessInfo2 = 4, +} + +function parseProcessCommand(cmd: BinaryProtocolCommand & { commandSet: CommandSetId.Process }): ParseClientCommandResult { + switch (cmd.command) { + case ProcessCommandId.ProcessInfo: + throw new Error("TODO"); + case ProcessCommandId.ResumeRuntime: + return parseProcessResumeRuntime(cmd); + case ProcessCommandId.ProcessEnvironment: + throw new Error("TODO"); + case ProcessCommandId.ProcessInfo2: + throw new Error("TODO"); + default: + console.warn("unexpected Process command: " + cmd.command); + return { success: false, error: `unexpected Process command ${cmd.command}` }; + } +} + +function parseProcessResumeRuntime(cmd: BinaryProtocolCommand & { commandSet: CommandSetId.Process, command: ProcessCommandId.ResumeRuntime }): ParseClientCommandResult { + const buf = cmd.payload; + if (buf.byteLength !== 0) { + return { success: false, error: "unexpected payload in Process ResumeRuntime command" }; + } + const command: ProcessCommandResumeRuntime = { command_set: "Process", command: "ResumeRuntime" }; + return { success: true, result: command }; +} From c3c9a25dce608917c2d1092f57b5bdef1030747b Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Mon, 18 Jul 2022 15:31:14 -0400 Subject: [PATCH 52/84] wasm_ipc_stream: wire up close command Use a sentinal "buf" value (-1) to signal that the writer closed the stream --- src/mono/mono/component/diagnostics_server.c | 15 +++-------- .../server_pthread/stream-queue.ts | 26 ++++++++++++------- .../server_pthread/streaming-session.ts | 2 +- 3 files changed, 22 insertions(+), 21 deletions(-) diff --git a/src/mono/mono/component/diagnostics_server.c b/src/mono/mono/component/diagnostics_server.c index b212460052dd6c..c0c5ae6d9b3687 100644 --- a/src/mono/mono/component/diagnostics_server.c +++ b/src/mono/mono/component/diagnostics_server.c @@ -318,9 +318,6 @@ wasm_ipc_stream_read (void *self, uint8_t *buffer, uint32_t bytes_to_read, uint3 static bool wasm_ipc_stream_write (void *self, const uint8_t *buffer, uint32_t bytes_to_write, uint32_t *bytes_written, uint32_t timeout_ms) { - EM_ASM({ - console.log ("wasm_ipc_stream_write"); - }); WasmIpcStream *stream = (WasmIpcStream *)self; g_assert (timeout_ms == EP_INFINITE_WAIT); // pass it down to the queue if the timeout param starts being used int r = queue_push_sync (&stream->queue, buffer, bytes_to_write, bytes_written); @@ -330,19 +327,15 @@ wasm_ipc_stream_write (void *self, const uint8_t *buffer, uint32_t bytes_to_writ static bool wasm_ipc_stream_flush (void *self) { - EM_ASM({ - console.log ("wasm_ipc_stream_flush"); - }); return true; } static bool wasm_ipc_stream_close (void *self) { - // TODO: signal the writer to close - EM_ASM({ - console.log ("wasm_ipc_stream_close"); - }); - return true; + WasmIpcStream *stream = (WasmIpcStream*)self; + // push the special buf value -1 to signal stream close. + int r = queue_push_sync (&stream->queue, (void*)(intptr_t)-1, 0, NULL); + return r == 0; } diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/stream-queue.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/stream-queue.ts index 40885a3cdc834f..7fe3da797b685d 100644 --- a/src/mono/wasm/runtime/diagnostics/server_pthread/stream-queue.ts +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/stream-queue.ts @@ -10,7 +10,7 @@ import * as Memory from "../../memory"; // EventPipeStreamQueue has 3 memory words that are used to communicate with the streaming thread: // struct MonoWasmEventPipeStreamQueue { -// void* buf; +// union { void* buf; intptr_t close_msg; /* -1 */ }; // int32_t count; // volatile int32_t write_done; // } @@ -34,18 +34,20 @@ import * as Memory from "../../memory"; // This would be a lot less hacky if more browsers implemented Atomics.waitAsync. // Then we wouldn't have to use emscripten_dispatch_to_thread, and instead the diagnostic server could // just call Atomics.waitAsync to wait for the streaming thread to write. +// const BUF_OFFSET = 0; const COUNT_OFFSET = 4; const WRITE_DONE_OFFSET = 8; type SyncSendBuffer = (buf: VoidPtr, len: number) => void; +type SyncSendClose = () => void; export class StreamQueue { readonly workAvailable: EventTarget = new EventTarget(); readonly signalWorkAvailable = this.signalWorkAvailableImpl.bind(this); - constructor(readonly queue_addr: VoidPtr, readonly syncSendBuffer: SyncSendBuffer) { + constructor(readonly queue_addr: VoidPtr, readonly syncSendBuffer: SyncSendBuffer, readonly syncSendClose: SyncSendClose) { this.workAvailable.addEventListener("workAvailable", this.onWorkAvailable.bind(this)); } @@ -75,11 +77,17 @@ export class StreamQueue { } private onWorkAvailable(this: StreamQueue /*,event: Event */): void { - const buf = Memory.getI32(this.buf_addr) as unknown as VoidPtr; - const count = Memory.getI32(this.count_addr); - Memory.setI32(this.buf_addr, 0); - if (count > 0) { - this.syncSendBuffer(buf, count); + const intptr_buf = this.buf_addr as unknown as number; + if (intptr_buf === -1) { + // special value signaling that the streaming thread closed the queue. + this.syncSendClose(); + } else { + const buf = Memory.getI32(this.buf_addr) as unknown as VoidPtr; + const count = Memory.getI32(this.count_addr); + Memory.setI32(this.buf_addr, 0); + if (count > 0) { + this.syncSendBuffer(buf, count); + } } /* buffer is now not full */ Memory.Atomics.storeI32(this.buf_full_addr, 0); @@ -91,8 +99,8 @@ export class StreamQueue { // maps stream queue addresses to StreamQueue instances const streamQueueMap = new Map(); -export function allocateQueue(nativeQueueAddr: VoidPtr, syncSendBuffer: SyncSendBuffer): StreamQueue { - const queue = new StreamQueue(nativeQueueAddr, syncSendBuffer); +export function allocateQueue(nativeQueueAddr: VoidPtr, syncSendBuffer: SyncSendBuffer, syncSendClose: SyncSendClose): StreamQueue { + const queue = new StreamQueue(nativeQueueAddr, syncSendBuffer, syncSendClose); streamQueueMap.set(nativeQueueAddr, queue); return queue; } diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/streaming-session.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/streaming-session.ts index 845303813bb4c2..fad8a706c60a86 100644 --- a/src/mono/wasm/runtime/diagnostics/server_pthread/streaming-session.ts +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/streaming-session.ts @@ -26,7 +26,7 @@ export async function makeEventPipeStreamingSession(ws: WebSocket | MockRemoteSo // then take over the websocket connection const conn = takeOverSocket(ws); // and set up queue notifications - const queue = allocateQueue(queueAddr, conn.write.bind(conn)); + const queue = allocateQueue(queueAddr, conn.write.bind(conn), conn.close.bind(conn)); const options = { rundownRequested: cmd.requestRundown, bufferSizeInMB: cmd.circularBufferMB, From 07c0691ffbe9f236643941cf832f1e061af894c5 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Mon, 18 Jul 2022 15:35:16 -0400 Subject: [PATCH 53/84] Send proper OK messages in replies to binary protocol commands --- .../diagnostics/server_pthread/index.ts | 39 ++++++++++++---- .../server_pthread/protocol-socket.ts | 44 +++++++++++++++++++ 2 files changed, 74 insertions(+), 9 deletions(-) diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts index 513c4d69f0ed25..d5a46bf414aad6 100644 --- a/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts @@ -25,6 +25,7 @@ import { isEventPipeCommandCollectTracing2, isEventPipeCommandStopTracing, isProcessCommandResumeRuntime, + EventPipeCommandCollectTracing2, } from "./protocol-client-commands"; import { makeEventPipeStreamingSession } from "./streaming-session"; import parseMockCommand from "./mock-command-parser"; @@ -36,6 +37,7 @@ import { isBinaryProtocolCommand, parseBinaryProtocolCommand, ParseClientCommandResult, + createBinaryCommandOKReply, } from "./protocol-socket"; function addOneShotMessageEventListener(src: EventTarget): Promise> { @@ -239,35 +241,54 @@ class DiagnosticServerImpl implements DiagnosticServer { // dispatch EventPipe commands received from the diagnostic client async dispatchEventPipeCommand(ws: WebSocket | MockRemoteSocket, cmd: EventPipeClientCommandBase): Promise { if (isEventPipeCommandCollectTracing2(cmd)) { - const session = await makeEventPipeStreamingSession(ws, cmd); - this.postClientReply(ws, "OK", session.sessionID); - console.debug("created session, now streaming: ", session); - cwraps.mono_wasm_event_pipe_session_start_streaming(session.sessionID); + await this.collectTracingEventPipe(ws, cmd); } else if (isEventPipeCommandStopTracing(cmd)) { - await this.stopEventPipe(cmd.sessionID); + await this.stopEventPipe(ws, cmd.sessionID); } else { console.warn("unknown EventPipe command: ", cmd); } } - postClientReply(ws: WebSocket | MockRemoteSocket, status: "OK", rest?: string | number): void { - ws.send(JSON.stringify([status, rest])); + postClientReplyOK(ws: WebSocket | MockRemoteSocket, payload?: Uint8Array): void { + // FIXME: send a binary response for non-mock sessions! + ws.send(createBinaryCommandOKReply(payload)); } - async stopEventPipe(sessionID: EventPipeSessionIDImpl): Promise { + async stopEventPipe(ws: WebSocket | MockRemoteSocket, sessionID: EventPipeSessionIDImpl): Promise { console.debug("stopEventPipe", sessionID); cwraps.mono_wasm_event_pipe_session_disable(sessionID); + // we might send OK before the session is actually stopped since the websocket is async + // but the client end should be robust to that. + this.postClientReplyOK(ws); + } + + async collectTracingEventPipe(ws: WebSocket | MockRemoteSocket, cmd: EventPipeCommandCollectTracing2): Promise { + const session = await makeEventPipeStreamingSession(ws, cmd); + const sessionIDbuf = new Uint8Array(8); // 64 bit + sessionIDbuf[0] = session.sessionID & 0xFF; + sessionIDbuf[1] = (session.sessionID >> 8) & 0xFF; + sessionIDbuf[2] = (session.sessionID >> 16) & 0xFF; + sessionIDbuf[3] = (session.sessionID >> 24) & 0xFF; + // sessionIDbuf[4..7] is 0 because all our session IDs are 32-bit + this.postClientReplyOK(ws, sessionIDbuf); + console.debug("created session, now streaming: ", session); + cwraps.mono_wasm_event_pipe_session_start_streaming(session.sessionID); } // dispatch Process commands received from the diagnostic client async dispatchProcessCommand(ws: WebSocket | MockRemoteSocket, cmd: ProcessClientCommandBase): Promise { if (isProcessCommandResumeRuntime(cmd)) { - this.resumeRuntime(); + this.processResumeRuntime(ws); } else { console.warn("unknown Process command", cmd); } } + processResumeRuntime(ws: WebSocket | MockRemoteSocket): void { + this.postClientReplyOK(ws); + this.resumeRuntime(); + } + resumeRuntime(): void { if (!this.runtimeResumed) { console.debug("resuming runtime startup"); diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/protocol-socket.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/protocol-socket.ts index 1108160856df9b..98a35159ce8564 100644 --- a/src/mono/wasm/runtime/diagnostics/server_pthread/protocol-socket.ts +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/protocol-socket.ts @@ -531,3 +531,47 @@ function parseProcessResumeRuntime(cmd: BinaryProtocolCommand & { commandSet: Co const command: ProcessCommandResumeRuntime = { command_set: "Process", command: "ResumeRuntime" }; return { success: true, result: command }; } + +const enum ServerCommandId { + OK = 0, + Error = 0xFF, +} + +const Serializer = { + advancePos(pos: { pos: number }, count: number): void { + pos.pos += count; + }, + serializeMagic(buf: Uint8Array, pos: { pos: number }): void { + buf.set(Parser.DOTNET_IPC_V1, pos.pos); + Serializer.advancePos(pos, Parser.DOTNET_IPC_V1.byteLength); + }, + serializeUint8(buf: Uint8Array, pos: { pos: number }, value: number): void { + buf[pos.pos++] = value; + }, + serializeUint16(buf: Uint8Array, pos: { pos: number }, value: number): void { + buf[pos.pos++] = value & 0xFF; + buf[pos.pos++] = (value >> 8) & 0xFF; + }, + serializeHeader(buf: Uint8Array, pos: { pos: number }, commandSet: CommandSetId, command: ServerCommandId, len: number): void { + Serializer.serializeMagic(buf, pos); + Serializer.serializeUint16(buf, pos, len); + Serializer.serializeUint8(buf, pos, commandSet); + Serializer.serializeUint8(buf, pos, command); + Serializer.serializeUint16(buf, pos, 0); // reserved + } +}; + +export function createBinaryCommandOKReply(payload?: Uint8Array): Uint8Array { + const fullHeaderSize = Parser.MinimalHeaderSize // magic, len + + 2 // commandSet, command + + 2; // reserved ; + const len = fullHeaderSize + (payload !== undefined ? payload.byteLength : 0); // magic, size, commandSet, command, reserved + const buf = new Uint8Array(len); + const pos = { pos: 0 }; + Serializer.serializeHeader(buf, pos, CommandSetId.Server, ServerCommandId.OK, len); + if (payload !== undefined) { + buf.set(payload, pos.pos); + Serializer.advancePos(pos, payload.byteLength); + } + return buf; +} From ab5e24741813bc318b53f6b5c2ee30ffd6ca03ba Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Mon, 18 Jul 2022 15:37:53 -0400 Subject: [PATCH 54/84] (testing) turn off the file session for now --- .../browser-eventpipe/Wasm.Browser.EventPipe.Sample.csproj | 5 ++++- src/mono/sample/wasm/browser-eventpipe/main.js | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/mono/sample/wasm/browser-eventpipe/Wasm.Browser.EventPipe.Sample.csproj b/src/mono/sample/wasm/browser-eventpipe/Wasm.Browser.EventPipe.Sample.csproj index 12277de07977cf..346b0a6c909994 100644 --- a/src/mono/sample/wasm/browser-eventpipe/Wasm.Browser.EventPipe.Sample.csproj +++ b/src/mono/sample/wasm/browser-eventpipe/Wasm.Browser.EventPipe.Sample.csproj @@ -22,7 +22,10 @@ }' /> + diff --git a/src/mono/sample/wasm/browser-eventpipe/main.js b/src/mono/sample/wasm/browser-eventpipe/main.js index 294542be6775fe..abb66871dc128e 100644 --- a/src/mono/sample/wasm/browser-eventpipe/main.js +++ b/src/mono/sample/wasm/browser-eventpipe/main.js @@ -58,8 +58,10 @@ function getOnClickHandler(startWork, stopWork, getIterationsDone) { return async function () { let sessions = MONO.diagnostics.getStartupSessions(); - if (typeof (sessions) !== "object" || sessions.length === "undefined" || sessions.length == 0) + if (typeof (sessions) !== "object" || sessions.length === "undefined") console.error("expected an array of sessions, got ", sessions); + if (sessions.length === 0) + return; // assume no sessions means they were turned off in the csproj file if (sessions.length != 1) console.error("expected one startup session, got ", sessions); let eventSession = sessions[0]; From bde9669d63198cde4b438523a593a230226ba5bd Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Mon, 18 Jul 2022 15:48:37 -0400 Subject: [PATCH 55/84] TODO: handle WS connection failures --- src/mono/wasm/runtime/diagnostics/server_pthread/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts index d5a46bf414aad6..2a045486397e86 100644 --- a/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts @@ -121,6 +121,7 @@ class DiagnosticServerImpl implements DiagnosticServer { return mockScript.open(); } else { const sock = new WebSocket(this.websocketUrl); + // TODO: add an "error" handler here - if we get readyState === 3, the connection failed. await addOneShotOpenEventListenr(sock); return sock; } From c4b8cc1ae3c51374713654215c187481094106c1 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Tue, 19 Jul 2022 09:36:12 -0400 Subject: [PATCH 56/84] remove em_asm(console.log); simplify wasm EP init Just call the EP JS callback directly from native --- src/mono/mono/component/diagnostics_server.c | 22 -------------------- src/mono/mono/component/event_pipe-stub.c | 7 ------- src/mono/mono/component/event_pipe-wasm.h | 5 ----- src/mono/mono/component/event_pipe.c | 21 +++---------------- src/mono/wasm/runtime/driver.c | 11 ---------- 5 files changed, 3 insertions(+), 63 deletions(-) diff --git a/src/mono/mono/component/diagnostics_server.c b/src/mono/mono/component/diagnostics_server.c index c0c5ae6d9b3687..b3c81a6e584937 100644 --- a/src/mono/mono/component/diagnostics_server.c +++ b/src/mono/mono/component/diagnostics_server.c @@ -67,18 +67,12 @@ static MonoComponentDiagnosticsServer fn_table = { static bool diagnostics_server_available (void) { - EM_ASM({ - console.log ("diagnostic server available"); - }); return true; } MonoComponentDiagnosticsServer * mono_component_diagnostics_server_init (void) { - EM_ASM({ - console.log ("diagnostic server component init"); - }); return &fn_table; } @@ -88,9 +82,6 @@ static bool ds_server_wasm_init (void) { /* called on the main thread when the runtime is sufficiently initialized */ - EM_ASM({ - console.log ("ds_server_wasm_init"); - }); mono_coop_sem_init (&wasm_ds_options.suspend_resume, 0); mono_wasm_diagnostic_server_on_runtime_server_init(&wasm_ds_options); return true; @@ -100,19 +91,12 @@ ds_server_wasm_init (void) static bool ds_server_wasm_shutdown (void) { - EM_ASM({ - console.log ("ds_server_wasm_shutdown"); - }); return true; } static void ds_server_wasm_pause_for_diagnostics_monitor (void) { - EM_ASM({ - console.log ("ds_server_wasm_pause_for_diagnostics_monitor"); - }); - /* wait until the DS receives a resume */ if (wasm_ds_options.suspend) { const guint timeout = 50; @@ -126,9 +110,6 @@ ds_server_wasm_pause_for_diagnostics_monitor (void) /* timed out */ cumulative_timeout += timeout; if (cumulative_timeout > warn_threshold) { - EM_ASM({ - console.log ("ds_server_wasm_pause_for_diagnostics_monitor paused for 5 seconds"); - }); cumulative_timeout = 0; } } @@ -140,9 +121,6 @@ ds_server_wasm_pause_for_diagnostics_monitor (void) static void ds_server_wasm_disable (void) { - EM_ASM({ - console.log ("ds_server_wasm_disable"); - }); } /* Allocated by mono_wasm_diagnostic_server_create_thread, diff --git a/src/mono/mono/component/event_pipe-stub.c b/src/mono/mono/component/event_pipe-stub.c index 5dc8a6b82b600c..194b7c05823b89 100644 --- a/src/mono/mono/component/event_pipe-stub.c +++ b/src/mono/mono/component/event_pipe-stub.c @@ -548,11 +548,4 @@ mono_wasm_event_pipe_session_disable (MonoWasmEventPipeSessionID session_id) { g_assert_not_reached (); } - -void -mono_wasm_event_pipe_set_early_startup_callback (mono_wasm_event_pipe_early_startup_cb callback) -{ - g_assert_not_reached (); -} - #endif /* HOST_WASM */ diff --git a/src/mono/mono/component/event_pipe-wasm.h b/src/mono/mono/component/event_pipe-wasm.h index 291626173788f9..b8853281a61dd0 100644 --- a/src/mono/mono/component/event_pipe-wasm.h +++ b/src/mono/mono/component/event_pipe-wasm.h @@ -26,8 +26,6 @@ typedef uint32_t MonoWasmEventPipeSessionID; #error "EventPipeSessionID is 64-bits, update the JS side to work with it" #endif -typedef void (*mono_wasm_event_pipe_early_startup_cb)(void); - EMSCRIPTEN_KEEPALIVE gboolean mono_wasm_event_pipe_enable (const ep_char8_t *output_path, IpcStream *ipc_stream, @@ -58,9 +56,6 @@ mono_wasm_diagnostic_server_post_resume_runtime (void); EMSCRIPTEN_KEEPALIVE IpcStream * mono_wasm_diagnostic_server_create_stream (void); -void -mono_wasm_event_pipe_set_early_startup_callback (mono_wasm_event_pipe_early_startup_cb callback); - G_END_DECLS #endif /* HOST_WASM */ diff --git a/src/mono/mono/component/event_pipe.c b/src/mono/mono/component/event_pipe.c index 9738f7d72255f7..c00ede7055c800 100644 --- a/src/mono/mono/component/event_pipe.c +++ b/src/mono/mono/component/event_pipe.c @@ -416,30 +416,15 @@ mono_wasm_event_pipe_session_disable (MonoWasmEventPipeSessionID session_id) return TRUE; } -static mono_wasm_event_pipe_early_startup_cb wasm_early_startup_callback; - -void -mono_wasm_event_pipe_set_early_startup_callback (mono_wasm_event_pipe_early_startup_cb callback) -{ - wasm_early_startup_callback = callback; -} - -void -invoke_wasm_early_startup_callback (void) -{ - EM_ASM({ - console.log ('in real invoke early callback\n'); - }); - if (wasm_early_startup_callback) - wasm_early_startup_callback (); -} +// JS callback to invoke on the main thread early during runtime initialization once eventpipe is functional but before too much of the rest of the runtime is loaded. +extern void mono_wasm_event_pipe_early_startup_callback (void); static void mono_wasm_event_pipe_init (void) { ep_init (); - invoke_wasm_early_startup_callback (); + mono_wasm_event_pipe_early_startup_callback (); } #endif /* HOST_WASM */ diff --git a/src/mono/wasm/runtime/driver.c b/src/mono/wasm/runtime/driver.c index 0eb986bef00e37..f7d74da39668bb 100644 --- a/src/mono/wasm/runtime/driver.c +++ b/src/mono/wasm/runtime/driver.c @@ -49,9 +49,6 @@ extern void mono_wasm_set_entrypoint_breakpoint (const char* assembly_name, int // Blazor specific custom routines - see dotnet_support.js for backing code extern void* mono_wasm_invoke_js_blazor (MonoString **exceptionMessage, void *callInfo, void* arg0, void* arg1, void* arg2); -// JS callback to invoke early during runtime initialization once eventpipe is functional but before too much of the rest of the runtime is loaded. -extern void mono_wasm_event_pipe_early_startup_callback (void); - void mono_wasm_enable_debugging (int); static int _marshal_type_from_mono_type (int mono_type, MonoClass *klass, MonoType *type); @@ -73,10 +70,6 @@ void mono_free (void*); int32_t mini_parse_debug_option (const char *option); char *mono_method_get_full_name (MonoMethod *method); -typedef void (*mono_wasm_event_pipe_early_startup_cb)(void); - -void mono_wasm_event_pipe_set_early_startup_callback (mono_wasm_event_pipe_early_startup_cb callback); - static void mono_wasm_init_finalizer_thread (void); #define MARSHAL_TYPE_NULL 0 @@ -542,10 +535,6 @@ mono_wasm_load_runtime (const char *unused, int debug_level) free (file_path); } - printf ("Before early startup callback\n"); - mono_wasm_event_pipe_set_early_startup_callback (mono_wasm_event_pipe_early_startup_callback); - printf ("After early startup callback\n"); - monovm_initialize (2, appctx_keys, appctx_values); mini_parse_debug_option ("top-runtime-invoke-unhandled"); From 051e2579182a1b6f7e5bc1d386bb514b5424677e Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Tue, 19 Jul 2022 09:58:24 -0400 Subject: [PATCH 57/84] remove debug output --- src/mono/mono/metadata/icall.c | 11 +---------- src/mono/mono/metadata/object.c | 6 ------ 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/src/mono/mono/metadata/icall.c b/src/mono/mono/metadata/icall.c index 1e52a93e4add28..bae99d743f7db4 100644 --- a/src/mono/mono/metadata/icall.c +++ b/src/mono/mono/metadata/icall.c @@ -6093,22 +6093,13 @@ ves_icall_System_Diagnostics_Debugger_IsAttached_internal (void) MonoBoolean ves_icall_System_Diagnostics_Debugger_IsLogging (void) { - printf ("in Debugger.IsLogging\n"); - gboolean res = mono_get_runtime_callbacks ()->debug_log_is_enabled + return mono_get_runtime_callbacks ()->debug_log_is_enabled && mono_get_runtime_callbacks ()->debug_log_is_enabled (); - printf ("in Debugger.IsLogging returning %s\n", res ? "True" : "False"); - return res; } void ves_icall_System_Diagnostics_Debugger_Log (int level, MonoString *volatile* category, MonoString *volatile* message) { - ERROR_DECL (error); - printf ("in Debugger.Log\n"); - gchar *msg = mono_string_to_utf8_checked_internal (*message, error); - mono_error_assert_ok (error); - printf ("in Debugger.Log with message '%s'\n", msg); - g_free (msg); if (mono_get_runtime_callbacks ()->debug_log) mono_get_runtime_callbacks ()->debug_log (level, *category, *message); } diff --git a/src/mono/mono/metadata/object.c b/src/mono/mono/metadata/object.c index d502537a15cd61..df63130a75f500 100644 --- a/src/mono/mono/metadata/object.c +++ b/src/mono/mono/metadata/object.c @@ -534,12 +534,6 @@ mono_runtime_class_init_full (MonoVTable *vtable, MonoError *error) MonoExceptionHandle exch = MONO_HANDLE_NEW (MonoException, exc); mono_threads_end_abort_protected_block (); - if (mono_trace_is_traced (G_LOG_LEVEL_DEBUG, MONO_TRACE_TYPE)) { - char* type_name = mono_type_full_name (m_class_get_byval_arg (klass)); - mono_trace (G_LOG_LEVEL_DEBUG, MONO_TRACE_TYPE, "Returned from running class .cctor for %s from '%s'", type_name, m_class_get_image (klass)->name); - g_free (type_name); - } - //exception extracted, error will be set to the right value later if (exc == NULL && !is_ok (error)) { // invoking failed but exc was not set exc = mono_error_convert_to_exception (error); From c75ee2ce47219abd6419855f39c1dc73f8b9b1c8 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Tue, 19 Jul 2022 09:58:42 -0400 Subject: [PATCH 58/84] remove debug output in startup --- src/mono/wasm/runtime/startup.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/mono/wasm/runtime/startup.ts b/src/mono/wasm/runtime/startup.ts index 4863e690efc1c5..8dcddc5467902d 100644 --- a/src/mono/wasm/runtime/startup.ts +++ b/src/mono/wasm/runtime/startup.ts @@ -359,13 +359,9 @@ async function finalize_startup(config: MonoConfig | MonoConfigError | undefined } try { - console.debug("MONO_WASM: Initializing mono runtime"); await _apply_configuration_from_args(config); - console.debug("MONO_WASM: applied config from args"); mono_wasm_globalization_init(config.globalization_mode!, config.diagnostic_tracing!); - console.debug("MONO_WASM: globalization init"); cwraps.mono_wasm_load_runtime("unused", config.debug_level || 0); - console.debug("MONO_WASM: loaded runtime"); runtimeHelpers.wait_for_debugger = config.wait_for_debugger; } catch (err: any) { _print_error("MONO_WASM: mono_wasm_load_runtime () failed", err); @@ -375,11 +371,9 @@ async function finalize_startup(config: MonoConfig | MonoConfigError | undefined const wasm_exit = cwraps.mono_wasm_exit; wasm_exit(1); } - console.debug("returning after calling runtime initialization rejection"); return; } - console.debug("got to before bindings_lazy_init"); bindings_lazy_init(); let tz; @@ -806,8 +800,7 @@ export type DownloadAssetsContext = { /// 3. At the point when this executes there is no pthread assigned to the worker yet. async function mono_wasm_pthread_worker_init(): Promise { // This is a good place for subsystems to attach listeners for pthreads_worker.currrentWorkerThreadEvents - console.debug("mono_wasm_pthread_worker_init"); pthreads_worker.currentWorkerThreadEvents.addEventListener(pthreads_worker.dotnetPthreadCreated, (ev) => { - console.debug("thread created", ev.pthread_self.pthread_id); + console.debug("pthread created", ev.pthread_self.pthread_id); }); } From bca82f0a3a0e85573ae512e8aa7191ab5a4f4144 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Tue, 19 Jul 2022 10:11:01 -0400 Subject: [PATCH 59/84] cleanup wasm ipc stream impl --- src/mono/mono/component/diagnostics_server.c | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/mono/mono/component/diagnostics_server.c b/src/mono/mono/component/diagnostics_server.c index b3c81a6e584937..3a010009053057 100644 --- a/src/mono/mono/component/diagnostics_server.c +++ b/src/mono/mono/component/diagnostics_server.c @@ -156,7 +156,7 @@ mono_wasm_diagnostic_server_create_thread (const char *websocket_url, pthread_t memset(out_thread_id, 0, sizeof(pthread_t)); return FALSE; } - + void mono_wasm_diagnostic_server_thread_attach_to_runtime (void) { @@ -176,11 +176,13 @@ mono_wasm_diagnostic_server_post_resume_runtime (void) } } +#define QUEUE_CLOSE_SENTINEL ((uint8_t*)(intptr_t)-1) + /* single-reader single-writer one-element queue. See * src/mono/wasm/runtime/diagnostics/server_pthread/stream-queue.ts - */ + */ typedef struct WasmIpcStreamQueue { - uint8_t *buf; + uint8_t *buf; /* or QUEUE_CLOSE_SENTINEL */ int32_t count; volatile int32_t buf_full; } WasmIpcStreamQueue; @@ -229,8 +231,14 @@ queue_push_sync (WasmIpcStreamQueue *q, const uint8_t *buf, uint32_t buf_size, u // wait until the reader reads the value int r = 0; if (G_LIKELY (will_wait)) { + gboolean is_browser_thread_inited = FALSE; + gboolean is_browser_thread = FALSE; while (mono_atomic_load_i32 (&q->buf_full) != 0) { - if (G_UNLIKELY (mono_threads_wasm_is_browser_thread ())) { + if (G_UNLIKELY (is_browser_thread_inited)) { + is_browser_thread = mono_threads_wasm_is_browser_thread (); + is_browser_thread_inited = TRUE; + } + if (G_UNLIKELY (is_browser_thread)) { /* can't use memory.atomic.wait32 on the main thread, spin instead */ /* this lets Emscripten run queued calls on the main thread */ emscripten_thread_sleep (1); @@ -312,7 +320,7 @@ wasm_ipc_stream_close (void *self) { WasmIpcStream *stream = (WasmIpcStream*)self; // push the special buf value -1 to signal stream close. - int r = queue_push_sync (&stream->queue, (void*)(intptr_t)-1, 0, NULL); + int r = queue_push_sync (&stream->queue, QUEUE_CLOSE_SENTINEL, 0, NULL); return r == 0; } From 83e4d29445b99dcfdc3a45785c2d9dec623d7311 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Tue, 19 Jul 2022 10:46:12 -0400 Subject: [PATCH 60/84] put diagnostics mocks behind a const flag --- src/mono/wasm/runtime/diagnostics/index.ts | 4 +- .../wasm/runtime/diagnostics/mock/index.ts | 224 +++++++++--------- .../runtime/diagnostics/mock/tsconfig.json | 6 +- .../diagnostics/server_pthread/index.ts | 9 +- .../diagnostics/server_pthread/mock-remote.ts | 6 +- .../diagnostics/server_pthread/tsconfig.json | 6 +- src/mono/wasm/runtime/rollup.config.js | 3 +- src/mono/wasm/runtime/types/consts.d.ts | 6 + src/mono/wasm/wasm.proj | 4 +- 9 files changed, 149 insertions(+), 119 deletions(-) diff --git a/src/mono/wasm/runtime/diagnostics/index.ts b/src/mono/wasm/runtime/diagnostics/index.ts index 374b55aa5ba6ba..d29d543203d8f4 100644 --- a/src/mono/wasm/runtime/diagnostics/index.ts +++ b/src/mono/wasm/runtime/diagnostics/index.ts @@ -29,12 +29,10 @@ let startup_session_configs: EventPipeSessionOptions[] = []; let startup_sessions: (EventPipeSession | null)[] | null = null; export function mono_wasm_event_pipe_early_startup_callback(): void { - console.debug("in mono_wasm_event_pipe_early_startup_callback in typescript"); if (startup_session_configs === null || startup_session_configs.length == 0) { - console.debug("no sessions, returning from mono_wasm_event_pipe_early_startup_callback"); return; } - console.debug("setting startup sessions based on startup session configs"); + console.debug("diagnostics: setting startup sessions based on startup session configs", startup_session_configs); startup_sessions = startup_session_configs.map(config => createAndStartEventPipeSession(config)); startup_session_configs = []; } diff --git a/src/mono/wasm/runtime/diagnostics/mock/index.ts b/src/mono/wasm/runtime/diagnostics/mock/index.ts index b468dd35a67c50..9c792f1ffa9456 100644 --- a/src/mono/wasm/runtime/diagnostics/mock/index.ts +++ b/src/mono/wasm/runtime/diagnostics/mock/index.ts @@ -1,4 +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 monoDiagnosticsMock from "consts:monoDiagnosticsMock"; export interface MockRemoteSocket extends EventTarget { addEventListener(type: T, listener: (this: MockRemoteSocket, ev: WebSocketEventMap[T]) => any, options?: boolean | AddEventListenerOptions): void; @@ -13,120 +16,129 @@ export interface Mock { run(): Promise; } -class MockScriptEngineSocketImpl implements MockRemoteSocket { - constructor(private readonly engine: MockScriptEngineImpl) { } - send(data: string | ArrayBuffer): void { - if (this.engine.trace) { - console.debug(`mock ${this.engine.ident} client sent: `, data); - } - let event: MessageEvent | null = null; - if (typeof data === "string") { - event = new MessageEvent("message", { data }); - } else { - const message = new ArrayBuffer(data.byteLength); - const messageView = new Uint8Array(message); - const dataView = new Uint8Array(data); - messageView.set(dataView); - event = new MessageEvent("message", { data: message }); - } - this.engine.mockReplyEventTarget.dispatchEvent(event); - } - addEventListener(event: T, listener: (event: WebSocketEventMap[T]) => any, options?: boolean | AddEventListenerOptions): void; - addEventListener(event: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void { - if (this.engine.trace) { - console.debug(`mock ${this.engine.ident} client added listener for ${event}`); - } - this.engine.eventTarget.addEventListener(event, listener, options); - } - removeEventListener(event: string, listener: EventListenerOrEventListenerObject): void { - if (this.engine.trace) { - console.debug(`mock ${this.engine.ident} client removed listener for ${event}`); - } - this.engine.eventTarget.removeEventListener(event, listener); - } - close(): void { - if (this.engine.trace) { - console.debug(`mock ${this.engine.ident} client closed`); - } - this.engine.mockReplyEventTarget.dispatchEvent(new CloseEvent("close")); - } - dispatchEvent(ev: Event): boolean { - return this.engine.eventTarget.dispatchEvent(ev); - } -} - -class MockScriptEngineImpl implements MockScriptEngine { - readonly socket: MockRemoteSocket; - // eventTarget that the MockReplySocket will dispatch to - readonly eventTarget: EventTarget = new EventTarget(); - // eventTarget that the MockReplySocket with send() to - readonly mockReplyEventTarget: EventTarget = new EventTarget(); - constructor(readonly trace: boolean, readonly ident: number) { - this.socket = new MockScriptEngineSocketImpl(this); - } - - reply(data: string | ArrayBuffer) { - if (this.trace) { - console.debug(`mock ${this.ident} reply:`, data); - } - this.eventTarget.dispatchEvent(new MessageEvent("message", { data })); - } - - async waitForSend(filter: (data: string | ArrayBuffer) => boolean): Promise { - const trace = this.trace; - if (trace) { - console.debug(`mock ${this.ident} waitForSend`); - } - const event = await new Promise>((resolve) => { - this.mockReplyEventTarget.addEventListener("message", (event) => { - if (trace) { - console.debug(`mock ${this.ident} waitForSend got:`, event); - } - resolve(event as MessageEvent); - }, { once: true }); - }); - if (!filter(event.data)) { - throw new Error("Unexpected data"); - } - return; - } -} - interface MockOptions { readonly trace: boolean; } -class MockImpl { - openCount: number; - engines: MockScriptEngineImpl[]; - readonly trace: boolean; - constructor(public readonly script: ((engine: MockScriptEngine) => Promise)[], options?: MockOptions) { - this.openCount = 0; - this.trace = options?.trace ?? false; - const count = script.length; - this.engines = new Array(count); - for (let i = 0; i < count; ++i) { - this.engines[i] = new MockScriptEngineImpl(this.trace, i); - } - } - open(): MockRemoteSocket { - const i = this.openCount++; - if (this.trace) { - console.debug(`mock ${i} open`); - } - return this.engines[i].socket; - } - - async run(): Promise { - await Promise.all(this.script.map((script, i) => script(this.engines[i]))); - } -} - export interface MockScriptEngine { waitForSend(filter: (data: string | ArrayBuffer) => boolean): Promise; waitForSend(filter: (data: string | ArrayBuffer) => boolean, extract: (data: string | ArrayBuffer) => T): Promise; reply(data: string | ArrayBuffer): void; } + +let MockImplConstructor: new (script: ((engine: MockScriptEngine) => Promise)[], options?: MockOptions) => Mock; export function mock(script: ((engine: MockScriptEngine) => Promise)[], options?: MockOptions): Mock { - return new MockImpl(script, options); + if (monoDiagnosticsMock) { + if (!MockImplConstructor) { + class MockScriptEngineSocketImpl implements MockRemoteSocket { + constructor(private readonly engine: MockScriptEngineImpl) { } + send(data: string | ArrayBuffer): void { + if (this.engine.trace) { + console.debug(`mock ${this.engine.ident} client sent: `, data); + } + let event: MessageEvent | null = null; + if (typeof data === "string") { + event = new MessageEvent("message", { data }); + } else { + const message = new ArrayBuffer(data.byteLength); + const messageView = new Uint8Array(message); + const dataView = new Uint8Array(data); + messageView.set(dataView); + event = new MessageEvent("message", { data: message }); + } + this.engine.mockReplyEventTarget.dispatchEvent(event); + } + addEventListener(event: T, listener: (event: WebSocketEventMap[T]) => any, options?: boolean | AddEventListenerOptions): void; + addEventListener(event: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void { + if (this.engine.trace) { + console.debug(`mock ${this.engine.ident} client added listener for ${event}`); + } + this.engine.eventTarget.addEventListener(event, listener, options); + } + removeEventListener(event: string, listener: EventListenerOrEventListenerObject): void { + if (this.engine.trace) { + console.debug(`mock ${this.engine.ident} client removed listener for ${event}`); + } + this.engine.eventTarget.removeEventListener(event, listener); + } + close(): void { + if (this.engine.trace) { + console.debug(`mock ${this.engine.ident} client closed`); + } + this.engine.mockReplyEventTarget.dispatchEvent(new CloseEvent("close")); + } + dispatchEvent(ev: Event): boolean { + return this.engine.eventTarget.dispatchEvent(ev); + } + } + + class MockScriptEngineImpl implements MockScriptEngine { + readonly socket: MockRemoteSocket; + // eventTarget that the MockReplySocket will dispatch to + readonly eventTarget: EventTarget = new EventTarget(); + // eventTarget that the MockReplySocket with send() to + readonly mockReplyEventTarget: EventTarget = new EventTarget(); + constructor(readonly trace: boolean, readonly ident: number) { + this.socket = new MockScriptEngineSocketImpl(this); + } + + reply(data: string | ArrayBuffer) { + if (this.trace) { + console.debug(`mock ${this.ident} reply:`, data); + } + this.eventTarget.dispatchEvent(new MessageEvent("message", { data })); + } + + async waitForSend(filter: (data: string | ArrayBuffer) => boolean): Promise { + const trace = this.trace; + if (trace) { + console.debug(`mock ${this.ident} waitForSend`); + } + const event = await new Promise>((resolve) => { + this.mockReplyEventTarget.addEventListener("message", (event) => { + if (trace) { + console.debug(`mock ${this.ident} waitForSend got:`, event); + } + resolve(event as MessageEvent); + }, { once: true }); + }); + if (!filter(event.data)) { + throw new Error("Unexpected data"); + } + return; + } + } + + MockImplConstructor = class MockImpl implements Mock { + openCount: number; + engines: MockScriptEngineImpl[]; + readonly trace: boolean; + constructor(public readonly script: ((engine: MockScriptEngine) => Promise)[], options?: MockOptions) { + this.openCount = 0; + this.trace = options?.trace ?? false; + const count = script.length; + this.engines = new Array(count); + for (let i = 0; i < count; ++i) { + this.engines[i] = new MockScriptEngineImpl(this.trace, i); + } + } + open(): MockRemoteSocket { + const i = this.openCount++; + if (this.trace) { + console.debug(`mock ${i} open`); + } + return this.engines[i].socket; + } + + async run(): Promise { + await Promise.all(this.script.map((script, i) => script(this.engines[i]))); + } + }; + } + return new MockImplConstructor(script, options); + } else { + return undefined as unknown as Mock; + } } + + diff --git a/src/mono/wasm/runtime/diagnostics/mock/tsconfig.json b/src/mono/wasm/runtime/diagnostics/mock/tsconfig.json index ea592d1a092a80..071a4d824c62a4 100644 --- a/src/mono/wasm/runtime/diagnostics/mock/tsconfig.json +++ b/src/mono/wasm/runtime/diagnostics/mock/tsconfig.json @@ -1,3 +1,7 @@ { - "extends": "../../tsconfig.worker.json" + "extends": "../../tsconfig.worker.json", + "include": [ + "../../**/*.ts", + "../../**/*.d.ts" + ] } diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts index 2a045486397e86..4f0394093eed23 100644 --- a/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts @@ -3,6 +3,7 @@ /// +import monoDiagnosticsMock from "consts:monoDiagnosticsMock"; import { assertNever, mono_assert } from "../../types"; import { pthread_self } from "../../pthreads/worker"; import { Module } from "../../imports"; @@ -73,7 +74,7 @@ class DiagnosticServerImpl implements DiagnosticServer { constructor(websocketUrl: string) { this.websocketUrl = websocketUrl; pthread_self.addEventListenerFromBrowser(this.onMessageFromMainThread.bind(this)); - this.mocked = websocketUrl.startsWith("mock:"); + this.mocked = monoDiagnosticsMock && websocketUrl.startsWith("mock:"); } private startRequestedController = createPromiseController().promise_control; @@ -117,7 +118,7 @@ class DiagnosticServerImpl implements DiagnosticServer { } async openSocket(): Promise { - if (this.mocked) { + if (monoDiagnosticsMock && this.mocked) { return mockScript.open(); } else { const sock = new WebSocket(this.websocketUrl); @@ -131,7 +132,7 @@ class DiagnosticServerImpl implements DiagnosticServer { try { const ws = await this.openSocket(); let p: Promise> | Promise; - if (this.mocked) { + if (monoDiagnosticsMock && this.mocked) { p = addOneShotMessageEventListener(ws); } else { p = addOneShotProtocolCommandEventListener(createProtocolSocket(ws)); @@ -312,7 +313,7 @@ export function mono_wasm_diagnostic_server_on_server_thread_created(websocketUr const websocketUrl = Module.UTF8ToString(websocketUrlPtr); console.debug(`mono_wasm_diagnostic_server_on_server_thread_created, url ${websocketUrl}`); const server = new DiagnosticServerImpl(websocketUrl); - if (websocketUrl.startsWith("mock:")) { + if (monoDiagnosticsMock && websocketUrl.startsWith("mock:")) { queueMicrotask(() => { mockScript.run(); }); diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/mock-remote.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/mock-remote.ts index 67b61393998563..df02fe7264796f 100644 --- a/src/mono/wasm/runtime/diagnostics/server_pthread/mock-remote.ts +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/mock-remote.ts @@ -1,7 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -import { mock, MockScriptEngine } from "../mock"; +import monoDiagnosticsMock from "consts:monoDiagnosticsMock"; +import type { Mock, MockScriptEngine } from "../mock"; +import { mock } from "../mock"; import { createPromiseController } from "../../promise-controller"; function expectAdvertise(data: string | ArrayBuffer) { @@ -49,4 +51,4 @@ const script: ((engine: MockScriptEngine) => Promise)[] = [ ]; /// a mock script that simulates the initial part of the diagnostic server protocol -export const mockScript = mock(script, { trace: true }); +export const mockScript = monoDiagnosticsMock ? mock(script, { trace: true }) : undefined as unknown as Mock; diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/tsconfig.json b/src/mono/wasm/runtime/diagnostics/server_pthread/tsconfig.json index ea592d1a092a80..071a4d824c62a4 100644 --- a/src/mono/wasm/runtime/diagnostics/server_pthread/tsconfig.json +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/tsconfig.json @@ -1,3 +1,7 @@ { - "extends": "../../tsconfig.worker.json" + "extends": "../../tsconfig.worker.json", + "include": [ + "../../**/*.ts", + "../../**/*.d.ts" + ] } diff --git a/src/mono/wasm/runtime/rollup.config.js b/src/mono/wasm/runtime/rollup.config.js index 9ab2cf18b1302f..f796536160eb16 100644 --- a/src/mono/wasm/runtime/rollup.config.js +++ b/src/mono/wasm/runtime/rollup.config.js @@ -15,6 +15,7 @@ const isDebug = configuration !== "Release"; const productVersion = process.env.ProductVersion || "7.0.0-dev"; const nativeBinDir = process.env.NativeBinDir ? process.env.NativeBinDir.replace(/"/g, "") : "bin"; const monoWasmThreads = process.env.MonoWasmThreads === "true" ? true : false; +const monoDiagnosticsMock = process.env.MonoDiagnosticsMock === "true" ? true : false; const terserConfig = { compress: { defaults: false,// too agressive minification breaks subsequent emcc compilation @@ -62,7 +63,7 @@ const inlineAssert = [ pattern: /^\s*mono_assert/gm, failure: "previous regexp didn't inline all mono_assert statements" }]; -const outputCodePlugins = [regexReplace(inlineAssert), consts({ productVersion, configuration, monoWasmThreads }), typescript()]; +const outputCodePlugins = [regexReplace(inlineAssert), consts({ productVersion, configuration, monoWasmThreads, monoDiagnosticsMock }), typescript()]; const externalDependencies = [ "node/buffer" diff --git a/src/mono/wasm/runtime/types/consts.d.ts b/src/mono/wasm/runtime/types/consts.d.ts index c71e7862d5a479..86758a4e980fc3 100644 --- a/src/mono/wasm/runtime/types/consts.d.ts +++ b/src/mono/wasm/runtime/types/consts.d.ts @@ -8,3 +8,9 @@ declare module "consts:monoWasmThreads" { const constant: boolean; export default constant; } + +/* if true, include mock impplementations of diagnostics sockets */ +declare module "consts:monoDiagnosticsMock" { + const constant: boolean; + export default constant; +} diff --git a/src/mono/wasm/wasm.proj b/src/mono/wasm/wasm.proj index 2fce384d45e358..ab7f916dae19c0 100644 --- a/src/mono/wasm/wasm.proj +++ b/src/mono/wasm/wasm.proj @@ -11,6 +11,8 @@ true true + false + true @@ -370,7 +372,7 @@ - + Date: Tue, 19 Jul 2022 11:26:36 -0400 Subject: [PATCH 61/84] don't build wasm-specific DS if threads are disabled --- src/mono/mono/component/diagnostics_server.c | 55 ++++++++++---------- src/mono/mono/utils/mono-threads-wasm.h | 10 ++-- 2 files changed, 32 insertions(+), 33 deletions(-) diff --git a/src/mono/mono/component/diagnostics_server.c b/src/mono/mono/component/diagnostics_server.c index 3a010009053057..0f040ee14712a6 100644 --- a/src/mono/mono/component/diagnostics_server.c +++ b/src/mono/mono/component/diagnostics_server.c @@ -19,7 +19,7 @@ static bool diagnostics_server_available (void); -#ifndef HOST_WASM +#if !defined (HOST_WASM) || defined (DISABLE_THREADS) static MonoComponentDiagnosticsServer fn_table = { { MONO_COMPONENT_ITF_VERSION, &diagnostics_server_available }, &ds_server_init, @@ -27,14 +27,8 @@ static MonoComponentDiagnosticsServer fn_table = { &ds_server_pause_for_diagnostics_monitor, &ds_server_disable }; -#else -typedef struct _MonoWasmDiagnosticServerOptions { - int32_t suspend; /* set from JS! */ - MonoCoopSem suspend_resume; -} MonoWasmDiagnosticServerOptions; -static MonoWasmDiagnosticServerOptions wasm_ds_options; -static pthread_t ds_thread_id; +#else /* !defined (HOST_WASM) || defined (DISABLE_THREADS) */ static bool ds_server_wasm_init (void); @@ -48,13 +42,6 @@ ds_server_wasm_pause_for_diagnostics_monitor (void); static void ds_server_wasm_disable (void); -extern void -mono_wasm_diagnostic_server_on_runtime_server_init (MonoWasmDiagnosticServerOptions *out_options); - -EMSCRIPTEN_KEEPALIVE void -mono_wasm_diagnostic_server_resume_runtime_startup (void); - - static MonoComponentDiagnosticsServer fn_table = { { MONO_COMPONENT_ITF_VERSION, &diagnostics_server_available }, &ds_server_wasm_init, @@ -62,21 +49,20 @@ static MonoComponentDiagnosticsServer fn_table = { &ds_server_wasm_pause_for_diagnostics_monitor, &ds_server_wasm_disable, }; -#endif -static bool -diagnostics_server_available (void) -{ - return true; -} +typedef struct _MonoWasmDiagnosticServerOptions { + int32_t suspend; /* set from JS! */ + MonoCoopSem suspend_resume; +} MonoWasmDiagnosticServerOptions; -MonoComponentDiagnosticsServer * -mono_component_diagnostics_server_init (void) -{ - return &fn_table; -} +static MonoWasmDiagnosticServerOptions wasm_ds_options; +static pthread_t ds_thread_id; -#ifdef HOST_WASM +extern void +mono_wasm_diagnostic_server_on_runtime_server_init (MonoWasmDiagnosticServerOptions *out_options); + +EMSCRIPTEN_KEEPALIVE void +mono_wasm_diagnostic_server_resume_runtime_startup (void); static bool ds_server_wasm_init (void) @@ -315,6 +301,7 @@ wasm_ipc_stream_flush (void *self) { return true; } + static bool wasm_ipc_stream_close (void *self) { @@ -324,5 +311,17 @@ wasm_ipc_stream_close (void *self) return r == 0; } +#endif /* !defined (HOST_WASM) || defined (DISABLE_THREADS) */ -#endif +static bool +diagnostics_server_available (void) +{ + return true; +} + +MonoComponentDiagnosticsServer * +mono_component_diagnostics_server_init (void) +{ + return &fn_table; + +} diff --git a/src/mono/mono/utils/mono-threads-wasm.h b/src/mono/mono/utils/mono-threads-wasm.h index 5ff92b2b1ee6ca..c06d8501e1ec3e 100644 --- a/src/mono/mono/utils/mono-threads-wasm.h +++ b/src/mono/mono/utils/mono-threads-wasm.h @@ -42,11 +42,6 @@ mono_threads_wasm_async_run_in_main_thread_vi (void (*func)(gpointer), gpointer void mono_threads_wasm_async_run_in_main_thread_vii (void (*func)(gpointer, gpointer), gpointer user_data1, gpointer user_data2); -#endif /* DISABLE_THREADS */ - -// Called from register_thread when a pthread attaches to the runtime -void -mono_threads_wasm_on_thread_attached (void); static inline int32_t @@ -64,6 +59,11 @@ mono_wasm_atomic_wait_i32 (volatile int32_t *addr, int32_t expected, int32_t tim // 2 == "timed-out", timeout expired before thread was woken up return __builtin_wasm_memory_atomic_wait32((int32_t*)addr, expected, timeout_ns); } +#endif /* DISABLE_THREADS */ + +// Called from register_thread when a pthread attaches to the runtime +void +mono_threads_wasm_on_thread_attached (void); #endif /* HOST_WASM*/ From 033b0c476a6a59e7790e7c58d012793462d43a7a Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Tue, 19 Jul 2022 12:44:35 -0400 Subject: [PATCH 62/84] refactor and cleanup - Move the IPC parsing and serialization into separate files - Try to have one responsibility per class - update comments and docs --- src/mono/wasm/runtime/diagnostics/README.md | 5 +- .../runtime/diagnostics/browser/controller.ts | 3 +- src/mono/wasm/runtime/diagnostics/index.ts | 1 + .../diagnostics/server_pthread/index.ts | 13 +- .../ipc-protocol/base-parser.ts | 120 +++++ .../ipc-protocol/base-serializer.ts | 44 ++ .../server_pthread/ipc-protocol/magic.ts | 25 + .../server_pthread/ipc-protocol/parser.ts | 147 ++++++ .../server_pthread/ipc-protocol/serializer.ts | 16 + .../server_pthread/ipc-protocol/types.ts | 57 +++ .../server_pthread/protocol-socket.ts | 455 +++--------------- .../server_pthread/socket-connection.ts | 14 +- .../server_pthread/stream-queue.ts | 29 +- .../server_pthread/streaming-session.ts | 13 +- .../diagnostics/shared/controller-commands.ts | 15 +- .../wasm/runtime/diagnostics/shared/types.ts | 13 - 16 files changed, 527 insertions(+), 443 deletions(-) create mode 100644 src/mono/wasm/runtime/diagnostics/server_pthread/ipc-protocol/base-parser.ts create mode 100644 src/mono/wasm/runtime/diagnostics/server_pthread/ipc-protocol/base-serializer.ts create mode 100644 src/mono/wasm/runtime/diagnostics/server_pthread/ipc-protocol/magic.ts create mode 100644 src/mono/wasm/runtime/diagnostics/server_pthread/ipc-protocol/parser.ts create mode 100644 src/mono/wasm/runtime/diagnostics/server_pthread/ipc-protocol/serializer.ts create mode 100644 src/mono/wasm/runtime/diagnostics/server_pthread/ipc-protocol/types.ts diff --git a/src/mono/wasm/runtime/diagnostics/README.md b/src/mono/wasm/runtime/diagnostics/README.md index 964c9b563d864f..09e438fd1b93d5 100644 --- a/src/mono/wasm/runtime/diagnostics/README.md +++ b/src/mono/wasm/runtime/diagnostics/README.md @@ -5,8 +5,7 @@ What's in here: - `index.ts` toplevel APIs - `browser/` APIs for the main thread. The main thread has 2 responsibilities: - control the overall diagnostic server `browser/controller.ts` - - establish communication channels between EventPipe session streaming threads and the diagnostic server pthread -- `server_pthread/` A long-running worker that owns the WebSocket connections out of the browser and that receives the session payloads from the streaming threads. -- `pthread/` (**TODO* decide if this is necessary) APIs for normal pthreads that need to do things to diagnostics +- `server_pthread/` A long-running worker that owns the WebSocket connections out of the browser to th ehost and that receives the session payloads from the streaming threads. The server receives streaming EventPipe data from +EventPipe streaming threads (that are just ordinary C pthreads) through a shared memory queue and forwards the data to the WebSocket. The server uses the [DS binary IPC protocol](https://github.com/dotnet/diagnostics/blob/main/documentation/design-docs/ipc-protocol.md) which repeatedly opens WebSockets to the host. - `shared/` type definitions to be shared between the worker and browser main thread - `mock/` a utility to fake WebSocket connectings by playing back a script. Used for prototyping the diagnostic server without hooking up to a real WebSocket. diff --git a/src/mono/wasm/runtime/diagnostics/browser/controller.ts b/src/mono/wasm/runtime/diagnostics/browser/controller.ts index ff453bb20bcae0..79cd9a7a498417 100644 --- a/src/mono/wasm/runtime/diagnostics/browser/controller.ts +++ b/src/mono/wasm/runtime/diagnostics/browser/controller.ts @@ -4,8 +4,7 @@ import cwraps from "../../cwraps"; import { withStackAlloc, getI32 } from "../../memory"; import { Thread, waitForThread } from "../../pthreads/browser"; -import { makeDiagnosticServerControlCommand } from "../shared/controller-commands"; -import { isDiagnosticMessage } from "../shared/types"; +import { isDiagnosticMessage, makeDiagnosticServerControlCommand } from "../shared/controller-commands"; /// An object that can be used to control the diagnostic server. export interface ServerController { diff --git a/src/mono/wasm/runtime/diagnostics/index.ts b/src/mono/wasm/runtime/diagnostics/index.ts index d29d543203d8f4..9f6ba4ed76ae09 100644 --- a/src/mono/wasm/runtime/diagnostics/index.ts +++ b/src/mono/wasm/runtime/diagnostics/index.ts @@ -28,6 +28,7 @@ export interface Diagnostics { let startup_session_configs: EventPipeSessionOptions[] = []; let startup_sessions: (EventPipeSession | null)[] | null = null; +// called from C on the main thread export function mono_wasm_event_pipe_early_startup_callback(): void { if (startup_session_configs === null || startup_session_configs.length == 0) { return; diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts index 4f0394093eed23..03b2acaa84be16 100644 --- a/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts @@ -8,10 +8,11 @@ import { assertNever, mono_assert } from "../../types"; import { pthread_self } from "../../pthreads/worker"; import { Module } from "../../imports"; import cwraps from "../../cwraps"; -import { EventPipeSessionIDImpl, isDiagnosticMessage } from "../shared/types"; +import { EventPipeSessionIDImpl } from "../shared/types"; import { CharPtr } from "../../types/emscripten"; import { DiagnosticServerControlCommand, + isDiagnosticMessage } from "../shared/controller-commands"; import { mockScript } from "./mock-remote"; @@ -33,13 +34,19 @@ import parseMockCommand from "./mock-command-parser"; import { CommonSocket } from "./common-socket"; import { createProtocolSocket, dotnetDiagnosticsServerProtocolCommandEvent, - BinaryProtocolCommand, ProtocolCommandEvent, +} from "./protocol-socket"; +import { + BinaryProtocolCommand, isBinaryProtocolCommand, +} from "./ipc-protocol/types"; +import { parseBinaryProtocolCommand, ParseClientCommandResult, +} from "./ipc-protocol/parser"; +import { createBinaryCommandOKReply, -} from "./protocol-socket"; +} from "./ipc-protocol/serializer"; function addOneShotMessageEventListener(src: EventTarget): Promise> { return new Promise((resolve) => { diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/ipc-protocol/base-parser.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/ipc-protocol/base-parser.ts new file mode 100644 index 00000000000000..6ac893185ac92a --- /dev/null +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/ipc-protocol/base-parser.ts @@ -0,0 +1,120 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import Magic from "./magic"; +import { BinaryProtocolCommand } from "./types"; + +function advancePos(pos: { pos: number }, offset: number): void { + pos.pos += offset; +} + +const Parser = { + tryParseHeader(buf: Uint8Array, pos: { pos: number }): boolean { + let j = pos.pos; + for (let i = 0; i < Magic.DOTNET_IPC_V1.length; i++) { + if (buf[j++] !== Magic.DOTNET_IPC_V1[i]) { + return false; + } + } + advancePos(pos, Magic.DOTNET_IPC_V1.length); + return true; + }, + tryParseSize(buf: Uint8Array, pos: { pos: number }): number | undefined { + return Parser.tryParseUint16(buf, pos); + }, + tryParseCommand(buf: Uint8Array, pos: { pos: number }): BinaryProtocolCommand | undefined { + const commandSet = Parser.tryParseUint8(buf, pos); + if (commandSet === undefined) + return undefined; + const command = Parser.tryParseUint8(buf, pos); + if (command === undefined) + return undefined; + if (Parser.tryParseReserved(buf, pos) === undefined) + return undefined; + const payload = buf.slice(pos.pos); + const result = { + commandSet, + command, + payload + }; + return result; + }, + tryParseReserved(buf: Uint8Array, pos: { pos: number }): true | undefined { + const reservedLength = 2; // 2 bytes reserved, must be 0 + for (let i = 0; i < reservedLength; i++) { + const reserved = Parser.tryParseUint8(buf, pos); + if (reserved === undefined || reserved !== 0) { + return undefined; + } + } + return true; + }, + tryParseUint8(buf: Uint8Array, pos: { pos: number }): number | undefined { + const j = pos.pos; + if (j >= buf.byteLength) { + return undefined; + } + const size = buf[j]; + advancePos(pos, 1); + return size; + }, + tryParseUint16(buf: Uint8Array, pos: { pos: number }): number | undefined { + const j = pos.pos; + if (j + 1 >= buf.byteLength) { + return undefined; + } + const size = (buf[j + 1] << 8) | buf[j]; + advancePos(pos, 2); + return size; + }, + tryParseUint32(buf: Uint8Array, pos: { pos: number }): number | undefined { + const j = pos.pos; + if (j + 3 >= buf.byteLength) { + return undefined; + } + const size = (buf[j + 3] << 24) | (buf[j + 2] << 16) | (buf[j + 1] << 8) | buf[j]; + advancePos(pos, 4); + return size; + }, + tryParseUint64(buf: Uint8Array, pos: { pos: number }): [number, number] | undefined { + const lo = Parser.tryParseUint32(buf, pos); + if (lo === undefined) + return undefined; + const hi = Parser.tryParseUint32(buf, pos); + if (hi === undefined) + return undefined; + return [lo, hi]; + }, + tryParseBool(buf: Uint8Array, pos: { pos: number }): boolean | undefined { + const r = Parser.tryParseUint8(buf, pos); + if (r === undefined) + return undefined; + return r !== 0; + }, + tryParseArraySize(buf: Uint8Array, pos: { pos: number }): number | undefined { + const r = Parser.tryParseUint32(buf, pos); + if (r === undefined) + return undefined; + return r; + }, + tryParseStringLength(buf: Uint8Array, pos: { pos: number }): number | undefined { + return Parser.tryParseArraySize(buf, pos); + }, + tryParseUtf16String(buf: Uint8Array, pos: { pos: number }): string | undefined { + const length = Parser.tryParseStringLength(buf, pos); + if (length === undefined) + return undefined; + const j = pos.pos; + if (j + length * 2 > buf.byteLength) { + return undefined; + } + const result = new Array(length); + for (let i = 0; i < length; i++) { + result[i] = (buf[j + 2 * i + 1] << 8) | buf[j + 2 * i]; + } + advancePos(pos, length * 2); + return String.fromCharCode.apply(null, result); + } +}; + +export default Parser; diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/ipc-protocol/base-serializer.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/ipc-protocol/base-serializer.ts new file mode 100644 index 00000000000000..e42b6dcdd6de22 --- /dev/null +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/ipc-protocol/base-serializer.ts @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import { CommandSetId, ServerCommandId } from "./types"; +import Magic from "./magic"; + +function advancePos(pos: { pos: number }, count: number): void { + pos.pos += count; +} + + +const Serializer = { + computeMessageByteLength(payload?: Uint8Array): number { + const fullHeaderSize = Magic.MinimalHeaderSize // magic, len + + 2 // commandSet, command + + 2; // reserved ; + const len = fullHeaderSize + (payload !== undefined ? payload.byteLength : 0); // magic, size, commandSet, command, reserved + return len; + }, + serializeMagic(buf: Uint8Array, pos: { pos: number }): void { + buf.set(Magic.DOTNET_IPC_V1, pos.pos); + advancePos(pos, Magic.DOTNET_IPC_V1.byteLength); + }, + serializeUint8(buf: Uint8Array, pos: { pos: number }, value: number): void { + buf[pos.pos++] = value; + }, + serializeUint16(buf: Uint8Array, pos: { pos: number }, value: number): void { + buf[pos.pos++] = value & 0xFF; + buf[pos.pos++] = (value >> 8) & 0xFF; + }, + serializeHeader(buf: Uint8Array, pos: { pos: number }, commandSet: CommandSetId, command: ServerCommandId, len: number): void { + Serializer.serializeMagic(buf, pos); + Serializer.serializeUint16(buf, pos, len); + Serializer.serializeUint8(buf, pos, commandSet); + Serializer.serializeUint8(buf, pos, command); + Serializer.serializeUint16(buf, pos, 0); // reserved + }, + serializePayload(buf: Uint8Array, pos: { pos: number }, payload: Uint8Array): void { + buf.set(payload, pos.pos); + advancePos(pos, payload.byteLength); + } +}; + +export default Serializer; diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/ipc-protocol/magic.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/ipc-protocol/magic.ts new file mode 100644 index 00000000000000..e7f27b9c6ab1f2 --- /dev/null +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/ipc-protocol/magic.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. + +let magic_buf: Uint8Array = null!; +const Magic = { + get DOTNET_IPC_V1(): Uint8Array { + if (magic_buf === null) { + const magic = "DOTNET_IPC_V1"; + const magic_len = magic.length + 1; // nul terminated + magic_buf = new Uint8Array(magic_len); + for (let i = 0; i < magic_len; i++) { + magic_buf[i] = magic.charCodeAt(i); + } + magic_buf[magic_len - 1] = 0; + } + return magic_buf; + }, + get MinimalHeaderSize(): number { + // we just need to see the magic and the size + const sizeOfSize = 2; + return Magic.DOTNET_IPC_V1.byteLength + sizeOfSize; + }, +}; + +export default Magic; diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/ipc-protocol/parser.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/ipc-protocol/parser.ts new file mode 100644 index 00000000000000..32fc4203f00ecd --- /dev/null +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/ipc-protocol/parser.ts @@ -0,0 +1,147 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import Parser from "./base-parser"; +import { + ProtocolClientCommandBase, + ProcessClientCommandBase, + EventPipeClientCommandBase, + EventPipeCommandCollectTracing2, + EventPipeCollectTracingCommandProvider, + EventPipeCommandStopTracing, + ProcessCommandResumeRuntime, +} from "../protocol-client-commands"; +import { + BinaryProtocolCommand, + ParseResultOk, + ParseResultFail, + CommandSetId, + EventPipeCommandId, + ProcessCommandId, +} from "./types"; + +interface ParseClientCommandResultOk extends ParseResultOk { + readonly result: C; +} + +export type ParseClientCommandResult = ParseClientCommandResultOk | ParseResultFail; + +export function parseBinaryProtocolCommand(cmd: BinaryProtocolCommand): ParseClientCommandResult { + switch (cmd.commandSet) { + case CommandSetId.Reserved: + throw new Error("unexpected reserved command_set command"); + case CommandSetId.Dump: + throw new Error("TODO"); + case CommandSetId.EventPipe: + return parseEventPipeCommand(cmd); + case CommandSetId.Profiler: + throw new Error("TODO"); + case CommandSetId.Process: + return parseProcessCommand(cmd); + default: + return { success: false, error: `unexpected command_set ${cmd.commandSet} command` }; + } +} + +function parseEventPipeCommand(cmd: BinaryProtocolCommand & { commandSet: CommandSetId.EventPipe }): ParseClientCommandResult { + switch (cmd.command) { + case EventPipeCommandId.StopTracing: + return parseEventPipeStopTracing(cmd); + case EventPipeCommandId.CollectTracing: + throw new Error("TODO"); + case EventPipeCommandId.CollectTracing2: + return parseEventPipeCollectTracing2(cmd); + default: + console.warn("unexpected EventPipe command: " + cmd.command); + return { success: false, error: `unexpected EventPipe command ${cmd.command}` }; + } +} + +function parseEventPipeCollectTracing2(cmd: BinaryProtocolCommand & { commandSet: CommandSetId.EventPipe, command: EventPipeCommandId.CollectTracing2 }): ParseClientCommandResult { + const pos = { pos: 0 }; + const buf = cmd.payload; + const circularBufferMB = Parser.tryParseUint32(buf, pos); + if (circularBufferMB === undefined) { + return { success: false, error: "failed to parse circularBufferMB in EventPipe CollectTracing2 command" }; + } + const format = Parser.tryParseUint32(buf, pos); + if (format === undefined) { + return { success: false, error: "failed to parse format in EventPipe CollectTracing2 command" }; + } + const requestRundown = Parser.tryParseBool(buf, pos); + if (requestRundown === undefined) { + return { success: false, error: "failed to parse requestRundown in EventPipe CollectTracing2 command" }; + } + const numProviders = Parser.tryParseArraySize(buf, pos); + if (numProviders === undefined) { + return { success: false, error: "failed to parse numProviders in EventPipe CollectTracing2 command" }; + } + const providers = new Array(numProviders); + for (let i = 0; i < numProviders; i++) { + const result = parseEventPipeCollectTracingCommandProvider(buf, pos); + if (!result.success) { + return result; + } + providers[i] = result.result; + } + const command: EventPipeCommandCollectTracing2 = { command_set: "EventPipe", command: "CollectTracing2", circularBufferMB, format, requestRundown, providers }; + return { success: true, result: command }; +} + +function parseEventPipeCollectTracingCommandProvider(buf: Uint8Array, pos: { pos: number }): ParseClientCommandResult { + const keywords = Parser.tryParseUint64(buf, pos); + if (keywords === undefined) { + return { success: false, error: "failed to parse keywords in EventPipe CollectTracing provider" }; + } + const logLevel = Parser.tryParseUint32(buf, pos); + if (logLevel === undefined) + return { success: false, error: "failed to parse logLevel in EventPipe CollectTracing provider" }; + const providerName = Parser.tryParseUtf16String(buf, pos); + if (providerName === undefined) + return { success: false, error: "failed to parse providerName in EventPipe CollectTracing provider" }; + const filterData = Parser.tryParseUtf16String(buf, pos); + if (filterData === undefined) + return { success: false, error: "failed to parse filterData in EventPipe CollectTracing provider" }; + const provider: EventPipeCollectTracingCommandProvider = { keywords, logLevel, provider_name: providerName, filter_data: filterData }; + return { success: true, result: provider }; +} + +function parseEventPipeStopTracing(cmd: BinaryProtocolCommand & { commandSet: CommandSetId.EventPipe, command: EventPipeCommandId.StopTracing }): ParseClientCommandResult { + const pos = { pos: 0 }; + const buf = cmd.payload; + const sessionID = Parser.tryParseUint64(buf, pos); + if (sessionID === undefined) { + return { success: false, error: "failed to parse sessionID in EventPipe StopTracing command" }; + } + const [lo, hi] = sessionID; + if (hi !== 0) { + return { success: false, error: "sessionID is too large in EventPipe StopTracing command" }; + } + const command: EventPipeCommandStopTracing = { command_set: "EventPipe", command: "StopTracing", sessionID: lo }; + return { success: true, result: command }; +} + +function parseProcessCommand(cmd: BinaryProtocolCommand & { commandSet: CommandSetId.Process }): ParseClientCommandResult { + switch (cmd.command) { + case ProcessCommandId.ProcessInfo: + throw new Error("TODO"); + case ProcessCommandId.ResumeRuntime: + return parseProcessResumeRuntime(cmd); + case ProcessCommandId.ProcessEnvironment: + throw new Error("TODO"); + case ProcessCommandId.ProcessInfo2: + throw new Error("TODO"); + default: + console.warn("unexpected Process command: " + cmd.command); + return { success: false, error: `unexpected Process command ${cmd.command}` }; + } +} + +function parseProcessResumeRuntime(cmd: BinaryProtocolCommand & { commandSet: CommandSetId.Process, command: ProcessCommandId.ResumeRuntime }): ParseClientCommandResult { + const buf = cmd.payload; + if (buf.byteLength !== 0) { + return { success: false, error: "unexpected payload in Process ResumeRuntime command" }; + } + const command: ProcessCommandResumeRuntime = { command_set: "Process", command: "ResumeRuntime" }; + return { success: true, result: command }; +} diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/ipc-protocol/serializer.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/ipc-protocol/serializer.ts new file mode 100644 index 00000000000000..86c6976cb5c0ea --- /dev/null +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/ipc-protocol/serializer.ts @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import Serializer from "./base-serializer"; +import { CommandSetId, ServerCommandId } from "./types"; + +export function createBinaryCommandOKReply(payload?: Uint8Array): Uint8Array { + const len = Serializer.computeMessageByteLength(payload); + const buf = new Uint8Array(len); + const pos = { pos: 0 }; + Serializer.serializeHeader(buf, pos, CommandSetId.Server, ServerCommandId.OK, len); + if (payload !== undefined) { + Serializer.serializePayload(buf, pos, payload); + } + return buf; +} diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/ipc-protocol/types.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/ipc-protocol/types.ts new file mode 100644 index 00000000000000..2031de3499bd3a --- /dev/null +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/ipc-protocol/types.ts @@ -0,0 +1,57 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Just the minimal info we can pull from an IPC message +export interface BinaryProtocolCommand { + commandSet: number; + command: number; + payload: Uint8Array; +} + +export function isBinaryProtocolCommand(x: object): x is BinaryProtocolCommand { + return "commandSet" in x && "command" in x && "payload" in x; +} + +export interface ParseResultBase { + readonly success: boolean; +} + +export interface ParseResultOk extends ParseResultBase { + readonly success: true; +} + +export interface ParseResultFail extends ParseResultBase { + readonly success: false; + readonly error: string; +} + + +export const enum CommandSetId { + Reserved = 0, + Dump = 1, + EventPipe = 2, + Profiler = 3, + Process = 4, + /* future*/ + + // replies + Server = 0xFF, +} + +export const enum EventPipeCommandId { + StopTracing = 1, + CollectTracing = 2, + CollectTracing2 = 3, +} + +export const enum ProcessCommandId { + ProcessInfo = 0, + ResumeRuntime = 1, + ProcessEnvironment = 2, + ProcessInfo2 = 4, +} + +export const enum ServerCommandId { + OK = 0, + Error = 0xFF, +} diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/protocol-socket.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/protocol-socket.ts index 98a35159ce8564..ff93dbd9190d45 100644 --- a/src/mono/wasm/runtime/diagnostics/server_pthread/protocol-socket.ts +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/protocol-socket.ts @@ -2,29 +2,16 @@ // The .NET Foundation licenses this file to you under the MIT license. import type { CommonSocket } from "./common-socket"; -import type { - ProtocolClientCommandBase, - EventPipeClientCommandBase, - EventPipeCommandCollectTracing2, - EventPipeCollectTracingCommandProvider, - EventPipeCommandStopTracing, - ProcessClientCommandBase, - ProcessCommandResumeRuntime, -} from "./protocol-client-commands"; +import { + BinaryProtocolCommand, + ParseResultFail, + ParseResultOk, +} from "./ipc-protocol/types"; +import Magic from "./ipc-protocol/magic"; +import Parser from "./ipc-protocol/base-parser"; export const dotnetDiagnosticsServerProtocolCommandEvent = "dotnet:diagnostics:protocolCommand" as const; -// Just the minimal info we can pull from the -export interface BinaryProtocolCommand { - commandSet: number; - command: number; - payload: Uint8Array; -} - -export function isBinaryProtocolCommand(x: object): x is BinaryProtocolCommand { - return "commandSet" in x && "command" in x && "payload" in x; -} - export interface ProtocolCommandEvent extends Event { type: typeof dotnetDiagnosticsServerProtocolCommandEvent; data: BinaryProtocolCommand; @@ -60,48 +47,21 @@ interface PartialCommandState { } -interface ParseResultBase { - readonly success: boolean; -} - -interface ParseResultOk extends ParseResultBase { - readonly success: true; -} - -interface ParseResultBinaryCommandOk extends ParseResultOk { +export interface ParseResultBinaryCommandOk extends ParseResultOk { readonly command: BinaryProtocolCommand | undefined; readonly newState: State; } -interface ParseResultFail extends ParseResultBase { - readonly success: false; - readonly error: string; -} - -type ParseResult = ParseResultBinaryCommandOk | ParseResultFail; +export type ParseResult = ParseResultBinaryCommandOk | ParseResultFail; -class ProtocolSocketImpl implements ProtocolSocket { +/// A helper object that accumulates command data that is received and provides parsed commands +class StatefulParser { private state: State = { state: InState.Idle }; - private protocolListeners = 0; - private readonly messageListener: (this: CommonSocket, ev: MessageEvent) => void = this.onMessage.bind(this); - constructor(private readonly sock: CommonSocket) { - } - onMessage(this: ProtocolSocketImpl, ev: MessageEvent): void { - console.debug("protocol socket received message", ev.data); - if (typeof ev.data === "object" && ev.data instanceof ArrayBuffer) { - this.onArrayBuffer(ev.data); - } else if (typeof ev.data === "object" && ev.data instanceof Blob) { - ev.data.arrayBuffer().then(this.onArrayBuffer.bind(this)); - } - // otherwise it's string, ignore it. - } - dispatchEvent(evt: Event): boolean { - return this.sock.dispatchEvent(evt); - } + constructor(private readonly emitCommandCallback: (command: BinaryProtocolCommand) => void) { } - onArrayBuffer(this: ProtocolSocketImpl, buf: ArrayBuffer) { - console.debug("protocol-socket: parsing array buffer", buf); + /// process the data in the given buffer and update the state. + receiveBuffer(buf: ArrayBuffer): void { if (this.state.state == InState.Error) { return; } @@ -116,11 +76,7 @@ class ProtocolSocketImpl implements ProtocolSocket { this.setState(result.newState); if (result.command) { const command = result.command; - console.debug("protocol-socket: queueing command", command); - queueMicrotask(() => { - console.debug("dispatching protocol event with command", command); - this.dispatchProtocolCommandEvent(command); - }); + this.emitCommandCallback(command); } } else { console.warn("socket received invalid command header", buf, result.error); @@ -131,7 +87,7 @@ class ProtocolSocketImpl implements ProtocolSocket { tryParseHeader(buf: Uint8Array): ParseResult { const pos = { pos: 0 }; - if (buf.byteLength < Parser.MinimalHeaderSize) { + if (buf.byteLength < Magic.MinimalHeaderSize) { // TODO: we need to see the magic and the size to make a partial commmand return { success: false, error: "not enough data" }; } @@ -139,7 +95,7 @@ class ProtocolSocketImpl implements ProtocolSocket { return { success: false, error: "invalid header" }; } const size = Parser.tryParseSize(buf, pos); - if (size === undefined || size < Parser.MinimalHeaderSize) { + if (size === undefined || size < Magic.MinimalHeaderSize) { return { success: false, error: "invalid size" }; } // make a "partially completed" state with a buffer of the right size and just the header upto the size @@ -179,7 +135,7 @@ class ProtocolSocketImpl implements ProtocolSocket { const newState = { state: InState.PartialCommand, buf, size: partialSize }; return { success: true, command: undefined, newState }; } else { - const pos = { pos: Parser.MinimalHeaderSize }; + const pos = { pos: Magic.MinimalHeaderSize }; let result = this.tryParseCompletedBuffer(buf, pos); if (overflow) { console.warn("additional bytes past command payload", overflow); @@ -201,6 +157,50 @@ class ProtocolSocketImpl implements ProtocolSocket { return { success: true, command, newState: { state: InState.Idle } }; } + private setState(state: State) { + this.state = state; + } + + reset() { + this.setState({ state: InState.Idle }); + } + +} + +class ProtocolSocketImpl implements ProtocolSocket { + private readonly statefulParser = new StatefulParser(this.emitCommandCallback.bind(this)); + private protocolListeners = 0; + private readonly messageListener: (this: CommonSocket, ev: MessageEvent) => void = this.onMessage.bind(this); + constructor(private readonly sock: CommonSocket) { } + + onMessage(this: ProtocolSocketImpl, ev: MessageEvent): void { + console.debug("protocol socket received message", ev.data); + if (typeof ev.data === "object" && ev.data instanceof ArrayBuffer) { + this.onArrayBuffer(ev.data); + } else if (typeof ev.data === "object" && ev.data instanceof Blob) { + ev.data.arrayBuffer().then(this.onArrayBuffer.bind(this)); + } + // otherwise it's string, ignore it. + } + + dispatchEvent(evt: Event): boolean { + return this.sock.dispatchEvent(evt); + } + + onArrayBuffer(this: ProtocolSocketImpl, buf: ArrayBuffer) { + console.debug("protocol-socket: parsing array buffer", buf); + this.statefulParser.receiveBuffer(buf); + } + + // called by the stateful parser when it has a complete command + emitCommandCallback(this: this, command: BinaryProtocolCommand): void { + console.debug("protocol-socket: queueing command", command); + queueMicrotask(() => { + console.debug("dispatching protocol event with command", command); + this.dispatchProtocolCommandEvent(command); + }); + } + dispatchProtocolCommandEvent(cmd: BinaryProtocolCommand): void { const ev = new Event(dotnetDiagnosticsServerProtocolCommandEvent); @@ -227,7 +227,7 @@ class ProtocolSocketImpl implements ProtocolSocket { this.protocolListeners--; if (this.protocolListeners === 0) { this.sock.removeEventListener("message", this.messageListener); - this.setState({ state: InState.Idle }); + this.statefulParser.reset(); } } this.sock.removeEventListener(type, listener); @@ -239,339 +239,12 @@ class ProtocolSocketImpl implements ProtocolSocket { close() { this.sock.close(); + this.statefulParser.reset(); } - private setState(state: State) { - this.state = state; - } } export function createProtocolSocket(socket: CommonSocket): ProtocolSocket { return new ProtocolSocketImpl(socket); } -const Parser = { - magic_buf: null as Uint8Array | null, - get DOTNET_IPC_V1(): Uint8Array { - if (Parser.magic_buf === null) { - const magic = "DOTNET_IPC_V1"; - const magic_len = magic.length + 1; // nul terminated - Parser.magic_buf = new Uint8Array(magic_len); - for (let i = 0; i < magic_len; i++) { - Parser.magic_buf[i] = magic.charCodeAt(i); - } - Parser.magic_buf[magic_len - 1] = 0; - } - return Parser.magic_buf; - }, - get MinimalHeaderSize(): number { - // we just need to see the magic and the size - const sizeOfSize = 2; - return Parser.DOTNET_IPC_V1.byteLength + sizeOfSize; - }, - advancePos(pos: { pos: number }, offset: number) { - pos.pos += offset; - }, - tryParseHeader(buf: Uint8Array, pos: { pos: number }): boolean { - let j = pos.pos; - for (let i = 0; i < Parser.DOTNET_IPC_V1.length; i++) { - if (buf[j++] !== Parser.DOTNET_IPC_V1[i]) { - return false; - } - } - Parser.advancePos(pos, Parser.DOTNET_IPC_V1.length); - return true; - }, - tryParseSize(buf: Uint8Array, pos: { pos: number }): number | undefined { - return Parser.tryParseUint16(buf, pos); - }, - tryParseCommand(buf: Uint8Array, pos: { pos: number }): BinaryProtocolCommand | undefined { - const commandSet = Parser.tryParseUint8(buf, pos); - if (commandSet === undefined) - return undefined; - const command = Parser.tryParseUint8(buf, pos); - if (command === undefined) - return undefined; - if (Parser.tryParseReserved(buf, pos) === undefined) - return undefined; - const payload = buf.slice(pos.pos); - const result = { - commandSet, - command, - payload - }; - return result; - }, - tryParseReserved(buf: Uint8Array, pos: { pos: number }): true | undefined { - const reservedLength = 2; // 2 bytes reserved, must be 0 - for (let i = 0; i < reservedLength; i++) { - const reserved = Parser.tryParseUint8(buf, pos); - if (reserved === undefined || reserved !== 0) { - return undefined; - } - } - return true; - }, - tryParseUint8(buf: Uint8Array, pos: { pos: number }): number | undefined { - const j = pos.pos; - if (j >= buf.byteLength) { - return undefined; - } - const size = buf[j]; - Parser.advancePos(pos, 1); - return size; - }, - tryParseUint16(buf: Uint8Array, pos: { pos: number }): number | undefined { - const j = pos.pos; - if (j + 1 >= buf.byteLength) { - return undefined; - } - const size = (buf[j + 1] << 8) | buf[j]; - Parser.advancePos(pos, 2); - return size; - }, - tryParseUint32(buf: Uint8Array, pos: { pos: number }): number | undefined { - const j = pos.pos; - if (j + 3 >= buf.byteLength) { - return undefined; - } - const size = (buf[j + 3] << 24) | (buf[j + 2] << 16) | (buf[j + 1] << 8) | buf[j]; - Parser.advancePos(pos, 4); - return size; - }, - tryParseUint64(buf: Uint8Array, pos: { pos: number }): [number, number] | undefined { - const lo = Parser.tryParseUint32(buf, pos); - if (lo === undefined) - return undefined; - const hi = Parser.tryParseUint32(buf, pos); - if (hi === undefined) - return undefined; - return [lo, hi]; - }, - tryParseBool(buf: Uint8Array, pos: { pos: number }): boolean | undefined { - const r = Parser.tryParseUint8(buf, pos); - if (r === undefined) - return undefined; - return r !== 0; - }, - tryParseArraySize(buf: Uint8Array, pos: { pos: number }): number | undefined { - const r = Parser.tryParseUint32(buf, pos); - if (r === undefined) - return undefined; - return r; - }, - tryParseStringLength(buf: Uint8Array, pos: { pos: number }): number | undefined { - return Parser.tryParseArraySize(buf, pos); - }, - tryParseUtf16String(buf: Uint8Array, pos: { pos: number }): string | undefined { - const length = Parser.tryParseStringLength(buf, pos); - if (length === undefined) - return undefined; - const j = pos.pos; - if (j + length * 2 > buf.byteLength) { - return undefined; - } - const result = new Array(length); - for (let i = 0; i < length; i++) { - result[i] = (buf[j + 2 * i + 1] << 8) | buf[j + 2 * i]; - } - Parser.advancePos(pos, length * 2); - return String.fromCharCode.apply(null, result); - } -}; - - -const enum CommandSetId { - Reserved = 0, - Dump = 1, - EventPipe = 2, - Profiler = 3, - Process = 4, - /* future*/ - - // replies - Server = 0xFF, -} - -interface ParseClientCommandResultOk extends ParseResultOk { - readonly result: C; -} - -export type ParseClientCommandResult = ParseClientCommandResultOk | ParseResultFail; - -export function parseBinaryProtocolCommand(cmd: BinaryProtocolCommand): ParseClientCommandResult { - switch (cmd.commandSet) { - case CommandSetId.Reserved: - throw new Error("unexpected reserved command_set command"); - case CommandSetId.Dump: - throw new Error("TODO"); - case CommandSetId.EventPipe: - return parseEventPipeCommand(cmd); - case CommandSetId.Profiler: - throw new Error("TODO"); - case CommandSetId.Process: - return parseProcessCommand(cmd); - default: - return { success: false, error: `unexpected command_set ${cmd.commandSet} command` }; - } -} - -const enum EventPipeCommandId { - StopTracing = 1, - CollectTracing = 2, - CollectTracing2 = 3, -} - -function parseEventPipeCommand(cmd: BinaryProtocolCommand & { commandSet: CommandSetId.EventPipe }): ParseClientCommandResult { - switch (cmd.command) { - case EventPipeCommandId.StopTracing: - return parseEventPipeStopTracing(cmd); - case EventPipeCommandId.CollectTracing: - throw new Error("TODO"); - case EventPipeCommandId.CollectTracing2: - return parseEventPipeCollectTracing2(cmd); - default: - console.warn("unexpected EventPipe command: " + cmd.command); - return { success: false, error: `unexpected EventPipe command ${cmd.command}` }; - } -} - -function parseEventPipeCollectTracing2(cmd: BinaryProtocolCommand & { commandSet: CommandSetId.EventPipe, command: EventPipeCommandId.CollectTracing2 }): ParseClientCommandResult { - const pos = { pos: 0 }; - const buf = cmd.payload; - const circularBufferMB = Parser.tryParseUint32(buf, pos); - if (circularBufferMB === undefined) { - return { success: false, error: "failed to parse circularBufferMB in EventPipe CollectTracing2 command" }; - } - const format = Parser.tryParseUint32(buf, pos); - if (format === undefined) { - return { success: false, error: "failed to parse format in EventPipe CollectTracing2 command" }; - } - const requestRundown = Parser.tryParseBool(buf, pos); - if (requestRundown === undefined) { - return { success: false, error: "failed to parse requestRundown in EventPipe CollectTracing2 command" }; - } - const numProviders = Parser.tryParseArraySize(buf, pos); - if (numProviders === undefined) { - return { success: false, error: "failed to parse numProviders in EventPipe CollectTracing2 command" }; - } - const providers = new Array(numProviders); - for (let i = 0; i < numProviders; i++) { - const result = parseEventPipeCollectTracingCommandProvider(buf, pos); - if (!result.success) { - return result; - } - providers[i] = result.result; - } - const command: EventPipeCommandCollectTracing2 = { command_set: "EventPipe", command: "CollectTracing2", circularBufferMB, format, requestRundown, providers }; - return { success: true, result: command }; -} - -function parseEventPipeCollectTracingCommandProvider(buf: Uint8Array, pos: { pos: number }): ParseClientCommandResult { - const keywords = Parser.tryParseUint64(buf, pos); - if (keywords === undefined) { - return { success: false, error: "failed to parse keywords in EventPipe CollectTracing provider" }; - } - const logLevel = Parser.tryParseUint32(buf, pos); - if (logLevel === undefined) - return { success: false, error: "failed to parse logLevel in EventPipe CollectTracing provider" }; - const providerName = Parser.tryParseUtf16String(buf, pos); - if (providerName === undefined) - return { success: false, error: "failed to parse providerName in EventPipe CollectTracing provider" }; - const filterData = Parser.tryParseUtf16String(buf, pos); - if (filterData === undefined) - return { success: false, error: "failed to parse filterData in EventPipe CollectTracing provider" }; - const provider: EventPipeCollectTracingCommandProvider = { keywords, logLevel, provider_name: providerName, filter_data: filterData }; - return { success: true, result: provider }; -} - -function parseEventPipeStopTracing(cmd: BinaryProtocolCommand & { commandSet: CommandSetId.EventPipe, command: EventPipeCommandId.StopTracing }): ParseClientCommandResult { - const pos = { pos: 0 }; - const buf = cmd.payload; - const sessionID = Parser.tryParseUint64(buf, pos); - if (sessionID === undefined) { - return { success: false, error: "failed to parse sessionID in EventPipe StopTracing command" }; - } - const [lo, hi] = sessionID; - if (hi !== 0) { - return { success: false, error: "sessionID is too large in EventPipe StopTracing command" }; - } - const command: EventPipeCommandStopTracing = { command_set: "EventPipe", command: "StopTracing", sessionID: lo }; - return { success: true, result: command }; -} - -const enum ProcessCommandId { - ProcessInfo = 0, - ResumeRuntime = 1, - ProcessEnvironment = 2, - ProcessInfo2 = 4, -} - -function parseProcessCommand(cmd: BinaryProtocolCommand & { commandSet: CommandSetId.Process }): ParseClientCommandResult { - switch (cmd.command) { - case ProcessCommandId.ProcessInfo: - throw new Error("TODO"); - case ProcessCommandId.ResumeRuntime: - return parseProcessResumeRuntime(cmd); - case ProcessCommandId.ProcessEnvironment: - throw new Error("TODO"); - case ProcessCommandId.ProcessInfo2: - throw new Error("TODO"); - default: - console.warn("unexpected Process command: " + cmd.command); - return { success: false, error: `unexpected Process command ${cmd.command}` }; - } -} - -function parseProcessResumeRuntime(cmd: BinaryProtocolCommand & { commandSet: CommandSetId.Process, command: ProcessCommandId.ResumeRuntime }): ParseClientCommandResult { - const buf = cmd.payload; - if (buf.byteLength !== 0) { - return { success: false, error: "unexpected payload in Process ResumeRuntime command" }; - } - const command: ProcessCommandResumeRuntime = { command_set: "Process", command: "ResumeRuntime" }; - return { success: true, result: command }; -} - -const enum ServerCommandId { - OK = 0, - Error = 0xFF, -} - -const Serializer = { - advancePos(pos: { pos: number }, count: number): void { - pos.pos += count; - }, - serializeMagic(buf: Uint8Array, pos: { pos: number }): void { - buf.set(Parser.DOTNET_IPC_V1, pos.pos); - Serializer.advancePos(pos, Parser.DOTNET_IPC_V1.byteLength); - }, - serializeUint8(buf: Uint8Array, pos: { pos: number }, value: number): void { - buf[pos.pos++] = value; - }, - serializeUint16(buf: Uint8Array, pos: { pos: number }, value: number): void { - buf[pos.pos++] = value & 0xFF; - buf[pos.pos++] = (value >> 8) & 0xFF; - }, - serializeHeader(buf: Uint8Array, pos: { pos: number }, commandSet: CommandSetId, command: ServerCommandId, len: number): void { - Serializer.serializeMagic(buf, pos); - Serializer.serializeUint16(buf, pos, len); - Serializer.serializeUint8(buf, pos, commandSet); - Serializer.serializeUint8(buf, pos, command); - Serializer.serializeUint16(buf, pos, 0); // reserved - } -}; - -export function createBinaryCommandOKReply(payload?: Uint8Array): Uint8Array { - const fullHeaderSize = Parser.MinimalHeaderSize // magic, len - + 2 // commandSet, command - + 2; // reserved ; - const len = fullHeaderSize + (payload !== undefined ? payload.byteLength : 0); // magic, size, commandSet, command, reserved - const buf = new Uint8Array(len); - const pos = { pos: 0 }; - Serializer.serializeHeader(buf, pos, CommandSetId.Server, ServerCommandId.OK, len); - if (payload !== undefined) { - buf.set(payload, pos.pos); - Serializer.advancePos(pos, payload.byteLength); - } - return buf; -} diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/socket-connection.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/socket-connection.ts index 1a508b8b6bfb0c..d843c0c72eed1a 100644 --- a/src/mono/wasm/runtime/diagnostics/server_pthread/socket-connection.ts +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/socket-connection.ts @@ -6,7 +6,7 @@ import { VoidPtr } from "../../types/emscripten"; import { Module } from "../../imports"; import type { CommonSocket } from "./common-socket"; enum ListenerState { - SendingTrailingData, + Sending, Closed, Error } @@ -25,11 +25,15 @@ class SocketGuts { } } + +/// A wrapper around a WebSocket that just sends data back to the host. +/// It sets up message and clsoe handlers on the WebSocket tht put it into an idle state +/// if the connection closes or we receive any replies. export class EventPipeSocketConnection { private _state: ListenerState; readonly stream: SocketGuts; constructor(readonly socket: CommonSocket) { - this._state = ListenerState.SendingTrailingData; + this._state = ListenerState.Sending; this.stream = new SocketGuts(socket); } @@ -48,7 +52,7 @@ export class EventPipeSocketConnection { write(ptr: VoidPtr, len: number): boolean { switch (this._state) { - case ListenerState.SendingTrailingData: + case ListenerState.Sending: this.stream.write(ptr, len); return true; case ListenerState.Closed: @@ -61,7 +65,7 @@ export class EventPipeSocketConnection { private _onMessage(event: MessageEvent): void { switch (this._state) { - case ListenerState.SendingTrailingData: + case ListenerState.Sending: /* unexpected message */ console.warn("EventPipe session stream received unexpected message from websocket", event); // TODO notify runtime that the connection had an error @@ -106,6 +110,8 @@ export class EventPipeSocketConnection { } } +/// Take over a WebSocket that was used by the diagnostic server to receive the StartCollecting command and +/// use it for sending the event pipe data back to the host. export function takeOverSocket(socket: CommonSocket): EventPipeSocketConnection { const connection = new EventPipeSocketConnection(socket); connection.addListeners(); diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/stream-queue.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/stream-queue.ts index 7fe3da797b685d..61dff8c80a3e40 100644 --- a/src/mono/wasm/runtime/diagnostics/server_pthread/stream-queue.ts +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/stream-queue.ts @@ -12,29 +12,18 @@ import * as Memory from "../../memory"; // struct MonoWasmEventPipeStreamQueue { // union { void* buf; intptr_t close_msg; /* -1 */ }; // int32_t count; -// volatile int32_t write_done; +// volatile int32_t buf_full; // } // -// To write, the streaming thread does: -// -// int32_t queue_write (MonoWasmEventPipeSteamQueue *queue, uint8_t *buf, int32_t len, int32_t *bytes_written) -// { -// queue->buf = buf; -// queue->count = len; -// //WISH: mono_wasm_memory_atomic_notify (&queue->wakeup_write, 1); // __builtin_wasm_memory_atomic_notify((int*)addr, count); -// emscripten_dispatch_to_thread (diagnostic_thread_id, wakeup_stream_queue, queue); -// int r = mono_wasm_memory_atomic_wait (&queue->wakeup_write_done, 0, -1); // __builtin_wasm_memory_atomic_wait((int*)addr, expected, timeout); // returns 0 ok, 1 not_equal, 2 timed out -// if (G_UNLIKELY (r != 0) { -// return -1; -// } -// result = Atomics.load (wakeup_write_done); // 0 or errno -// if (bytes_writen) *bytes_written = len; -// mono_atomic_store_int32 (&queue->wakeup_write_done, 0); -// -// This would be a lot less hacky if more browsers implemented Atomics.waitAsync. -// Then we wouldn't have to use emscripten_dispatch_to_thread, and instead the diagnostic server could -// just call Atomics.waitAsync to wait for the streaming thread to write. +// To write, the streaming thread: +// 1. sets buf (or close_msg) and count, and then atomically sets buf_full. +// 2. queues mono_wasm_diagnostic_server_stream_signal_work_available to run on the diagnostic server thread +// 3. waits for buf_full to be 0. // +// Note this is a little bit fragile if there are multiple writers. +// There _are_ multiple writers - when the streaming session first starts, either the diagnostic server thread +// or the main thread write to the queue before the streaming thread starts. But those actions are +// implicitly serialized because the streaming thread isn't started until the writes are done. const BUF_OFFSET = 0; const COUNT_OFFSET = 4; diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/streaming-session.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/streaming-session.ts index fad8a706c60a86..938db52bd29fb3 100644 --- a/src/mono/wasm/runtime/diagnostics/server_pthread/streaming-session.ts +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/streaming-session.ts @@ -14,15 +14,19 @@ import { } from "./protocol-client-commands"; import { createEventPipeStreamingSession } from "../shared/create-session"; +/// The streaming session holds all the pieces of an event pipe streaming session that the +/// diagnostic server knows about: the session ID, a +/// queue used by the EventPipe streaming thread to forward events to the diagnostic server thread, +/// and a wrapper around the WebSocket object used to send event data back to the host. export class EventPipeStreamingSession { - constructor(readonly sessionID: EventPipeSessionIDImpl, readonly ws: WebSocket | MockRemoteSocket, + constructor(readonly sessionID: EventPipeSessionIDImpl, readonly queue: StreamQueue, readonly connection: EventPipeSocketConnection) { } } export async function makeEventPipeStreamingSession(ws: WebSocket | MockRemoteSocket, cmd: EventPipeCommandCollectTracing2): Promise { // First, create the native IPC stream and get its queue. const ipcStreamAddr = cwraps.mono_wasm_diagnostic_server_create_stream(); // FIXME: this should be a wrapped in a JS object so we can free it when we're done. - const queueAddr = mono_wasm_diagnostic_server_get_stream_queue(ipcStreamAddr); + const queueAddr = getQueueAddrFromStreamAddr(ipcStreamAddr); // then take over the websocket connection const conn = takeOverSocket(ws); // and set up queue notifications @@ -36,7 +40,7 @@ export async function makeEventPipeStreamingSession(ws: WebSocket | MockRemoteSo const sessionID = createEventPipeStreamingSession(ipcStreamAddr, options); if (sessionID === false) throw new Error("failed to create event pipe session"); - return new EventPipeStreamingSession(sessionID, ws, queue, conn); + return new EventPipeStreamingSession(sessionID, queue, conn); } @@ -61,7 +65,6 @@ function providersStringFromObject(providers: EventPipeCollectTracingCommandProv const IPC_STREAM_QUEUE_OFFSET = 4; /* keep in sync with mono_wasm_diagnostic_server_create_stream() in C */ -function mono_wasm_diagnostic_server_get_stream_queue(streamAddr: VoidPtr): VoidPtr { - // TODO: this can probably be in JS if we put the queue at a known address in the stream. (probably offset 4); +function getQueueAddrFromStreamAddr(streamAddr: VoidPtr): VoidPtr { return streamAddr + IPC_STREAM_QUEUE_OFFSET; } diff --git a/src/mono/wasm/runtime/diagnostics/shared/controller-commands.ts b/src/mono/wasm/runtime/diagnostics/shared/controller-commands.ts index 3f03c746b81c03..5e08f56c627ea4 100644 --- a/src/mono/wasm/runtime/diagnostics/shared/controller-commands.ts +++ b/src/mono/wasm/runtime/diagnostics/shared/controller-commands.ts @@ -1,9 +1,20 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -import { DiagnosticMessage } from "./types"; +import type { MonoThreadMessage } from "../../pthreads/shared"; +import { isMonoThreadMessage } from "../../pthreads/shared"; -/// Commands from the main thread to the diagnostic server +// Messages from the main thread to the diagnostic server thread +export interface DiagnosticMessage extends MonoThreadMessage { + type: "diagnostic_server"; + cmd: string; +} + +export function isDiagnosticMessage(x: unknown): x is DiagnosticMessage { + return isMonoThreadMessage(x) && x.type === "diagnostic_server"; +} + +/// Commands from the diagnostic server controller on the main thread to the diagnostic server export type DiagnosticServerControlCommand = | DiagnosticServerControlCommandStart | DiagnosticServerControlCommandStop diff --git a/src/mono/wasm/runtime/diagnostics/shared/types.ts b/src/mono/wasm/runtime/diagnostics/shared/types.ts index b15e23b3891270..af5de7c4729374 100644 --- a/src/mono/wasm/runtime/diagnostics/shared/types.ts +++ b/src/mono/wasm/runtime/diagnostics/shared/types.ts @@ -1,18 +1,5 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -import type { MonoThreadMessage } from "../../pthreads/shared"; -import { isMonoThreadMessage } from "../../pthreads/shared"; - export type EventPipeSessionIDImpl = number; -export interface DiagnosticMessage extends MonoThreadMessage { - type: "diagnostic_server"; - cmd: string; -} - -export function isDiagnosticMessage(x: unknown): x is DiagnosticMessage { - return isMonoThreadMessage(x) && x.type === "diagnostic_server"; -} - - From 0d1eefe237204a2e3900ca5895c69bb71103259a Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Tue, 19 Jul 2022 13:12:42 -0400 Subject: [PATCH 63/84] help treeshaking verified that all the DS and EP JS code is dropped if monoWasmThreads is falsed. --- src/mono/wasm/runtime/diagnostics/index.ts | 90 +++++++++++++--------- 1 file changed, 53 insertions(+), 37 deletions(-) diff --git a/src/mono/wasm/runtime/diagnostics/index.ts b/src/mono/wasm/runtime/diagnostics/index.ts index 9f6ba4ed76ae09..ce179c85fe3f73 100644 --- a/src/mono/wasm/runtime/diagnostics/index.ts +++ b/src/mono/wasm/runtime/diagnostics/index.ts @@ -1,6 +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 monoWasmThreads from "consts:monoWasmThreads"; import type { DiagnosticOptions, EventPipeSessionOptions, @@ -30,12 +31,14 @@ let startup_sessions: (EventPipeSession | null)[] | null = null; // called from C on the main thread export function mono_wasm_event_pipe_early_startup_callback(): void { - if (startup_session_configs === null || startup_session_configs.length == 0) { - return; + if (monoWasmThreads) { + if (startup_session_configs === null || startup_session_configs.length == 0) { + return; + } + console.debug("diagnostics: setting startup sessions based on startup session configs", startup_session_configs); + startup_sessions = startup_session_configs.map(config => createAndStartEventPipeSession(config)); + startup_session_configs = []; } - console.debug("diagnostics: setting startup sessions based on startup session configs", startup_session_configs); - startup_sessions = startup_session_configs.map(config => createAndStartEventPipeSession(config)); - startup_session_configs = []; } @@ -49,26 +52,34 @@ function createAndStartEventPipeSession(options: (EventPipeSessionOptions)): Eve return session; } +function getDiagnostics(): Diagnostics { + if (monoWasmThreads) { + return { + /// An enumeration of the level (higher value means more detail): + /// LogAlways: 0, + /// Critical: 1, + /// Error: 2, + /// Warning: 3, + /// Informational: 4, + /// Verbose: 5, + EventLevel: eventLevel, + /// A builder for creating an EventPipeSessionOptions instance. + SessionOptionsBuilder: SessionOptionsBuilder, + /// Creates a new EventPipe session that will collect trace events from the runtime and managed libraries. + /// Use the options to control the kinds of events to be collected. + /// Multiple sessions may be created and started at the same time. + createEventPipeSession: makeEventPipeSession, + getStartupSessions(): (EventPipeSession | null)[] { + return Array.from(startup_sessions || []); + }, + }; + } else { + return undefined as unknown as Diagnostics; + } +} + /// APIs for working with .NET diagnostics from JavaScript. -export const diagnostics: Diagnostics = { - /// An enumeration of the level (higher value means more detail): - /// LogAlways: 0, - /// Critical: 1, - /// Error: 2, - /// Warning: 3, - /// Informational: 4, - /// Verbose: 5, - EventLevel: eventLevel, - /// A builder for creating an EventPipeSessionOptions instance. - SessionOptionsBuilder: SessionOptionsBuilder, - /// Creates a new EventPipe session that will collect trace events from the runtime and managed libraries. - /// Use the options to control the kinds of events to be collected. - /// Multiple sessions may be created and started at the same time. - createEventPipeSession: makeEventPipeSession, - getStartupSessions(): (EventPipeSession | null)[] { - return Array.from(startup_sessions || []); - }, -}; +export const diagnostics: Diagnostics = getDiagnostics(); // Initialization flow /// * The runtime calls configure_diagnostics with options from MonoConfig @@ -84,22 +95,27 @@ let suspendOnStartup = false; let diagnosticsServerEnabled = false; export async function mono_wasm_init_diagnostics(options: DiagnosticOptions): Promise { - if (!is_nullish(options.server)) { - if (options.server.connect_url === undefined || typeof (options.server.connect_url) !== "string") { - throw new Error("server.connect_url must be a string"); - } - const url = options.server.connect_url; - const suspend = boolsyOption(options.server.suspend); - const controller = await startDiagnosticServer(url); - if (controller) { - diagnosticsServerEnabled = true; - if (suspend) { - suspendOnStartup = true; + if (!monoWasmThreads) { + console.warn("ignoring diagnostics options because this runtime does not support diagnostics", options); + return; + } else { + if (!is_nullish(options.server)) { + if (options.server.connect_url === undefined || typeof (options.server.connect_url) !== "string") { + throw new Error("server.connect_url must be a string"); + } + const url = options.server.connect_url; + const suspend = boolsyOption(options.server.suspend); + const controller = await startDiagnosticServer(url); + if (controller) { + diagnosticsServerEnabled = true; + if (suspend) { + suspendOnStartup = true; + } } } + const sessions = options?.sessions ?? []; + startup_session_configs.push(...sessions); } - const sessions = options?.sessions ?? []; - startup_session_configs.push(...sessions); } function boolsyOption(x: string | boolean): boolean { From be703f255cc94ef4a4d94b92dae0eb8183741804 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Tue, 19 Jul 2022 14:00:43 -0400 Subject: [PATCH 64/84] update DS design notes --- .../runtime/diagnostics/diagnostic-server.md | 55 +++---------------- 1 file changed, 9 insertions(+), 46 deletions(-) diff --git a/src/mono/wasm/runtime/diagnostics/diagnostic-server.md b/src/mono/wasm/runtime/diagnostics/diagnostic-server.md index 081967a362df07..d2413645326d51 100644 --- a/src/mono/wasm/runtime/diagnostics/diagnostic-server.md +++ b/src/mono/wasm/runtime/diagnostics/diagnostic-server.md @@ -19,9 +19,8 @@ We will need a dedicated thread to handle the WebSocket connections. This threa - The diagnostic worker needs to start before the runtime starts - we have to be able to accept connections in "wait to start" mode to do startup profiling. -- The diagnostic worker needs to be able to send commands to the runtime. Or to directlly start sessions. -- The runtime needs to be able to send events from (some? all?) thread to the server. -- The diagnostic worker needs to be able to notify the runtime when the connection is closed. Or to directly stop sessions. +- WebSocket JS objects are not transferable between WebWorkers. So the diagnostic server worker needs to +forward streamed eventpipe data from the EventPipe session streaming thread to the WebSocket. ## Make the diagnostic Worker a pthread @@ -31,49 +30,13 @@ Early during runtime startup if the appropriate bits are set, we will call into The problem is if the diagnostic URL has a "suspend" option, the Worker should wait in JS for a resume command and then post a message back to the main thread to resume. -So we need to create a promise in the main thread that becomes resolved when we receive some kind of notification from the worker. We could use the `Atomics.waitAsync` to make a promise on the main thread that will resolve when the memory changes. +One idea was to use a promise on the main thread to wait for the diagnostic server to signal us. But that would be too early - before `mono_wasm_load_runtime` runs - and unfortunately the DS server needs to be able to create EventPipe session IDs before resuming the runtime. If we could break up `mono_wasm_load_runtime` into several callees we could set up the runtime threading and minimal EventPipie initialization and then pause until the resume. -## DS IPC stream - -the native code for an EP session uses an `IpcStream` object to do the actual reading and writing which has a vtable of callbacks to provide the implementation. -We can use the `ds-ipc-pal-socket.c` implementation as a guide - essentially there's an `IpcStream` subclass that -has a custom vtable that has pointers to the functions to call. - -Once we're sending the actual payloads, we can wrap up the bytes in a JS buffer and pass it over to websocket. - -For this to work well we probably need the diagnostic Worker to be a pthread? - -It would be nice if we didn't have to do our own message queue. - -## Make our own MessagePort - -the emscripten onmessage handler in the worker errors on unknown messages. the emscripten onmessage handler in the main thread ignores unknown messages. - -So when mono starts a thread it can send over a port to the main thread. +But instead right now we busy-wait in the main thread in `ds_server_wasm_pause_for_diagnostics_monitor`. This at least processes the Emscripten dispatch queue (so other pthreads can do syscalls), but it hangs the browser UI. -then the main thread can talk to the worker thread. - -There's a complication here that we need to be careful because emscripten reuses workers for pthreads. but if we only ever talk over the dedicated channel, it's probably good. - -Emscripten `pthread_t` is a pointer. hope...fully... they're not reused. - -Basically make `mono_thread_create_internal` run some JS that - -## TODO - -- [browser] in dotnet preInit read the config options. extract websocket URL and whether to suspend. -- [browser] call down to C to start diagnostics -- [browser] create a pthread and have it call the JS diagnostic server start and then set it to not die after thread main returns. return pthread id to JS. -- [server_worker] start listening on the URL. -- [browser] if suspending, listen for a continue event from the JS and await for that to resolve. -- [server_worker] when there's a session start event, if we were suspended, add a pending session, and send a continue event. -- [server_worker] wait for a "runtime started" event? -- [server_worker] when there's a new session start event, call down to C to start and EP session. **FIXME** need a port at this point - -- [browser] in early startup callback, start any pending EP sessions (and create ports for them to the diagnostic server) -- [session_streamer] call out to JS to post messages to the diagnostic server. -- [browser] in C, fire events, which will wake up the session streamer -- [session_streamer] post more messages - -So the tricky bit is that for startup sessions and for "course of running" sessions, we need the browser thread to originate the message port transfer. (hopefully queuing work on the main thread is good enough?) +## DS IPC stream -Also the streamer thread probably needs to do a bunch of preliminary work in asynchronous JS before it can begin serving sessions. +The native code for an EP session uses an `IpcStream` object to do the actual reading and writing which has a vtable of callbacks to provide the implementation. +We implement our own `WasmIpcStream` that has a 1-element single-writer single-reader queue so that synchronous writes from the eventpipe streaming threads wake the diagnostic server to pull the filled buffer +and send it over the websocket. +There's no particular reason why this has to be (1) synchronous, (2) 1-element. Although that would make the implementation more complicated. If there's a perf issue here we could look into something more sophisticated. From 7a63e7e970aa43775ceba45b42c592d4936889eb Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Tue, 19 Jul 2022 14:20:56 -0400 Subject: [PATCH 65/84] remove more printfs --- src/mono/wasm/runtime/driver.c | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/mono/wasm/runtime/driver.c b/src/mono/wasm/runtime/driver.c index f7d74da39668bb..a4108177548f07 100644 --- a/src/mono/wasm/runtime/driver.c +++ b/src/mono/wasm/runtime/driver.c @@ -33,8 +33,6 @@ #include "wasm-config.h" #include "pinvoke.h" -#include - #ifdef GEN_PINVOKE #include "wasm_m2n_invoke.g.h" #endif @@ -485,9 +483,6 @@ cleanup_runtime_config (MonovmRuntimeConfigArguments *args, void *user_data) EMSCRIPTEN_KEEPALIVE void mono_wasm_load_runtime (const char *unused, int debug_level) { - if (!emscripten_is_main_browser_thread ()) - printf ("load_runtime called in a worker!\n"); - const char *interp_opts = ""; #ifndef INVARIANT_GLOBALIZATION @@ -611,9 +606,7 @@ mono_wasm_load_runtime (const char *unused, int debug_level) mono_wasm_register_bundled_satellite_assemblies (); mono_trace_init (); mono_trace_set_log_handler (wasm_trace_logger, NULL); - printf ("wasm: before mono_jit_init_version\n"); root_domain = mono_jit_init_version ("mono", NULL); - printf ("wasm: after mono_jit_init_version\n"); mono_initialize_internals(); From e5d1838c80e7a6f926478e9c5493279c9b8a1252 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Tue, 19 Jul 2022 14:31:44 -0400 Subject: [PATCH 66/84] use PromiseController in more places --- .../wasm/runtime/pthreads/browser/index.ts | 27 ++++++++----------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/src/mono/wasm/runtime/pthreads/browser/index.ts b/src/mono/wasm/runtime/pthreads/browser/index.ts index f4e4bafcfed0b2..9f6aa5f6bf2e55 100644 --- a/src/mono/wasm/runtime/pthreads/browser/index.ts +++ b/src/mono/wasm/runtime/pthreads/browser/index.ts @@ -4,6 +4,7 @@ import { Module } from "../../imports"; import { pthread_ptr, MonoWorkerMessageChannelCreated, isMonoWorkerMessageChannelCreated, monoSymbol } from "../shared"; import { MonoThreadMessage } from "../shared"; +import { PromiseController, createPromiseController } from "../../promise-controller"; const threads: Map = new Map(); @@ -21,33 +22,27 @@ class ThreadImpl implements Thread { } } -interface ThreadCreateResolveReject { - resolve: (thread: Thread) => void, - reject: (error: Error) => void -} - -const thread_promises: Map = new Map(); +const thread_promises: Map[]> = new Map(); /// wait until the thread with the given id has set up a message port to the runtime export function waitForThread(pthread_ptr: pthread_ptr): Promise { if (threads.has(pthread_ptr)) { return Promise.resolve(threads.get(pthread_ptr)!); } - const promise: Promise = new Promise((resolve, reject) => { - const arr = thread_promises.get(pthread_ptr); - if (arr === undefined) { - thread_promises.set(pthread_ptr, new Array({ resolve, reject })); - } else { - arr.push({ resolve, reject }); - } - }); - return promise; + const promiseAndController = createPromiseController(); + const arr = thread_promises.get(pthread_ptr); + if (arr === undefined) { + thread_promises.set(pthread_ptr, [promiseAndController.promise_control]); + } else { + arr.push(promiseAndController.promise_control); + } + return promiseAndController.promise; } function resolvePromises(pthread_ptr: pthread_ptr, thread: Thread): void { const arr = thread_promises.get(pthread_ptr); if (arr !== undefined) { - arr.forEach(({ resolve }) => resolve(thread)); + arr.forEach((controller) => controller.resolve(thread)); thread_promises.delete(pthread_ptr); } } From d92d45fc8c16eefc2d76c7d5c5d1b8dbb198f847 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Tue, 19 Jul 2022 14:35:26 -0400 Subject: [PATCH 67/84] remove more console.debug in startup --- src/mono/wasm/runtime/startup.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/mono/wasm/runtime/startup.ts b/src/mono/wasm/runtime/startup.ts index 8dcddc5467902d..da2b549cc1877a 100644 --- a/src/mono/wasm/runtime/startup.ts +++ b/src/mono/wasm/runtime/startup.ts @@ -31,8 +31,6 @@ export const mono_wasm_runtime_is_initialized = new GuardedPromise((resolv runtime_is_initialized_reject = reject; }); -const IS_WORKER = typeof (self) !== "undefined" && typeof ((self).importScripts) !== "undefined"; - let ctx: DownloadAssetsContext | null = null; export function configure_emscripten_startup(module: DotnetModule, exportedAPI: DotnetPublicAPI): void { @@ -126,11 +124,6 @@ export function configure_emscripten_startup(module: DotnetModule, exportedAPI: async function mono_wasm_pre_init(): Promise { const moduleExt = Module as DotnetModule; - if (IS_WORKER) { - console.debug("mono_wasm_pre_init running in a worker"); - - } - Module.addRunDependency("mono_wasm_pre_init"); // wait for locateFile setup on NodeJs From 530fa1c5913a536eda46b05e804754ac7705dec5 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Tue, 19 Jul 2022 16:34:21 -0400 Subject: [PATCH 68/84] fix Windows build --- src/mono/mono/component/event_pipe-wasm.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mono/mono/component/event_pipe-wasm.h b/src/mono/mono/component/event_pipe-wasm.h index b8853281a61dd0..3ed49233b90834 100644 --- a/src/mono/mono/component/event_pipe-wasm.h +++ b/src/mono/mono/component/event_pipe-wasm.h @@ -6,13 +6,13 @@ #define _MONO_COMPONENT_EVENT_PIPE_WASM_H #include -#include #include #include #include #ifdef HOST_WASM +#include #include G_BEGIN_DECLS From ca2f2041acebe85385d4a3a6b9e95c67a18bf586 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Tue, 19 Jul 2022 16:10:53 -0400 Subject: [PATCH 69/84] add MONO_WASM prefix to console logging outputs --- .../runtime/diagnostics/browser/controller.ts | 12 ++++----- .../diagnostics/browser/file-session.ts | 8 +++--- src/mono/wasm/runtime/diagnostics/index.ts | 4 +-- .../diagnostics/server_pthread/index.ts | 26 +++++++++---------- .../server_pthread/ipc-protocol/parser.ts | 4 +-- .../server_pthread/protocol-socket.ts | 18 ++++++------- .../server_pthread/socket-connection.ts | 2 +- 7 files changed, 37 insertions(+), 37 deletions(-) diff --git a/src/mono/wasm/runtime/diagnostics/browser/controller.ts b/src/mono/wasm/runtime/diagnostics/browser/controller.ts index 79cd9a7a498417..b7cdd6f0f7e5ff 100644 --- a/src/mono/wasm/runtime/diagnostics/browser/controller.ts +++ b/src/mono/wasm/runtime/diagnostics/browser/controller.ts @@ -16,15 +16,15 @@ class ServerControllerImpl implements ServerController { server.port.addEventListener("message", this.onServerReply.bind(this)); } start(): void { - console.debug("signaling the diagnostic server to start"); + console.debug("MONO_WASM: signaling the diagnostic server to start"); this.server.postMessageToWorker(makeDiagnosticServerControlCommand("start")); } stop(): void { - console.debug("signaling the diagnostic server to stop"); + console.debug("MONO_WASM: signaling the diagnostic server to stop"); this.server.postMessageToWorker(makeDiagnosticServerControlCommand("stop")); } postServerAttachToRuntime(): void { - console.debug("signal the diagnostic server to attach to the runtime"); + console.debug("MONO_WASM: signal the diagnostic server to attach to the runtime"); this.server.postMessageToWorker(makeDiagnosticServerControlCommand("attach_to_runtime")); } @@ -33,7 +33,7 @@ class ServerControllerImpl implements ServerController { if (isDiagnosticMessage(d)) { switch (d.cmd) { default: - console.warn("Unknown control reply command: ", d); + console.warn("MONO_WASM: Unknown control reply command: ", d); break; } } @@ -50,7 +50,7 @@ export function getController(): ServerController { export async function startDiagnosticServer(websocket_url: string): Promise { const sizeOfPthreadT = 4; - console.debug(`starting the diagnostic server url: ${websocket_url}`); + console.info(`MONO_WASM: starting the diagnostic server url: ${websocket_url}`); const result: number | undefined = withStackAlloc(sizeOfPthreadT, (pthreadIdPtr) => { if (!cwraps.mono_wasm_diagnostic_server_create_thread(websocket_url, pthreadIdPtr)) return undefined; @@ -58,7 +58,7 @@ export async function startDiagnosticServer(websocket_url: string): Promise { if (this._state !== State.Initialized) { - throw new Error(`EventPipe session ${this.sessionID} already started`); + throw new Error(`MONO_WASM: EventPipe session ${this.sessionID} already started`); } this._state = State.Started; start_streaming(this._sessionID); - console.debug(`EventPipe session ${this.sessionID} started`); + console.debug(`MONO_WASM: EventPipe session ${this.sessionID} started`); } stop = () => { @@ -56,7 +56,7 @@ class EventPipeFileSession implements EventPipeSession { } this._state = State.Done; stop_streaming(this._sessionID); - console.debug(`EventPipe session ${this.sessionID} stopped`); + console.debug(`MONO_WASM: EventPipe session ${this.sessionID} stopped`); } getTraceBlob = () => { diff --git a/src/mono/wasm/runtime/diagnostics/index.ts b/src/mono/wasm/runtime/diagnostics/index.ts index ce179c85fe3f73..c42a8715022764 100644 --- a/src/mono/wasm/runtime/diagnostics/index.ts +++ b/src/mono/wasm/runtime/diagnostics/index.ts @@ -35,7 +35,7 @@ export function mono_wasm_event_pipe_early_startup_callback(): void { if (startup_session_configs === null || startup_session_configs.length == 0) { return; } - console.debug("diagnostics: setting startup sessions based on startup session configs", startup_session_configs); + console.debug("MONO_WASM: diagnostics: setting startup sessions based on startup session configs", startup_session_configs); startup_sessions = startup_session_configs.map(config => createAndStartEventPipeSession(config)); startup_session_configs = []; } @@ -96,7 +96,7 @@ let diagnosticsServerEnabled = false; export async function mono_wasm_init_diagnostics(options: DiagnosticOptions): Promise { if (!monoWasmThreads) { - console.warn("ignoring diagnostics options because this runtime does not support diagnostics", options); + console.warn("MONO_WASM: ignoring diagnostics options because this runtime does not support diagnostics", options); return; } else { if (!is_nullish(options.server)) { diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts index 03b2acaa84be16..393bf312df2666 100644 --- a/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts @@ -108,7 +108,7 @@ class DiagnosticServerImpl implements DiagnosticServer { await this.startRequestedController.promise; await this.attachToRuntimeController.promise; // can't start tracing until we've attached to the runtime while (!this.stopRequested) { - console.debug("diagnostic server: advertising and waiting for client"); + console.debug("MONO_WASM: diagnostic server: advertising and waiting for client"); const p1: Promise<"first" | "second"> = this.advertiseAndWaitForClient().then(() => "first"); const p2: Promise<"first" | "second"> = this.stopRequestedController.promise.then(() => "second"); const result = await Promise.race([p1, p2]); @@ -116,7 +116,7 @@ class DiagnosticServerImpl implements DiagnosticServer { case "first": break; case "second": - console.debug("stop requested"); + console.debug("MONO_WASM: stop requested"); break; default: assertNever(result); @@ -146,7 +146,7 @@ class DiagnosticServerImpl implements DiagnosticServer { } this.sendAdvertise(ws); const message = await p; - console.debug("received advertising response: ", message); + console.debug("MONO_WASM: received advertising response: ", message); queueMicrotask(() => this.parseAndDispatchMessage(ws, message)); } finally { // if there were errors, resume the runtime anyway @@ -158,14 +158,14 @@ class DiagnosticServerImpl implements DiagnosticServer { try { const cmd = this.parseCommand(message); if (cmd === null) { - console.error("unexpected message from client", message); + console.error("MONO_WASM: unexpected message from client", message); return; } else if (isEventPipeCommand(cmd)) { await this.dispatchEventPipeCommand(ws, cmd); } else if (isProcessCommand(cmd)) { await this.dispatchProcessCommand(ws, cmd); // resume } else { - console.warn("Client sent unknown command", cmd); + console.warn("MONO_WASM Client sent unknown command", cmd); } } finally { // if there were errors, resume the runtime anyway @@ -211,12 +211,12 @@ class DiagnosticServerImpl implements DiagnosticServer { if (typeof message.data === "string") { return parseMockCommand(message.data); } else { - console.debug("parsing byte command: ", message.data); + console.debug("MONO_WASM: parsing byte command: ", message.data); const result = parseProtocolCommand(message.data); if (result.success) { return result.result; } else { - console.warn("failed to parse command: ", result.error); + console.warn("MONO_WASM: failed to parse command: ", result.error); return null; } } @@ -242,7 +242,7 @@ class DiagnosticServerImpl implements DiagnosticServer { this.attachToRuntime(); break; default: - console.warn("Unknown control command: ", cmd); + console.warn("MONO_WASM: Unknown control command: ", cmd); break; } } @@ -254,7 +254,7 @@ class DiagnosticServerImpl implements DiagnosticServer { } else if (isEventPipeCommandStopTracing(cmd)) { await this.stopEventPipe(ws, cmd.sessionID); } else { - console.warn("unknown EventPipe command: ", cmd); + console.warn("MONO_WASM: unknown EventPipe command: ", cmd); } } @@ -264,7 +264,7 @@ class DiagnosticServerImpl implements DiagnosticServer { } async stopEventPipe(ws: WebSocket | MockRemoteSocket, sessionID: EventPipeSessionIDImpl): Promise { - console.debug("stopEventPipe", sessionID); + console.debug("MONO_WASM: stopEventPipe", sessionID); cwraps.mono_wasm_event_pipe_session_disable(sessionID); // we might send OK before the session is actually stopped since the websocket is async // but the client end should be robust to that. @@ -280,7 +280,7 @@ class DiagnosticServerImpl implements DiagnosticServer { sessionIDbuf[3] = (session.sessionID >> 24) & 0xFF; // sessionIDbuf[4..7] is 0 because all our session IDs are 32-bit this.postClientReplyOK(ws, sessionIDbuf); - console.debug("created session, now streaming: ", session); + console.debug("MONO_WASM: created session, now streaming: ", session); cwraps.mono_wasm_event_pipe_session_start_streaming(session.sessionID); } @@ -289,7 +289,7 @@ class DiagnosticServerImpl implements DiagnosticServer { if (isProcessCommandResumeRuntime(cmd)) { this.processResumeRuntime(ws); } else { - console.warn("unknown Process command", cmd); + console.warn("MONO_WASM: unknown Process command", cmd); } } @@ -300,7 +300,7 @@ class DiagnosticServerImpl implements DiagnosticServer { resumeRuntime(): void { if (!this.runtimeResumed) { - console.debug("resuming runtime startup"); + console.info("MONO_WASM: resuming runtime startup"); cwraps.mono_wasm_diagnostic_server_post_resume_runtime(); this.runtimeResumed = true; } diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/ipc-protocol/parser.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/ipc-protocol/parser.ts index 32fc4203f00ecd..fe695a3525bab7 100644 --- a/src/mono/wasm/runtime/diagnostics/server_pthread/ipc-protocol/parser.ts +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/ipc-protocol/parser.ts @@ -52,7 +52,7 @@ function parseEventPipeCommand(cmd: BinaryProtocolCommand & { commandSet: Comman case EventPipeCommandId.CollectTracing2: return parseEventPipeCollectTracing2(cmd); default: - console.warn("unexpected EventPipe command: " + cmd.command); + console.warn("MONO_WASM: unexpected EventPipe command: " + cmd.command); return { success: false, error: `unexpected EventPipe command ${cmd.command}` }; } } @@ -132,7 +132,7 @@ function parseProcessCommand(cmd: BinaryProtocolCommand & { commandSet: CommandS case ProcessCommandId.ProcessInfo2: throw new Error("TODO"); default: - console.warn("unexpected Process command: " + cmd.command); + console.warn("MMONO_WASM: unexpected Process command: " + cmd.command); return { success: false, error: `unexpected Process command ${cmd.command}` }; } } diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/protocol-socket.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/protocol-socket.ts index ff93dbd9190d45..a9a7e3ebfc0e41 100644 --- a/src/mono/wasm/runtime/diagnostics/server_pthread/protocol-socket.ts +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/protocol-socket.ts @@ -72,14 +72,14 @@ class StatefulParser { result = this.tryAppendBuffer(new Uint8Array(buf)); } if (result.success) { - console.debug("protocol-socket: got result", result); + console.debug("MONO_WASM: protocol-socket: got result", result); this.setState(result.newState); if (result.command) { const command = result.command; this.emitCommandCallback(command); } } else { - console.warn("socket received invalid command header", buf, result.error); + console.warn("MONO_WASM: socket received invalid command header", buf, result.error); // FIXME: dispatch error event? this.setState({ state: InState.Error }); } @@ -138,7 +138,7 @@ class StatefulParser { const pos = { pos: Magic.MinimalHeaderSize }; let result = this.tryParseCompletedBuffer(buf, pos); if (overflow) { - console.warn("additional bytes past command payload", overflow); + console.warn("MONO_WASM: additional bytes past command payload", overflow); if (result.success) { const newResult: ParseResultBinaryCommandOk = { success: true, command: result.command, newState: { state: InState.Error } }; result = newResult; @@ -174,7 +174,7 @@ class ProtocolSocketImpl implements ProtocolSocket { constructor(private readonly sock: CommonSocket) { } onMessage(this: ProtocolSocketImpl, ev: MessageEvent): void { - console.debug("protocol socket received message", ev.data); + console.debug("MONO_WASM: protocol socket received message", ev.data); if (typeof ev.data === "object" && ev.data instanceof ArrayBuffer) { this.onArrayBuffer(ev.data); } else if (typeof ev.data === "object" && ev.data instanceof Blob) { @@ -188,15 +188,15 @@ class ProtocolSocketImpl implements ProtocolSocket { } onArrayBuffer(this: ProtocolSocketImpl, buf: ArrayBuffer) { - console.debug("protocol-socket: parsing array buffer", buf); + console.debug("MONO_WASM: protocol-socket: parsing array buffer", buf); this.statefulParser.receiveBuffer(buf); } // called by the stateful parser when it has a complete command emitCommandCallback(this: this, command: BinaryProtocolCommand): void { - console.debug("protocol-socket: queueing command", command); + console.debug("MONO_WASM: protocol-socket: queueing command", command); queueMicrotask(() => { - console.debug("dispatching protocol event with command", command); + console.debug("MONO_WASM: dispatching protocol event with command", command); this.dispatchProtocolCommandEvent(command); }); } @@ -213,7 +213,7 @@ class ProtocolSocketImpl implements ProtocolSocket { this.sock.addEventListener(type, listener, options); if (type === dotnetDiagnosticsServerProtocolCommandEvent) { if (this.protocolListeners === 0) { - console.debug("adding protocol listener, with a message chaser"); + console.debug("MONO_WASM: adding protocol listener, with a message chaser"); this.sock.addEventListener("message", this.messageListener); } this.protocolListeners++; @@ -223,7 +223,7 @@ class ProtocolSocketImpl implements ProtocolSocket { removeEventListener(type: K, listener: (this: ProtocolSocket, ev: ProtocolSocketEventMap[K]) => any): void; removeEventListener(type: string, listener: EventListenerOrEventListenerObject): void { if (type === dotnetDiagnosticsServerProtocolCommandEvent) { - console.debug("removing protocol listener and message chaser"); + console.debug("MONO_WASM: removing protocol listener and message chaser"); this.protocolListeners--; if (this.protocolListeners === 0) { this.sock.removeEventListener("message", this.messageListener); diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/socket-connection.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/socket-connection.ts index d843c0c72eed1a..5bf518de3716ae 100644 --- a/src/mono/wasm/runtime/diagnostics/server_pthread/socket-connection.ts +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/socket-connection.ts @@ -67,7 +67,7 @@ export class EventPipeSocketConnection { switch (this._state) { case ListenerState.Sending: /* unexpected message */ - console.warn("EventPipe session stream received unexpected message from websocket", event); + console.warn("MONO_WASM: EventPipe session stream received unexpected message from websocket", event); // TODO notify runtime that the connection had an error this._state = ListenerState.Error; break; From d1d088a7e48a6e79c5503b77658db9e20a144b28 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Wed, 20 Jul 2022 13:13:46 -0400 Subject: [PATCH 70/84] fix sample logic when startup session is disabled --- .../sample/wasm/browser-eventpipe/main.js | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/mono/sample/wasm/browser-eventpipe/main.js b/src/mono/sample/wasm/browser-eventpipe/main.js index abb66871dc128e..bf4ad92afb0ab6 100644 --- a/src/mono/sample/wasm/browser-eventpipe/main.js +++ b/src/mono/sample/wasm/browser-eventpipe/main.js @@ -60,25 +60,27 @@ function getOnClickHandler(startWork, stopWork, getIterationsDone) { if (typeof (sessions) !== "object" || sessions.length === "undefined") console.error("expected an array of sessions, got ", sessions); - if (sessions.length === 0) - return; // assume no sessions means they were turned off in the csproj file - if (sessions.length != 1) - console.error("expected one startup session, got ", sessions); - let eventSession = sessions[0]; - - console.debug("eventSession state is ", eventSession._state); // ooh protected member access + let eventSession = null; + if (sessions.length !== 0) { + if (sessions.length != 1) + console.error("expected one startup session, got ", sessions); + eventSession = sessions[0]; + console.debug("eventSession state is ", eventSession._state); // ooh protected member access + } const ret = await doWork(startWork, stopWork, getIterationsDone); + if (eventSession !== null) { + eventSession.stop(); - eventSession.stop(); + const filename = "dotnet-wasm-" + makeTimestamp() + ".nettrace"; + const blob = eventSession.getTraceBlob(); + const uri = URL.createObjectURL(blob); + downloadData(uri, filename); + } - const filename = "dotnet-wasm-" + makeTimestamp() + ".nettrace"; - - const blob = eventSession.getTraceBlob(); - const uri = URL.createObjectURL(blob); - downloadData(uri, filename); + console.debug("sample onclick handler done"); } } From e6979c17333559b00513b8c7bf7144f614986c7e Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Wed, 20 Jul 2022 13:14:16 -0400 Subject: [PATCH 71/84] improve debug output for DS server keep track of open/advertise counts and print them when receiving replies --- .../diagnostics/server_pthread/index.ts | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts index 393bf312df2666..72e68179d48398 100644 --- a/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts @@ -135,8 +135,12 @@ class DiagnosticServerImpl implements DiagnosticServer { } } + private openCount = 0; + async advertiseAndWaitForClient(): Promise { try { + const connNum = this.openCount++; + console.debug("MONO_WASM: opening websocket and sending ADVR_V1", connNum); const ws = await this.openSocket(); let p: Promise> | Promise; if (monoDiagnosticsMock && this.mocked) { @@ -146,19 +150,19 @@ class DiagnosticServerImpl implements DiagnosticServer { } this.sendAdvertise(ws); const message = await p; - console.debug("MONO_WASM: received advertising response: ", message); - queueMicrotask(() => this.parseAndDispatchMessage(ws, message)); + console.debug("MONO_WASM: received advertising response: ", message, connNum); + queueMicrotask(() => this.parseAndDispatchMessage(ws, connNum, message)); } finally { // if there were errors, resume the runtime anyway this.resumeRuntime(); } } - async parseAndDispatchMessage(ws: CommonSocket, message: MessageEvent | ProtocolCommandEvent): Promise { + async parseAndDispatchMessage(ws: CommonSocket, connNum: number, message: MessageEvent | ProtocolCommandEvent): Promise { try { - const cmd = this.parseCommand(message); + const cmd = this.parseCommand(message, connNum); if (cmd === null) { - console.error("MONO_WASM: unexpected message from client", message); + console.error("MONO_WASM: unexpected message from client", message, connNum); return; } else if (isEventPipeCommand(cmd)) { await this.dispatchEventPipeCommand(ws, cmd); @@ -207,16 +211,17 @@ class DiagnosticServerImpl implements DiagnosticServer { } - parseCommand(message: MessageEvent | ProtocolCommandEvent): ProtocolClientCommandBase | null { + parseCommand(message: MessageEvent | ProtocolCommandEvent, connNum: number): ProtocolClientCommandBase | null { if (typeof message.data === "string") { return parseMockCommand(message.data); } else { - console.debug("MONO_WASM: parsing byte command: ", message.data); + console.debug("MONO_WASM: parsing byte command: ", message.data, connNum); const result = parseProtocolCommand(message.data); + console.debug("MONO_WASM: parsied byte command: ", result, connNum); if (result.success) { return result.result; } else { - console.warn("MONO_WASM: failed to parse command: ", result.error); + console.warn("MONO_WASM: failed to parse command: ", result.error, connNum); return null; } } From 0bf7c0b0e09260d474b52bebbd177408c777205f Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Wed, 20 Jul 2022 15:43:52 -0400 Subject: [PATCH 72/84] bugfix: don't confuse buf_addr for the value stored in it the buf_addr is always the same for a given queue. the value in it is what we need to check to see if it's the sentinel value --- .../runtime/diagnostics/server_pthread/stream-queue.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/stream-queue.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/stream-queue.ts index 61dff8c80a3e40..afbc7f5400a97f 100644 --- a/src/mono/wasm/runtime/diagnostics/server_pthread/stream-queue.ts +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/stream-queue.ts @@ -32,6 +32,8 @@ const WRITE_DONE_OFFSET = 8; type SyncSendBuffer = (buf: VoidPtr, len: number) => void; type SyncSendClose = () => void; +const STREAM_CLOSE_SENTINEL = -1; + export class StreamQueue { readonly workAvailable: EventTarget = new EventTarget(); readonly signalWorkAvailable = this.signalWorkAvailableImpl.bind(this); @@ -66,12 +68,12 @@ export class StreamQueue { } private onWorkAvailable(this: StreamQueue /*,event: Event */): void { - const intptr_buf = this.buf_addr as unknown as number; - if (intptr_buf === -1) { + const buf = Memory.getI32(this.buf_addr) as unknown as VoidPtr; + const intptr_buf = buf as unknown as number; + if (intptr_buf === STREAM_CLOSE_SENTINEL) { // special value signaling that the streaming thread closed the queue. this.syncSendClose(); } else { - const buf = Memory.getI32(this.buf_addr) as unknown as VoidPtr; const count = Memory.getI32(this.count_addr); Memory.setI32(this.buf_addr, 0); if (count > 0) { From b3676b526be326d7f94b5388fb436bd1711e04d4 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Wed, 20 Jul 2022 15:45:37 -0400 Subject: [PATCH 73/84] slight refactor of EventPipeSocketConnection and more logging --- .../server_pthread/socket-connection.ts | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/socket-connection.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/socket-connection.ts index 5bf518de3716ae..47e216f1a7060c 100644 --- a/src/mono/wasm/runtime/diagnostics/server_pthread/socket-connection.ts +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/socket-connection.ts @@ -12,16 +12,16 @@ enum ListenerState { } class SocketGuts { - constructor(private readonly ws: CommonSocket) { } + constructor(public readonly socket: CommonSocket) { } close(): void { - this.ws.close(); + this.socket.close(); } write(data: VoidPtr, size: number): void { const buf = new ArrayBuffer(size); const view = new Uint8Array(buf); // Can we avoid this copy? view.set(new Uint8Array(Module.HEAPU8.buffer, data as unknown as number, size)); - this.ws.send(buf); + this.socket.send(buf); } } @@ -32,12 +32,13 @@ class SocketGuts { export class EventPipeSocketConnection { private _state: ListenerState; readonly stream: SocketGuts; - constructor(readonly socket: CommonSocket) { + constructor(socket: CommonSocket) { this._state = ListenerState.Sending; this.stream = new SocketGuts(socket); } close(): void { + console.debug("MONO_WASM: EventPipe session stream closing websocket"); switch (this._state) { case ListenerState.Error: return; @@ -97,16 +98,18 @@ export class EventPipeSocketConnection { } } - private _onError(/*event: Event*/) { + private _onError(event: Event) { + console.debug("MONO_WASM: EventPipe session stream websocket error", event); this._state = ListenerState.Error; this.stream.close(); // TODO: notify runtime that connection had an error } addListeners(): void { - this.socket.addEventListener("message", this._onMessage.bind(this)); - this.socket.addEventListener("close", this._onClose.bind(this)); - this.socket.addEventListener("error", this._onError.bind(this)); + const socket = this.stream.socket; + socket.addEventListener("message", this._onMessage.bind(this)); + addEventListener("close", this._onClose.bind(this)); + addEventListener("error", this._onError.bind(this)); } } From 2d220949c241d99c62f0993dab6ba6013ab1fe37 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Wed, 20 Jul 2022 16:09:46 -0400 Subject: [PATCH 74/84] review feedback --- src/mono/mono/component/diagnostics_server.c | 40 ++++++++++++-------- src/mono/mono/component/event_pipe.c | 1 - 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/src/mono/mono/component/diagnostics_server.c b/src/mono/mono/component/diagnostics_server.c index 0f040ee14712a6..bfc5e439d8730d 100644 --- a/src/mono/mono/component/diagnostics_server.c +++ b/src/mono/mono/component/diagnostics_server.c @@ -77,6 +77,7 @@ ds_server_wasm_init (void) static bool ds_server_wasm_shutdown (void) { + mono_coop_sem_destroy (&wasm_ds_options.suspend_resume); return true; } @@ -85,21 +86,22 @@ ds_server_wasm_pause_for_diagnostics_monitor (void) { /* wait until the DS receives a resume */ if (wasm_ds_options.suspend) { - const guint timeout = 50; - const guint warn_threshold = 5000; - guint cumulative_timeout = 0; - while (true) { - MonoSemTimedwaitRet res = mono_coop_sem_timedwait (&wasm_ds_options.suspend_resume, timeout, MONO_SEM_FLAGS_ALERTABLE); - if (res == MONO_SEM_TIMEDWAIT_RET_SUCCESS || res == MONO_SEM_TIMEDWAIT_RET_ALERTED) - break; - else { - /* timed out */ - cumulative_timeout += timeout; - if (cumulative_timeout > warn_threshold) { - cumulative_timeout = 0; - } - } - } + /* WISH: it would be better if we split mono_runtime_init_checked() (and runtime + * initialization in general) into two separate functions that we could call from + * JS, and wait for the resume event in JS. That would allow the browser to remain + * responsive. + * + * (We can't pause earlier because we need to start up enough of the runtime that DS + * can call ep_enable_2() and get session IDs back. Which seems to require + * mono_jit_init_version() to be called. ) + * + * With the current setup we block the browser UI. Emscripten still processes its + * queued work in futex_wait_busy, so at least other pthreads aren't waiting for us. + * But the user can't interact with the browser tab at all. Even the JS console is + * not displayed. + */ + int res = mono_coop_sem_wait(&wasm_ds_options.suspend_resume, MONO_SEM_FLAGS_NONE); + g_assert (res == 0); } } @@ -107,6 +109,9 @@ ds_server_wasm_pause_for_diagnostics_monitor (void) static void ds_server_wasm_disable (void) { + /* DS disable seems to only be called for the AOT compiler, which should never get here on + * HOST_WASM */ + g_assert_not_reached (); } /* Allocated by mono_wasm_diagnostic_server_create_thread, @@ -120,6 +125,7 @@ extern void mono_wasm_diagnostic_server_on_server_thread_created (char *websocke static void* server_thread (void* unused_arg G_GNUC_UNUSED) { + g_assert (ds_websocket_url != NULL); char* ws_url = g_strdup (ds_websocket_url); g_free (ds_websocket_url); ds_websocket_url = NULL; @@ -133,6 +139,9 @@ mono_wasm_diagnostic_server_create_thread (const char *websocket_url, pthread_t { pthread_t thread; + if (!websocket_url) + return FALSE; + g_assert (!ds_websocket_url); ds_websocket_url = g_strdup (websocket_url); if (!pthread_create (&thread, NULL, server_thread, NULL)) { @@ -323,5 +332,4 @@ MonoComponentDiagnosticsServer * mono_component_diagnostics_server_init (void) { return &fn_table; - } diff --git a/src/mono/mono/component/event_pipe.c b/src/mono/mono/component/event_pipe.c index c00ede7055c800..4d59f5af94b681 100644 --- a/src/mono/mono/component/event_pipe.c +++ b/src/mono/mono/component/event_pipe.c @@ -369,7 +369,6 @@ mono_wasm_event_pipe_enable (const ep_char8_t *output_path, /* EventPipeSessionType session_type = EP_SESSION_TYPE_FILE, */ /* EventPipieSerializationFormat format = EP_SERIALIZATION_FORMAT_NETTRACE_V4, */ /* bool */ gboolean rundown_requested, - /* IpcStream stream = NULL, */ /* EventPipeSessionSycnhronousCallback sync_callback = NULL, */ /* void *callback_additional_data, */ MonoWasmEventPipeSessionID *out_session_id) From b8b2148ad666099e2ef8048dbfe3b0b5f49c5524 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Wed, 20 Jul 2022 16:12:27 -0400 Subject: [PATCH 75/84] merge fixup --- src/mono/wasm/wasm.proj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mono/wasm/wasm.proj b/src/mono/wasm/wasm.proj index 6802cf8e723c9d..b775773d52f659 100644 --- a/src/mono/wasm/wasm.proj +++ b/src/mono/wasm/wasm.proj @@ -365,7 +365,7 @@ - Configuration:$(Configuration),NativeBinDir:$(NativeBinDir),ProductVersion:$(ProductVersion),MonoWasmThreads:$(MonoWasmThreads) + Configuration:$(Configuration),NativeBinDir:$(NativeBinDir),ProductVersion:$(ProductVersion),MonoWasmThreads:$(MonoWasmThreads),MonoWasmDiagnosticsMock:$(MonoWasmDiagnosticsMock) From 0de97196afe1c1ce65ff581623e9653c74455162 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Wed, 20 Jul 2022 16:40:47 -0400 Subject: [PATCH 76/84] fix bug in queue_push_sync main thread detection --- src/mono/mono/component/diagnostics_server.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mono/mono/component/diagnostics_server.c b/src/mono/mono/component/diagnostics_server.c index bfc5e439d8730d..880dcbb25bcb63 100644 --- a/src/mono/mono/component/diagnostics_server.c +++ b/src/mono/mono/component/diagnostics_server.c @@ -229,7 +229,7 @@ queue_push_sync (WasmIpcStreamQueue *q, const uint8_t *buf, uint32_t buf_size, u gboolean is_browser_thread_inited = FALSE; gboolean is_browser_thread = FALSE; while (mono_atomic_load_i32 (&q->buf_full) != 0) { - if (G_UNLIKELY (is_browser_thread_inited)) { + if (G_UNLIKELY (!is_browser_thread_inited)) { is_browser_thread = mono_threads_wasm_is_browser_thread (); is_browser_thread_inited = TRUE; } From 17d12e8bb43b11be05b86a99d00f49e4b140b7dd Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Thu, 21 Jul 2022 16:49:56 -0400 Subject: [PATCH 77/84] fix typo --- src/mono/wasm/wasm.proj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mono/wasm/wasm.proj b/src/mono/wasm/wasm.proj index b775773d52f659..9f4de70026d9aa 100644 --- a/src/mono/wasm/wasm.proj +++ b/src/mono/wasm/wasm.proj @@ -365,7 +365,7 @@ - Configuration:$(Configuration),NativeBinDir:$(NativeBinDir),ProductVersion:$(ProductVersion),MonoWasmThreads:$(MonoWasmThreads),MonoWasmDiagnosticsMock:$(MonoWasmDiagnosticsMock) + Configuration:$(Configuration),NativeBinDir:$(NativeBinDir),ProductVersion:$(ProductVersion),MonoWasmThreads:$(MonoWasmThreads),MonoDiagnosticsMock:$(MonoDiagnosticsMock) From d2f0acd1ef448c7a64df89855edea430d9d6b420 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Thu, 21 Jul 2022 21:42:50 -0400 Subject: [PATCH 78/84] merge fixup --- src/mono/wasm/runtime/diagnostics/browser/file-session.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/mono/wasm/runtime/diagnostics/browser/file-session.ts b/src/mono/wasm/runtime/diagnostics/browser/file-session.ts index cff36a482f13a8..0e935eb471066f 100644 --- a/src/mono/wasm/runtime/diagnostics/browser/file-session.ts +++ b/src/mono/wasm/runtime/diagnostics/browser/file-session.ts @@ -48,7 +48,7 @@ class EventPipeFileSession implements EventPipeSession { this._state = State.Started; start_streaming(this._sessionID); console.debug(`MONO_WASM: EventPipe session ${this.sessionID} started`); - } + }; stop = () => { if (this._state !== State.Started) { @@ -57,7 +57,7 @@ class EventPipeFileSession implements EventPipeSession { this._state = State.Done; stop_streaming(this._sessionID); console.debug(`MONO_WASM: EventPipe session ${this.sessionID} stopped`); - } + }; getTraceBlob = () => { if (this._state !== State.Done) { @@ -65,7 +65,7 @@ class EventPipeFileSession implements EventPipeSession { } const data = Module.FS_readFile(this._tracePath, { encoding: "binary" }) as Uint8Array; return new Blob([data], { type: "application/octet-stream" }); - } + }; } function start_streaming(sessionID: EventPipeSessionIDImpl): void { From 2e135e7ff35dc7e0454c80f71121a0d6a7e9fe6a Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Thu, 21 Jul 2022 22:14:44 -0400 Subject: [PATCH 79/84] fix rollup warning when making the crypto worker --- src/mono/wasm/runtime/rollup.config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/mono/wasm/runtime/rollup.config.js b/src/mono/wasm/runtime/rollup.config.js index f796536160eb16..e67a0634a4f6cd 100644 --- a/src/mono/wasm/runtime/rollup.config.js +++ b/src/mono/wasm/runtime/rollup.config.js @@ -128,6 +128,7 @@ function makeWorkerConfig(workerName, workerInputSourcePath) { plugins }, ], + external: externalDependencies, plugins: outputCodePlugins, }; return workerConfig; From 7da9d41b07c7a3d43068bb756e7ba0912e68b132 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Fri, 22 Jul 2022 09:18:49 -0400 Subject: [PATCH 80/84] add MONO_WASM: prefix to logging --- .../wasm/runtime/diagnostics/server_pthread/index.ts | 2 +- src/mono/wasm/runtime/pthreads/browser/index.ts | 6 +++--- src/mono/wasm/runtime/pthreads/worker/index.ts | 10 +++++----- src/mono/wasm/runtime/startup.ts | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts index 72e68179d48398..268172697859ca 100644 --- a/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts @@ -91,7 +91,7 @@ class DiagnosticServerImpl implements DiagnosticServer { private attachToRuntimeController = createPromiseController().promise_control; start(): void { - console.log(`starting diagnostic server with url: ${this.websocketUrl}`); + console.log(`MONO_WASM: starting diagnostic server with url: ${this.websocketUrl}`); this.startRequestedController.resolve(); } stop(): void { diff --git a/src/mono/wasm/runtime/pthreads/browser/index.ts b/src/mono/wasm/runtime/pthreads/browser/index.ts index 9f6aa5f6bf2e55..b490436088ff40 100644 --- a/src/mono/wasm/runtime/pthreads/browser/index.ts +++ b/src/mono/wasm/runtime/pthreads/browser/index.ts @@ -78,7 +78,7 @@ export const getThreadIds = (): IterableIterator => threads.keys(); function monoDedicatedChannelMessageFromWorkerToMain(event: MessageEvent, thread: Thread): void { // TODO: add callbacks that will be called from here - console.debug("got message from worker on the dedicated channel", event.data, thread); + console.debug("MONO_WASM: got message from worker on the dedicated channel", event.data, thread); } // handler that runs in the main thread when a message is received from a pthread worker @@ -86,7 +86,7 @@ function monoWorkerMessageHandler(worker: Worker, ev: MessageEvent monoWorkerMessageHandler(worker, ev)); - console.debug("afterLoadWasmModuleToWorker added message event handler", worker); + console.debug("MONO_WASM: afterLoadWasmModuleToWorker added message event handler", worker); } /// These utility functions dig into Emscripten internals diff --git a/src/mono/wasm/runtime/pthreads/worker/index.ts b/src/mono/wasm/runtime/pthreads/worker/index.ts index 71ec15a38d271e..810a396db8f96d 100644 --- a/src/mono/wasm/runtime/pthreads/worker/index.ts +++ b/src/mono/wasm/runtime/pthreads/worker/index.ts @@ -48,17 +48,17 @@ export let pthread_self: PThreadSelf = null as any as PThreadSelf; /// pthreads that are running on the current worker. /// Example: /// currentWorkerThreadEvents.addEventListener(dotnetPthreadCreated, (ev: WorkerThreadEvent) => { -/// console.debug ("thread created on worker with id", ev.pthread_ptr); +/// console.debug("MONO_WASM: thread created on worker with id", ev.pthread_ptr); /// }); export const currentWorkerThreadEvents: WorkerThreadEventTarget = MonoWasmThreads ? new EventTarget() : null as any as WorkerThreadEventTarget; // treeshake if threads are disabled function monoDedicatedChannelMessageFromMainToWorker(event: MessageEvent): void { - console.debug("got message from main on the dedicated channel", event.data); + console.debug("MONO_WASM: got message from main on the dedicated channel", event.data); } function setupChannelToMainThread(pthread_ptr: pthread_ptr): PThreadSelf { - console.debug("creating a channel", pthread_ptr); + console.debug("MONO_WASM: creating a channel", pthread_ptr); const channel = new MessageChannel(); const workerPort = channel.port1; const mainPort = channel.port2; @@ -74,7 +74,7 @@ function setupChannelToMainThread(pthread_ptr: pthread_ptr): PThreadSelf { export function mono_wasm_pthread_on_pthread_attached(pthread_id: pthread_ptr): void { const self = pthread_self; mono_assert(self !== null && self.pthread_id == pthread_id, "expected pthread_self to be set already when attaching"); - console.debug("attaching pthread to runtime", pthread_id); + console.debug("MONO_WASM: attaching pthread to runtime", pthread_id); currentWorkerThreadEvents.dispatchEvent(makeWorkerThreadEvent(dotnetPthreadAttached, self)); } @@ -86,7 +86,7 @@ export function afterThreadInitTLS(): void { if (ENVIRONMENT_IS_PTHREAD) { const pthread_ptr = (Module)["_pthread_self"](); mono_assert(!is_nullish(pthread_ptr), "pthread_self() returned null"); - console.debug("after thread init, pthread ptr", pthread_ptr); + console.debug("MONO_WASM: after thread init, pthread ptr", pthread_ptr); const self = setupChannelToMainThread(pthread_ptr); currentWorkerThreadEvents.dispatchEvent(makeWorkerThreadEvent(dotnetPthreadCreated, self)); } diff --git a/src/mono/wasm/runtime/startup.ts b/src/mono/wasm/runtime/startup.ts index 199c1919926b26..6ff13c33114ae0 100644 --- a/src/mono/wasm/runtime/startup.ts +++ b/src/mono/wasm/runtime/startup.ts @@ -794,6 +794,6 @@ export type DownloadAssetsContext = { async function mono_wasm_pthread_worker_init(): Promise { // This is a good place for subsystems to attach listeners for pthreads_worker.currentWorkerThreadEvents pthreads_worker.currentWorkerThreadEvents.addEventListener(pthreads_worker.dotnetPthreadCreated, (ev) => { - console.debug("pthread created", ev.pthread_self.pthread_id); + console.debug("MONO_WASM: pthread created", ev.pthread_self.pthread_id); }); } From f4219a6e96de672c5100c513d3352b8f1ed06734 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Thu, 21 Jul 2022 16:50:28 -0400 Subject: [PATCH 81/84] make diagnostic server mocking friendlier Allow each test project to specify its own mock script. Also provide TypeScript declarations for the mocking interfaces Also always use binary protocol commands - don't send json for mocks. --- .../Wasm.Browser.EventPipe.Sample.csproj | 13 +- .../sample/wasm/browser-eventpipe/mock.js | 59 ++++++++ src/mono/wasm/runtime/diagnostics-mock.d.ts | 72 ++++++++++ .../wasm/runtime/diagnostics-mock.d.ts.sha256 | 1 + src/mono/wasm/runtime/diagnostics/README.md | 81 +++++++++++ .../runtime/diagnostics/mock/environment.ts | 126 ++++++++++++++++++ .../runtime/diagnostics/mock/export-types.ts | 11 ++ .../wasm/runtime/diagnostics/mock/index.ts | 59 +++++--- .../wasm/runtime/diagnostics/mock/types.ts | 39 ++++++ .../server_pthread/common-socket.ts | 3 +- .../diagnostics/server_pthread/index.ts | 103 +++++--------- .../ipc-protocol/base-serializer.ts | 52 ++++++-- .../server_pthread/ipc-protocol/serializer.ts | 36 +++++ .../server_pthread/mock-command-parser.ts | 19 --- .../diagnostics/server_pthread/mock-remote.ts | 54 ++------ .../protocol-client-commands.ts | 2 + .../server_pthread/protocol-socket.ts | 18 ++- .../diagnostics/server_pthread/tsconfig.json | 3 + .../wasm/runtime/pthreads/browser/index.ts | 2 +- src/mono/wasm/runtime/rollup.config.js | 21 ++- src/mono/wasm/runtime/tsconfig.shared.json | 1 + src/mono/wasm/wasm.proj | 2 +- 22 files changed, 607 insertions(+), 170 deletions(-) create mode 100644 src/mono/sample/wasm/browser-eventpipe/mock.js create mode 100644 src/mono/wasm/runtime/diagnostics-mock.d.ts create mode 100644 src/mono/wasm/runtime/diagnostics-mock.d.ts.sha256 create mode 100644 src/mono/wasm/runtime/diagnostics/mock/environment.ts create mode 100644 src/mono/wasm/runtime/diagnostics/mock/export-types.ts create mode 100644 src/mono/wasm/runtime/diagnostics/mock/types.ts delete mode 100644 src/mono/wasm/runtime/diagnostics/server_pthread/mock-command-parser.ts diff --git a/src/mono/sample/wasm/browser-eventpipe/Wasm.Browser.EventPipe.Sample.csproj b/src/mono/sample/wasm/browser-eventpipe/Wasm.Browser.EventPipe.Sample.csproj index 346b0a6c909994..206be4533d8f88 100644 --- a/src/mono/sample/wasm/browser-eventpipe/Wasm.Browser.EventPipe.Sample.csproj +++ b/src/mono/sample/wasm/browser-eventpipe/Wasm.Browser.EventPipe.Sample.csproj @@ -13,16 +13,25 @@ CA2007 + + true + + + - + ` to your `.csproj` + +3. configure the diagnostics server with a `mock:relative_url_of/mock.js` + + ```xml + + ``` + +4. The file `mock.js` should be an ES6 module with a default export like this: + + ```js + function script (env) { + return [ + async (conn) => { /* script for 1st call to "WebSocket.open" */ }, + async (conn) => { /* script for 2nd call to "WebSocket.open" */ }, + /* etc */ + ] + } + export default script; + ``` + +### Mock environment + +The mock environment parameter `env` (of type `MockEnvironment` defined in [./mock/index.ts](./mock/index.ts)) provides +access to utility functions useful for creating mock connection scripts. + +It includes: + +- `createPromiseController` - this is defined in [../promise-controller.ts](../promise-controller.ts). + +### Mock connection + +The mock script should return an array of functions `async (connection) => { ... }` where each function defines the interaction with one open WebSocket connection. Each function should return `Promise`. + +The connection object (of type `MockScriptConnection` defined in [./mock/index.ts](./mock/index.ts) has the following methods: + +- `waitForSend (filter: (data: string | ArrayBuffer) => boolean): Promise` or `waitForSend(filter: (data: string | ArrayBuffer) => boolean, extract: (data: string | ArrayBuffer) => T): Promise`. Waits until the diagnostic server sends a single message with data that is accepted by `filter` (note the mocking doesn't support aggregating multiple partial replies). If the `filter` returns a falsy value, the mock script will throw an error. If the `filter` returns a truthy value and there is an `extract` argument given, the data will be passed to `extract` and the returned promise will be resolved with that value. (This is useful for returning EventPipe session IDs, for example). + +- `reply(data: string | ArrayBuffer): void` sends a reply back to the diagnostic server. This can be anything, but should usually be a diagnostic server IPC protocol command + +### Mock example + +```js +function script (env) { + const sessionStarted = env.createPromiseController(); /* coordinate between the connections */ + return [ + async (conn) => { + /* first connection. Expect an ADVR packet */ + await conn.waitForSend(isAdvertisePacket); + conn.reply(makeEventPipeStartCollecting2 ({ "collectRundownEvents": "true", "providers": "WasmHello::5:EventCounterIntervalSec=1" })); + /* wait for an "OK" reply with 4 extra bytes of payload, which is the sessionID */ + const sessionID = await conn.waitForSend(isReplyOK(4), extractSessionID); + sessionStarted.promise_control.resolve(sessionID); + /* connection kept open. the runtime will send EventPipe data here */ + }, + async (conn) => { + /* second connection. Expect an ADVR packet and the sessionStarted sessionID */ + await Promise.all([conn.waitForSend (isAdvertisePacket); sessionStarted.promise]); + /* collect a trace for 5 seconds */ + await new Promise((resolve) => await new Promise((resolve) => { setTimeout(resolve, 1000); }); + const sessionID = await sessionStarted.promise; + conn.reply(makeEventPipeStopCollecting({sessionID})); + /* wait for an "OK" with no payload */ + await conn.waitForSend(isReplyOK()); + } + /* any further calls to "open" will be an error */ + ] +} +``` diff --git a/src/mono/wasm/runtime/diagnostics/mock/environment.ts b/src/mono/wasm/runtime/diagnostics/mock/environment.ts new file mode 100644 index 00000000000000..e0e5e68b08c470 --- /dev/null +++ b/src/mono/wasm/runtime/diagnostics/mock/environment.ts @@ -0,0 +1,126 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import { createPromiseController } from "../../promise-controller"; +import type { RemoveCommandSetAndId, EventPipeCommandCollectTracing2, EventPipeCommandStopTracing } from "../server_pthread/protocol-client-commands"; +import type { FilterPredicate, MockEnvironment } from "./types"; +import Serializer from "../server_pthread/ipc-protocol/base-serializer"; +import { CommandSetId, EventPipeCommandId, ProcessCommandId } from "../server_pthread/ipc-protocol/types"; +import { assertNever, mono_assert } from "../../types"; +import { delay } from "../../promise-utils"; + + + +function expectAdvertise(data: ArrayBuffer): boolean { + if (typeof (data) === "string") { + assertNever(data); + } else { + const view = new Uint8Array(data); + const ADVR_V1 = Array.from("ADVR_V1\0").map((c) => c.charCodeAt(0)); + /* TODO: check that the message is really long enough for the cookie, process ID and reserved bytes */ + return view.length >= ADVR_V1.length && ADVR_V1.every((v, i) => v === view[i]); + } +} + +function expectOk(payloadLength?: number): FilterPredicate { + return (data) => { + if (typeof (data) === "string") { + assertNever(data); + } else { + const view = new Uint8Array(data); + const extra = payloadLength !== undefined ? payloadLength : 0; + return view.length >= (20 + extra) && view[16] === 0xFF && view[17] == 0x00; + } + }; +} + +function extractOkSessionID(data: ArrayBuffer): number { + if (typeof (data) === "string") { + assertNever(data); + } else { + const view = new Uint8Array(data, 20, 8); + 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); + mono_assert(sessionIDHi === 0, "mock: sessionIDHi should be zero"); + return sessionIDLo; + } +} + +function computeStringByteLength(s: string): number { + if (s === undefined || s === null || s === "") + return 4; // just length of zero + return 4 + 2 * s.length + 2; // length + UTF16 + null +} + +function computeCollectTracing2PayloadByteLength(payload: RemoveCommandSetAndId): number { + let len = 0; + len += 4; // circularBufferMB + len += 4; // format + len += 1; // requestRundown + len += 4; // providers length + for (const provider of payload.providers) { + len += 8; // keywords + len += 4; // level + len += computeStringByteLength(provider.provider_name); + len += computeStringByteLength(provider.filter_data); + } + return len; +} + +function makeEventPipeCollectTracing2(payload: RemoveCommandSetAndId): Uint8Array { + const payloadLength = computeCollectTracing2PayloadByteLength(payload); + const messageLength = Serializer.computeMessageByteLength(payloadLength); + const buffer = new Uint8Array(messageLength); + const pos = { pos: 0 }; + Serializer.serializeHeader(buffer, pos, CommandSetId.EventPipe, EventPipeCommandId.CollectTracing2, messageLength); + Serializer.serializeUint32(buffer, pos, payload.circularBufferMB); + Serializer.serializeUint32(buffer, pos, payload.format); + Serializer.serializeUint8(buffer, pos, payload.requestRundown ? 1 : 0); + Serializer.serializeUint32(buffer, pos, payload.providers.length); + for (const provider of payload.providers) { + Serializer.serializeUint64(buffer, pos, provider.keywords); + Serializer.serializeUint32(buffer, pos, provider.logLevel); + Serializer.serializeString(buffer, pos, provider.provider_name); + Serializer.serializeString(buffer, pos, provider.filter_data); + } + return buffer; +} + +function makeEventPipeStopTracing(payload: RemoveCommandSetAndId): Uint8Array { + const payloadLength = 8; + const messageLength = Serializer.computeMessageByteLength(payloadLength); + const buffer = new Uint8Array(messageLength); + const pos = { pos: 0 }; + Serializer.serializeHeader(buffer, pos, CommandSetId.EventPipe, EventPipeCommandId.StopTracing, messageLength); + Serializer.serializeUint32(buffer, pos, payload.sessionID); + Serializer.serializeUint32(buffer, pos, 0); + return buffer; +} + +function makeProcessResumeRuntime(): Uint8Array { + const payloadLength = 0; + const messageLength = Serializer.computeMessageByteLength(payloadLength); + const buffer = new Uint8Array(messageLength); + const pos = { pos: 0 }; + Serializer.serializeHeader(buffer, pos, CommandSetId.Process, ProcessCommandId.ResumeRuntime, messageLength); + return buffer; +} + +export function createMockEnvironment(): MockEnvironment { + const command = { + makeEventPipeCollectTracing2, + makeEventPipeStopTracing, + makeProcessResumeRuntime, + }; + const reply = { + expectOk, + extractOkSessionID, + }; + return { + createPromiseController, + delay, + command, + reply, + expectAdvertise + }; +} diff --git a/src/mono/wasm/runtime/diagnostics/mock/export-types.ts b/src/mono/wasm/runtime/diagnostics/mock/export-types.ts new file mode 100644 index 00000000000000..1117fccb5c3735 --- /dev/null +++ b/src/mono/wasm/runtime/diagnostics/mock/export-types.ts @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +export type { + MockScriptConnection, + MockEnvironment +} from "./types"; + +export type { + PromiseAndController, +} from "../../promise-controller"; diff --git a/src/mono/wasm/runtime/diagnostics/mock/index.ts b/src/mono/wasm/runtime/diagnostics/mock/index.ts index 9c792f1ffa9456..b8f696b6f91407 100644 --- a/src/mono/wasm/runtime/diagnostics/mock/index.ts +++ b/src/mono/wasm/runtime/diagnostics/mock/index.ts @@ -3,6 +3,10 @@ import monoDiagnosticsMock from "consts:monoDiagnosticsMock"; +import { createMockEnvironment } from "./environment"; +import type { MockEnvironment, MockScriptConnection } from "./export-types"; +import { assertNever } from "../../types"; + export interface MockRemoteSocket extends EventTarget { addEventListener(type: T, listener: (this: MockRemoteSocket, ev: WebSocketEventMap[T]) => any, options?: boolean | AddEventListenerOptions): void; addEventListener(event: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void; @@ -20,14 +24,11 @@ interface MockOptions { readonly trace: boolean; } -export interface MockScriptEngine { - waitForSend(filter: (data: string | ArrayBuffer) => boolean): Promise; - waitForSend(filter: (data: string | ArrayBuffer) => boolean, extract: (data: string | ArrayBuffer) => T): Promise; - reply(data: string | ArrayBuffer): void; -} +type MockConnectionScript = (engine: MockScriptConnection) => Promise; +export type MockScript = (env: MockEnvironment) => MockConnectionScript[]; -let MockImplConstructor: new (script: ((engine: MockScriptEngine) => Promise)[], options?: MockOptions) => Mock; -export function mock(script: ((engine: MockScriptEngine) => Promise)[], options?: MockOptions): Mock { +let MockImplConstructor: new (script: MockScript, options?: MockOptions) => Mock; +export function mock(script: MockScript, options?: MockOptions): Mock { if (monoDiagnosticsMock) { if (!MockImplConstructor) { class MockScriptEngineSocketImpl implements MockRemoteSocket { @@ -72,7 +73,7 @@ export function mock(script: ((engine: MockScriptEngine) => Promise)[], op } } - class MockScriptEngineImpl implements MockScriptEngine { + class MockScriptEngineImpl implements MockScriptConnection { readonly socket: MockRemoteSocket; // eventTarget that the MockReplySocket will dispatch to readonly eventTarget: EventTarget = new EventTarget(); @@ -82,14 +83,28 @@ export function mock(script: ((engine: MockScriptEngine) => Promise)[], op this.socket = new MockScriptEngineSocketImpl(this); } - reply(data: string | ArrayBuffer) { + reply(data: ArrayBuffer | Uint8Array) { if (this.trace) { console.debug(`mock ${this.ident} reply:`, data); } - this.eventTarget.dispatchEvent(new MessageEvent("message", { data })); + let sendData: ArrayBuffer; + if (typeof data === "object" && data instanceof ArrayBuffer) { + sendData = new ArrayBuffer(data.byteLength); + const sendDataView = new Uint8Array(sendData); + const dataView = new Uint8Array(data); + sendDataView.set(dataView); + } else if (typeof data === "object" && data instanceof Uint8Array) { + sendData = new ArrayBuffer(data.byteLength); + const sendDataView = new Uint8Array(sendData); + sendDataView.set(data); + } else { + console.warn(`mock ${this.ident} reply got wrong kind of reply data, expected ArrayBuffer`, data); + assertNever(data); + } + this.eventTarget.dispatchEvent(new MessageEvent("message", { data: sendData })); } - async waitForSend(filter: (data: string | ArrayBuffer) => boolean): Promise { + async waitForSend(filter: (data: ArrayBuffer) => boolean, extract?: (data: ArrayBuffer) => T): Promise { const trace = this.trace; if (trace) { console.debug(`mock ${this.ident} waitForSend`); @@ -102,21 +117,32 @@ export function mock(script: ((engine: MockScriptEngine) => Promise)[], op resolve(event as MessageEvent); }, { once: true }); }); - if (!filter(event.data)) { + const data = event.data; + if (typeof data === "string") { + console.warn(`mock ${this.ident} waitForSend got string:`, data); + throw new Error("mock script connection received string data"); + } + if (!filter(data)) { throw new Error("Unexpected data"); } - return; + if (extract) { + return extract(data); + } + return undefined as any as T; } } MockImplConstructor = class MockImpl implements Mock { openCount: number; engines: MockScriptEngineImpl[]; + connectionScripts: MockConnectionScript[]; readonly trace: boolean; - constructor(public readonly script: ((engine: MockScriptEngine) => Promise)[], options?: MockOptions) { + constructor(public readonly mockScript: MockScript, options?: MockOptions) { + const env: MockEnvironment = createMockEnvironment(); + this.connectionScripts = mockScript(env); this.openCount = 0; this.trace = options?.trace ?? false; - const count = script.length; + const count = this.connectionScripts.length; this.engines = new Array(count); for (let i = 0; i < count; ++i) { this.engines[i] = new MockScriptEngineImpl(this.trace, i); @@ -131,7 +157,8 @@ export function mock(script: ((engine: MockScriptEngine) => Promise)[], op } async run(): Promise { - await Promise.all(this.script.map((script, i) => script(this.engines[i]))); + const scripts = this.connectionScripts; + await Promise.all(scripts.map((script, i) => script(this.engines[i]))); } }; } diff --git a/src/mono/wasm/runtime/diagnostics/mock/types.ts b/src/mono/wasm/runtime/diagnostics/mock/types.ts new file mode 100644 index 00000000000000..b3682db8c3f05c --- /dev/null +++ b/src/mono/wasm/runtime/diagnostics/mock/types.ts @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +import type { PromiseAndController } from "../../promise-controller"; +import type { + RemoveCommandSetAndId, + EventPipeCommandCollectTracing2, + EventPipeCommandStopTracing, +} from "../server_pthread/protocol-client-commands"; + + +export type FilterPredicate = (data: ArrayBuffer) => boolean; + +export interface MockScriptConnection { + waitForSend(filter: FilterPredicate): Promise; + waitForSend(filter: FilterPredicate, extract: (data: ArrayBuffer) => T): Promise; + reply(data: ArrayBuffer): void; +} + +interface MockEnvironmentCommand { + makeEventPipeCollectTracing2(payload: RemoveCommandSetAndId): Uint8Array; + makeEventPipeStopTracing(payload: RemoveCommandSetAndId): Uint8Array; + makeProcessResumeRuntime(): Uint8Array; +} + +interface MockEnvironmentReply { + + expectOk(extraPayload?: number): FilterPredicate; + + extractOkSessionID(data: ArrayBuffer): number; + +} + +export interface MockEnvironment { + createPromiseController(): PromiseAndController; + delay: (ms: number) => Promise; + command: MockEnvironmentCommand; + reply: MockEnvironmentReply; + expectAdvertise: FilterPredicate; +} diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/common-socket.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/common-socket.ts index c99bf59e265257..b6bb8084a26845 100644 --- a/src/mono/wasm/runtime/diagnostics/server_pthread/common-socket.ts +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/common-socket.ts @@ -9,7 +9,8 @@ export interface CommonSocket { removeEventListener(type: T, listener: (this: CommonSocket, ev: WebSocketEventMap[T]) => any): void; removeEventListener(event: string, listener: EventListenerOrEventListenerObject): void; dispatchEvent(evt: Event): boolean; - send(data: string | ArrayBuffer | Uint8Array | Blob | DataView): void; + // send is more general and can send a string, but we should only be sending binary data + send(data: ArrayBuffer | Uint8Array /*| Blob | DataView*/): void; close(): void; } diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts index 268172697859ca..989f826d57066b 100644 --- a/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts @@ -4,7 +4,7 @@ /// import monoDiagnosticsMock from "consts:monoDiagnosticsMock"; -import { assertNever, mono_assert } from "../../types"; +import { assertNever } from "../../types"; import { pthread_self } from "../../pthreads/worker"; import { Module } from "../../imports"; import cwraps from "../../cwraps"; @@ -15,9 +15,9 @@ import { isDiagnosticMessage } from "../shared/controller-commands"; -import { mockScript } from "./mock-remote"; -import type { MockRemoteSocket } from "../mock"; -import { createPromiseController } from "../../promise-controller"; +import { importAndInstantiateMock } from "./mock-remote"; +import type { Mock, MockRemoteSocket } from "../mock"; +import { PromiseAndController, createPromiseController } from "../../promise-controller"; import { isEventPipeCommand, isProcessCommand, @@ -30,7 +30,6 @@ import { EventPipeCommandCollectTracing2, } from "./protocol-client-commands"; import { makeEventPipeStreamingSession } from "./streaming-session"; -import parseMockCommand from "./mock-command-parser"; import { CommonSocket } from "./common-socket"; import { createProtocolSocket, dotnetDiagnosticsServerProtocolCommandEvent, @@ -45,16 +44,10 @@ import { ParseClientCommandResult, } from "./ipc-protocol/parser"; import { + createAdvertise, createBinaryCommandOKReply, } from "./ipc-protocol/serializer"; -function addOneShotMessageEventListener(src: EventTarget): Promise> { - return new Promise((resolve) => { - const listener = (event: Event) => { resolve(event as MessageEvent); }; - src.addEventListener("message", listener, { once: true }); - }); -} - function addOneShotProtocolCommandEventListener(src: EventTarget): Promise { return new Promise((resolve) => { const listener = (event: Event) => { resolve(event as ProtocolCommandEvent); }; @@ -75,13 +68,13 @@ export interface DiagnosticServer { class DiagnosticServerImpl implements DiagnosticServer { readonly websocketUrl: string; - readonly mocked: boolean; + readonly mocked: Promise | undefined; runtimeResumed = false; - constructor(websocketUrl: string) { + constructor(websocketUrl: string, mockPromise?: Promise) { this.websocketUrl = websocketUrl; pthread_self.addEventListenerFromBrowser(this.onMessageFromMainThread.bind(this)); - this.mocked = monoDiagnosticsMock && websocketUrl.startsWith("mock:"); + this.mocked = monoDiagnosticsMock ? mockPromise : undefined; } private startRequestedController = createPromiseController().promise_control; @@ -126,7 +119,7 @@ class DiagnosticServerImpl implements DiagnosticServer { async openSocket(): Promise { if (monoDiagnosticsMock && this.mocked) { - return mockScript.open(); + return (await this.mocked).open(); } else { const sock = new WebSocket(this.websocketUrl); // TODO: add an "error" handler here - if we get readyState === 3, the connection failed. @@ -142,12 +135,7 @@ class DiagnosticServerImpl implements DiagnosticServer { const connNum = this.openCount++; console.debug("MONO_WASM: opening websocket and sending ADVR_V1", connNum); const ws = await this.openSocket(); - let p: Promise> | Promise; - if (monoDiagnosticsMock && this.mocked) { - p = addOneShotMessageEventListener(ws); - } else { - p = addOneShotProtocolCommandEventListener(createProtocolSocket(ws)); - } + const p = addOneShotProtocolCommandEventListener(createProtocolSocket(ws)); this.sendAdvertise(ws); const message = await p; console.debug("MONO_WASM: received advertising response: ", message, connNum); @@ -158,7 +146,7 @@ class DiagnosticServerImpl implements DiagnosticServer { } } - async parseAndDispatchMessage(ws: CommonSocket, connNum: number, message: MessageEvent | ProtocolCommandEvent): Promise { + async parseAndDispatchMessage(ws: CommonSocket, connNum: number, message: ProtocolCommandEvent): Promise { try { const cmd = this.parseCommand(message, connNum); if (cmd === null) { @@ -178,52 +166,25 @@ class DiagnosticServerImpl implements DiagnosticServer { } sendAdvertise(ws: CommonSocket) { - const BUF_LENGTH = 34; - const buf = new ArrayBuffer(BUF_LENGTH); - const view = new Uint8Array(buf); - let pos = 0; - const text = "ADVR_V1"; - for (let i = 0; i < text.length; i++) { - view[pos++] = text.charCodeAt(i); - } - view[pos++] = 0; // nul terminator + /* FIXME: don't use const fake guid and fake process id. In dotnet-dsrouter the pid is used + * as a dictionary key,so if we ever supprt multiple runtimes, this might need to change. + */ const guid = "C979E170-B538-475C-BCF1-B04A30DA1430"; - guid.split("-").forEach((part) => { - // FIXME: I'm sure the endianness is wrong here - for (let i = 0; i < part.length; i += 2) { - const idx = part.length - i - 2; // go through the pieces backwards - view[pos++] = parseInt(part.substring(idx, idx + 2), 16); - } - }); - // "process ID" in 2 32-bit parts - const pid = [0, 1234]; // hi, lo - for (let i = 0; i < pid.length; i++) { - const j = pid[pid.length - i - 1]; //lo, hi - view[pos++] = j & 0xFF; - view[pos++] = (j >> 8) & 0xFF; - view[pos++] = (j >> 16) & 0xFF; - view[pos++] = (j >> 24) & 0xFF; - } - view[pos++] = 0; - view[pos++] = 0; // two reserved zero bytes - mono_assert(pos == BUF_LENGTH, "did not format ADVR_V1 correctly"); + const processIdLo = 0; + const processIdHi = 1234; + const buf = createAdvertise(guid, [processIdLo, processIdHi]); ws.send(buf); - } - parseCommand(message: MessageEvent | ProtocolCommandEvent, connNum: number): ProtocolClientCommandBase | null { - if (typeof message.data === "string") { - return parseMockCommand(message.data); + parseCommand(message: ProtocolCommandEvent, connNum: number): ProtocolClientCommandBase | null { + console.debug("MONO_WASM: parsing byte command: ", message.data, connNum); + const result = parseProtocolCommand(message.data); + console.debug("MONO_WASM: parsied byte command: ", result, connNum); + if (result.success) { + return result.result; } else { - console.debug("MONO_WASM: parsing byte command: ", message.data, connNum); - const result = parseProtocolCommand(message.data); - console.debug("MONO_WASM: parsied byte command: ", result, connNum); - if (result.success) { - return result.result; - } else { - console.warn("MONO_WASM: failed to parse command: ", result.error, connNum); - return null; - } + console.warn("MONO_WASM: failed to parse command: ", result.error, connNum); + return null; } } @@ -253,7 +214,7 @@ class DiagnosticServerImpl implements DiagnosticServer { } // dispatch EventPipe commands received from the diagnostic client - async dispatchEventPipeCommand(ws: WebSocket | MockRemoteSocket, cmd: EventPipeClientCommandBase): Promise { + async dispatchEventPipeCommand(ws: CommonSocket, cmd: EventPipeClientCommandBase): Promise { if (isEventPipeCommandCollectTracing2(cmd)) { await this.collectTracingEventPipe(ws, cmd); } else if (isEventPipeCommandStopTracing(cmd)) { @@ -263,7 +224,7 @@ class DiagnosticServerImpl implements DiagnosticServer { } } - postClientReplyOK(ws: WebSocket | MockRemoteSocket, payload?: Uint8Array): void { + postClientReplyOK(ws: CommonSocket, payload?: Uint8Array): void { // FIXME: send a binary response for non-mock sessions! ws.send(createBinaryCommandOKReply(payload)); } @@ -324,12 +285,16 @@ function parseProtocolCommand(data: ArrayBuffer | BinaryProtocolCommand): ParseC export function mono_wasm_diagnostic_server_on_server_thread_created(websocketUrlPtr: CharPtr): void { const websocketUrl = Module.UTF8ToString(websocketUrlPtr); console.debug(`mono_wasm_diagnostic_server_on_server_thread_created, url ${websocketUrl}`); - const server = new DiagnosticServerImpl(websocketUrl); + let mock: PromiseAndController | undefined = undefined; if (monoDiagnosticsMock && websocketUrl.startsWith("mock:")) { - queueMicrotask(() => { - mockScript.run(); + mock = createPromiseController(); + queueMicrotask(async () => { + const m = await importAndInstantiateMock(websocketUrl); + mock!.promise_control.resolve(m); + m.run(); }); } + const server = new DiagnosticServerImpl(websocketUrl, mock?.promise); queueMicrotask(() => { server.serverLoop(); }); diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/ipc-protocol/base-serializer.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/ipc-protocol/base-serializer.ts index e42b6dcdd6de22..616b7338ecfd5a 100644 --- a/src/mono/wasm/runtime/diagnostics/server_pthread/ipc-protocol/base-serializer.ts +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/ipc-protocol/base-serializer.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 { CommandSetId, ServerCommandId } from "./types"; +import { CommandSetId, ServerCommandId, EventPipeCommandId, ProcessCommandId } from "./types"; import Magic from "./magic"; function advancePos(pos: { pos: number }, count: number): void { @@ -9,12 +9,25 @@ function advancePos(pos: { pos: number }, count: number): void { } +function serializeHeader(buf: Uint8Array, pos: { pos: number }, commandSet: CommandSetId.EventPipe, command: EventPipeCommandId, len: number): void; +function serializeHeader(buf: Uint8Array, pos: { pos: number }, commandSet: CommandSetId.Process, command: ProcessCommandId, len: number): void; +function serializeHeader(buf: Uint8Array, pos: { pos: number }, commandSet: CommandSetId.Server, command: ServerCommandId, len: number): void; +function serializeHeader(buf: Uint8Array, pos: { pos: number }, commandSet: CommandSetId, command: EventPipeCommandId | ProcessCommandId | ServerCommandId, len: number): void { + Serializer.serializeMagic(buf, pos); + Serializer.serializeUint16(buf, pos, len); + Serializer.serializeUint8(buf, pos, commandSet); + Serializer.serializeUint8(buf, pos, command); + Serializer.serializeUint16(buf, pos, 0); // reserved +} + + const Serializer = { - computeMessageByteLength(payload?: Uint8Array): number { + computeMessageByteLength(payload?: number | Uint8Array): number { const fullHeaderSize = Magic.MinimalHeaderSize // magic, len + 2 // commandSet, command + 2; // reserved ; - const len = fullHeaderSize + (payload !== undefined ? payload.byteLength : 0); // magic, size, commandSet, command, reserved + const payloadLength = payload ? (payload instanceof Uint8Array ? payload.byteLength : payload) : 0; + const len = fullHeaderSize + payloadLength; // magic, size, commandSet, command, reserved return len; }, serializeMagic(buf: Uint8Array, pos: { pos: number }): void { @@ -28,17 +41,36 @@ const Serializer = { buf[pos.pos++] = value & 0xFF; buf[pos.pos++] = (value >> 8) & 0xFF; }, - serializeHeader(buf: Uint8Array, pos: { pos: number }, commandSet: CommandSetId, command: ServerCommandId, len: number): void { - Serializer.serializeMagic(buf, pos); - Serializer.serializeUint16(buf, pos, len); - Serializer.serializeUint8(buf, pos, commandSet); - Serializer.serializeUint8(buf, pos, command); - Serializer.serializeUint16(buf, pos, 0); // reserved + serializeUint32(buf: Uint8Array, pos: { pos: number }, value: number): void { + buf[pos.pos++] = value & 0xFF; + buf[pos.pos++] = (value >> 8) & 0xFF; + buf[pos.pos++] = (value >> 16) & 0xFF; + buf[pos.pos++] = (value >> 24) & 0xFF; + }, + serializeUint64(buf: Uint8Array, pos: { pos: number }, value: [number, number]): void { + Serializer.serializeUint32(buf, pos, value[0]); + Serializer.serializeUint32(buf, pos, value[1]); }, + serializeHeader, serializePayload(buf: Uint8Array, pos: { pos: number }, payload: Uint8Array): void { buf.set(payload, pos.pos); advancePos(pos, payload.byteLength); - } + }, + serializeString(buf: Uint8Array, pos: { pos: number }, s: string | null): void { + if (s === null) { + Serializer.serializeUint32(buf, pos, 0); + } else { + const len = s.length; + const hasNul = s[len - 1] === "\0"; + Serializer.serializeUint32(buf, pos, len + (hasNul ? 0 : 1)); + for (let i = 0; i < len; i++) { + Serializer.serializeUint16(buf, pos, s.charCodeAt(i)); + } + if (!hasNul) { + Serializer.serializeUint16(buf, pos, 0); + } + } + }, }; export default Serializer; diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/ipc-protocol/serializer.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/ipc-protocol/serializer.ts index 86c6976cb5c0ea..8f83d1253bc0a2 100644 --- a/src/mono/wasm/runtime/diagnostics/server_pthread/ipc-protocol/serializer.ts +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/ipc-protocol/serializer.ts @@ -3,6 +3,7 @@ import Serializer from "./base-serializer"; import { CommandSetId, ServerCommandId } from "./types"; +import { mono_assert } from "../../../types"; export function createBinaryCommandOKReply(payload?: Uint8Array): Uint8Array { const len = Serializer.computeMessageByteLength(payload); @@ -14,3 +15,38 @@ export function createBinaryCommandOKReply(payload?: Uint8Array): Uint8Array { } return buf; } + +function serializeGuid(buf: Uint8Array, pos: { pos: number }, guid: string): void { + guid.split("-").forEach((part) => { + // FIXME: I'm sure the endianness is wrong here + for (let i = 0; i < part.length; i += 2) { + const idx = part.length - i - 2; // go through the pieces backwards + buf[pos.pos++] = Number.parseInt(part.substring(idx, idx + 2), 16); + } + }); +} + +function serializeAsciiLiteralString(buf: Uint8Array, pos: { pos: number }, s: string): void { + const len = s.length; + const hasNul = s[len - 1] === "\0"; + for (let i = 0; i < len; i++) { + Serializer.serializeUint8(buf, pos, s.charCodeAt(i)); + } + if (!hasNul) { + Serializer.serializeUint8(buf, pos, 0); + } +} + + +export function createAdvertise(guid: string, processId: [/*lo*/ number, /*hi*/number]): Uint8Array { + const BUF_LENGTH = 34; + const buf = new Uint8Array(BUF_LENGTH); + const pos = { pos: 0 }; + const advrText = "ADVR_V1\0"; + serializeAsciiLiteralString(buf, pos, advrText); + serializeGuid(buf, pos, guid); + Serializer.serializeUint64(buf, pos, processId); + Serializer.serializeUint16(buf, pos, 0); // reserved + mono_assert(pos.pos == BUF_LENGTH, "did not format ADVR_V1 correctly"); + return buf; +} diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/mock-command-parser.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/mock-command-parser.ts deleted file mode 100644 index 2f29ed9daed20f..00000000000000 --- a/src/mono/wasm/runtime/diagnostics/server_pthread/mock-command-parser.ts +++ /dev/null @@ -1,19 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -import { ProtocolClientCommandBase, isDiagnosticCommandBase } from "./protocol-client-commands"; - -export default function parseCommand(x: string): ProtocolClientCommandBase | null { - let command: object; - try { - command = JSON.parse(x); - } catch (err) { - console.warn("error while parsing JSON diagnostic server protocol command", err); - return null; - } - if (isDiagnosticCommandBase(command)) { - return command; - } else { - console.warn("received a JSON diagnostic server protocol command without command_set or command", command); - return null; - } -} diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/mock-remote.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/mock-remote.ts index df02fe7264796f..37c65800db01df 100644 --- a/src/mono/wasm/runtime/diagnostics/server_pthread/mock-remote.ts +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/mock-remote.ts @@ -2,53 +2,19 @@ // The .NET Foundation licenses this file to you under the MIT license. import monoDiagnosticsMock from "consts:monoDiagnosticsMock"; -import type { Mock, MockScriptEngine } from "../mock"; +import type { Mock } from "../mock"; import { mock } from "../mock"; -import { createPromiseController } from "../../promise-controller"; -function expectAdvertise(data: string | ArrayBuffer) { - if (typeof (data) === "string") { - return data === "ADVR_V1"; +export function importAndInstantiateMock(mockURL: string): Promise { + if (monoDiagnosticsMock) { + const mockPrefix = "mock:"; + const scriptURL = mockURL.substring(mockPrefix.length); + return import(scriptURL).then((mockModule) => { + const script = mockModule.default; + return mock(script, { trace: true }); + }); } else { - const view = new Uint8Array(data); - const ADVR_V1 = Array.from("ADVR_V1\0").map((c) => c.charCodeAt(0)); - return view.length >= ADVR_V1.length && ADVR_V1.every((v, i) => v === view[i]); + return Promise.resolve(undefined as unknown as Mock); } } -const scriptPC = createPromiseController().promise_control; -const scriptPCunfulfilled = createPromiseController().promise_control; - -const script: ((engine: MockScriptEngine) => Promise)[] = [ - async (engine) => { - await engine.waitForSend(expectAdvertise); - engine.reply(JSON.stringify({ - command_set: "EventPipe", command: "CollectTracing2", - circularBufferMB: 1, - format: 1, - requestRundown: true, - providers: [ - { - keywords: [0, 0], - logLevel: 5, - provider_name: "WasmHello", - filter_data: "EventCounterIntervalSec=1" - } - ] - })); - scriptPC.resolve(); - }, - async (engine) => { - await engine.waitForSend(expectAdvertise); - await scriptPC.promise; - engine.reply(JSON.stringify({ "command_set": "Process", "command": "ResumeRuntime" })); - // engine.close(); - }, - async (engine) => { - await engine.waitForSend(expectAdvertise); - await scriptPCunfulfilled.promise; - } -]; - -/// a mock script that simulates the initial part of the diagnostic server protocol -export const mockScript = monoDiagnosticsMock ? mock(script, { trace: true }) : undefined as unknown as Mock; diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/protocol-client-commands.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/protocol-client-commands.ts index 9450bf63588803..0232ad32db7461 100644 --- a/src/mono/wasm/runtime/diagnostics/server_pthread/protocol-client-commands.ts +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/protocol-client-commands.ts @@ -47,6 +47,8 @@ export interface EventPipeCollectTracingCommandProvider { filter_data: string; } +export type RemoveCommandSetAndId = Omit; + export type ProtocolClientCommand = ProcessCommand | EventPipeCommand; export function isDiagnosticCommandBase(x: object): x is ProtocolClientCommandBase { diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/protocol-socket.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/protocol-socket.ts index a9a7e3ebfc0e41..e66d24a681ed16 100644 --- a/src/mono/wasm/runtime/diagnostics/server_pthread/protocol-socket.ts +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/protocol-socket.ts @@ -9,6 +9,7 @@ import { } from "./ipc-protocol/types"; import Magic from "./ipc-protocol/magic"; import Parser from "./ipc-protocol/base-parser"; +import { assertNever } from "../../types"; export const dotnetDiagnosticsServerProtocolCommandEvent = "dotnet:diagnostics:protocolCommand" as const; @@ -173,14 +174,19 @@ class ProtocolSocketImpl implements ProtocolSocket { private readonly messageListener: (this: CommonSocket, ev: MessageEvent) => void = this.onMessage.bind(this); constructor(private readonly sock: CommonSocket) { } - onMessage(this: ProtocolSocketImpl, ev: MessageEvent): void { + onMessage(this: ProtocolSocketImpl, ev: MessageEvent): void { + const data = ev.data; console.debug("MONO_WASM: protocol socket received message", ev.data); - if (typeof ev.data === "object" && ev.data instanceof ArrayBuffer) { - this.onArrayBuffer(ev.data); - } else if (typeof ev.data === "object" && ev.data instanceof Blob) { - ev.data.arrayBuffer().then(this.onArrayBuffer.bind(this)); + if (typeof data === "object" && data instanceof ArrayBuffer) { + this.onArrayBuffer(data); + } else if (typeof data === "object" && data instanceof Blob) { + data.arrayBuffer().then(this.onArrayBuffer.bind(this)); + } else if (typeof data === "string") { + // otherwise it's string, ignore it. + console.debug("MONO_WASM: protocol socket received string message; ignoring it", ev.data); + } else { + assertNever(data); } - // otherwise it's string, ignore it. } dispatchEvent(evt: Event): boolean { diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/tsconfig.json b/src/mono/wasm/runtime/diagnostics/server_pthread/tsconfig.json index 071a4d824c62a4..d74ee6a53077b3 100644 --- a/src/mono/wasm/runtime/diagnostics/server_pthread/tsconfig.json +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/tsconfig.json @@ -1,5 +1,8 @@ { "extends": "../../tsconfig.worker.json", + "compilerOptions": { + "module": "commonjs", + }, "include": [ "../../**/*.ts", "../../**/*.d.ts" diff --git a/src/mono/wasm/runtime/pthreads/browser/index.ts b/src/mono/wasm/runtime/pthreads/browser/index.ts index b490436088ff40..f71ad533cfaea3 100644 --- a/src/mono/wasm/runtime/pthreads/browser/index.ts +++ b/src/mono/wasm/runtime/pthreads/browser/index.ts @@ -86,7 +86,7 @@ function monoWorkerMessageHandler(worker: Worker, ev: MessageEvent make const allConfigs = [ iffeConfig, typesConfig, -].concat(workerConfigs); +].concat(workerConfigs) + .concat(diagnosticMockTypesConfig ? [diagnosticMockTypesConfig] : []); export default defineConfig(allConfigs); // this would create .sha256 file next to the output file, so that we do not touch datetime of the file if it's same -> faster incremental build. diff --git a/src/mono/wasm/runtime/tsconfig.shared.json b/src/mono/wasm/runtime/tsconfig.shared.json index 71af2ed6d9a5c8..4c3803845a9e8f 100644 --- a/src/mono/wasm/runtime/tsconfig.shared.json +++ b/src/mono/wasm/runtime/tsconfig.shared.json @@ -14,6 +14,7 @@ }, "exclude": [ "dotnet.d.ts", + "diagnostics-mock.d.ts", "bin" ] } diff --git a/src/mono/wasm/wasm.proj b/src/mono/wasm/wasm.proj index 9f4de70026d9aa..994f849f171cf7 100644 --- a/src/mono/wasm/wasm.proj +++ b/src/mono/wasm/wasm.proj @@ -354,7 +354,7 @@ <_RollupInputs Include="$(MonoProjectRoot)wasm/runtime/**/*.ts" - Exclude="$(MonoProjectRoot)wasm/runtime/dotnet.d.ts;$(MonoProjectRoot)wasm/runtime/node_modules/**/*.ts" /> + Exclude="$(MonoProjectRoot)wasm/runtime/dotnet.d.ts;$(MonoProjectRoot)wasm/runtime/diagnostics-mock.d.ts;$(MonoProjectRoot)wasm/runtime/node_modules/**/*.ts" /> <_RollupInputs Include="$(MonoProjectRoot)wasm/runtime/**/tsconfig.*" Exclude="$(MonoProjectRoot)wasm/runtime/node_modules/**/tsconfig.*" /> <_RollupInputs Include="$(MonoProjectRoot)wasm/runtime/workers/**/*.js"/> From 18e199ad4dc90fea4064f8ef4fde47666ef19b8d Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Fri, 22 Jul 2022 15:33:25 -0400 Subject: [PATCH 82/84] disable mocking in the sample project by default --- .../browser-eventpipe/Wasm.Browser.EventPipe.Sample.csproj | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/mono/sample/wasm/browser-eventpipe/Wasm.Browser.EventPipe.Sample.csproj b/src/mono/sample/wasm/browser-eventpipe/Wasm.Browser.EventPipe.Sample.csproj index 206be4533d8f88..94253eaed38346 100644 --- a/src/mono/sample/wasm/browser-eventpipe/Wasm.Browser.EventPipe.Sample.csproj +++ b/src/mono/sample/wasm/browser-eventpipe/Wasm.Browser.EventPipe.Sample.csproj @@ -14,7 +14,7 @@ - true + false @@ -25,14 +25,17 @@ "MONO_LOG_LEVEL": "warning", "MONO_LOG_MASK": "all" }' /> + + +