Add a diagnostic server for WebAssembly. Enable by building the runtime with `/p:WasmEnablePerfTracing=true` or `/p:WasmEnableThreads=true`.
To configure a project to start the diagnostic server, add this to the .csproj:
```xml
<WasmExtraConfig Include="diagnostic_options" Value='
{
"server": { "suspend": false, "connect_url": "ws://localhost:8088/diagnostics" }
}' />
```
The `connect_url` should be a WebSocket url serviced by `dotnet-dsrouter server-websocket` **from this branch** https://github.com/lambdageek/diagnostics/tree/wasm-server
Note that setting `"suspend": true` will hang the browser tab until a diagnostic tool such as `dotnet-trace collect` connects to the dsrouter.
---
Implement creating VFS file based sessions at runtime startup. Add the following to a .csproj:
```xml
<WasmExtraConfig Include="diagnostic_options" Value='
{
"sessions": [ { "collectRundownEvents": "true", "providers": "WasmHello::5:EventCounterIntervalSec=1" } ]
}' />
```
That will create and start one or more EventPipe sessions that will store their results into the VFS.
The startup session can be retrieved via `MONO.diagnostics.getStartupSessions()`. Each session `s` should be stopped via `s.stop()` and the data can then be extraced in a `Blob` using `s.getTraceBlob()`.
This is orthogonal to the diagnostic server support. You don't need `dotnet-dsrouter` running on the host. But you do need access to JavaScript on the main thread.
---
Notes/Issues:
* Tree shaking: I verified that if threads are not available, all the TypeScript diagnostics code is removed.
* Right now the server is not very robust to `dotnet-dsrouter` stopping, or starting after the runtime starts. The ideal order is to start `dotnet-dsrouter` first, and then open the browser
* Unrelated housekeeping fixes:
* Tell `wasm.proj` about all the subdirectories with .ts files - makes incremental builds notice changes in subdirectories.
* Add a rollup `dependencies` property to quiet a warning about `node/buffer`
* There's a mock implementation of a "websocket" that was used for protocol testing. I verified that tree-shaking removes this in thread-enabled Release builds.
* Bump `PTHREAD_POOL_SIZE` to `4` and set `PTHREAD_POOL_SIZE_STRICT=2` (returns `EAGAIN` from `pthread_create` if the pool needs to grow). The previous setting `PTHREAD_POOL_SIZE_STRING=1` (warn and try to grow the pool) seemed to lead to hangs. Usually that means the main thread is creating a thread and immediately calling `pthread_join` without returning to JS. We should investigate separately.
* The only implemented diagnostic server commands are `CollectTracing2`, `StopCollecting` and `ResumeRuntime`. None of the `Dump`, `Process` and `Profiler` commands are implemented and the server will crash if it receives them. It should be relatively straightforward to return a "command unsupported" reply (which would allow clients to gracefully disconnect), but it's not done yet.
* In some error states the runtime kills the browser tab with the following in a terminal window (if Chrome is started from a terminal: `FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory`). This probably means we're hitting a loop somewhere that rapidly exhausts JIT memory, but it's difficult to investigate when the JS console dies, too (happens with chrome stable v103 and chrome beta v104).
Fixes https://github.com/dotnet/runtime/issues/69674, contributes to https://github.com/dotnet/runtime/issues/72481
---
* [wasm] Enable the tracing component if threading is supported
* add a way to specify EP sessions in the MonoConfig
Currently not wired up to the runtime
* Add a mechanism to copy startup configs into the runtime and session IDs out
* 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.
* checkpoint: starting a session at startup works
* checkpoint add a controller and a webworker for DS
* checkpoint: diagnostic server
* fix eslint
* [diagnostic_server] wasm-specific fn_table
We won't be using the native C version
* [wasm-ep] disable DS connect ports in C, too
we will implement DS in JS
* 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.
* checkpoint: 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
* 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.
* wasm-mt: use a PThreadSelf struct instead of a raw MessagePort
* Move all the EP and diagnostic server modules to one directory
* 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.
* checkpoint: 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] 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.
* 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.
* WIP: set up a WasmIpcStream, create EP sessions from DS
Seems to create the session, but not seeing write events
yet. possibly due to not flushing?
* 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.
* cleanup browser-eventpipe sample
* call mono_wasm_event_pipe_early_startup_callback from event_pipe init
instead of from the rundown_execution_checkpoint_2 function
* if diagnostics server isn't enabled, don't try to initialize it
* checkpoint: start parsing binary commands
* checkpoint: Can parse a CollectTracing2 command and attempt to create a
session!
* [wasm-ep] use the new PromiseController<T>
* get back to the server loop quicker by queueing the parsing in the microtask
* update mock for binary ADVR_V1 message
* sample: don't suspend, and use a mock url
* wasm_ipc_stream: wire up close command
Use a sentinal "buf" value (-1) to signal that the writer closed the stream
* Send proper OK messages in replies to binary protocol commands
* (testing) turn off the file session for now
* remove em_asm(console.log); simplify wasm EP init
Just call the EP JS callback directly from native
* remove debug output
* cleanup wasm ipc stream impl
* put diagnostics mocks behind a const flag
* don't build wasm-specific DS if threads are disabled
* refactor and cleanup
- Move the IPC parsing and serialization into separate files
- Try to have one responsibility per class
- update comments and docs
* help treeshaking
verified that all the DS and EP JS code is dropped if monoWasmThreads is false.
* update DS design notes
* use PromiseController in more places
* fix Windows build
* add MONO_WASM prefix to console logging outputs
* improve debug output for DS server
keep track of open/advertise counts and print them when receiving replies
* 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
* fix bug in queue_push_sync main thread detection
* merge fixup
* fix rollup warning when making the crypto worker
* add MONO_WASM: prefix to logging
* 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.
* disable mocking in the sample project by default
* fixup after merge
* review feedback
- improve diagnostics mock README
- note that mocking just uses ES6 modules, testing with CJS is not supported right now.
- fix iteration over listeners when dispatching a one-shot event in the EventTargt polyfill
- use U32 getter in EP session creation
<ItemGroup Condition="'$(TargetsBrowser)' == 'true'">
<_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" />
</ItemGroup>
<!-- Components -->
#include <mono/utils/mono-publib.h>
#include <mono/utils/mono-compiler.h>
#include <eventpipe/ds-server.h>
+#ifdef HOST_WASM
+#include <eventpipe/ep-ipc-stream.h>
+#include <mono/component/event_pipe-wasm.h>
+#include <mono/utils/mono-coop-semaphore.h>
+#include <mono/utils/mono-threads-wasm.h>
+#include <emscripten/emscripten.h>
+#include <emscripten/threading.h>
+#endif
static bool
diagnostics_server_available (void);
+#if !defined (HOST_WASM) || defined (DISABLE_THREADS)
static MonoComponentDiagnosticsServer fn_table = {
{ MONO_COMPONENT_ITF_VERSION, &diagnostics_server_available },
&ds_server_init,
&ds_server_disable
};
+#else /* !defined (HOST_WASM) || defined (DISABLE_THREADS) */
+
+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,
+};
+
+typedef struct _MonoWasmDiagnosticServerOptions {
+ int32_t suspend; /* set from JS! */
+ MonoCoopSem suspend_resume;
+} MonoWasmDiagnosticServerOptions;
+
+static MonoWasmDiagnosticServerOptions wasm_ds_options;
+static pthread_t ds_thread_id;
+
+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)
+{
+ /* called on the main thread when the runtime is sufficiently initialized */
+ mono_coop_sem_init (&wasm_ds_options.suspend_resume, 0);
+ mono_wasm_diagnostic_server_on_runtime_server_init(&wasm_ds_options);
+ return true;
+}
+
+
+static bool
+ds_server_wasm_shutdown (void)
+{
+ mono_coop_sem_destroy (&wasm_ds_options.suspend_resume);
+ return true;
+}
+
+static void
+ds_server_wasm_pause_for_diagnostics_monitor (void)
+{
+ /* wait until the DS receives a resume */
+ if (wasm_ds_options.suspend) {
+ /* 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);
+ }
+}
+
+
+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,
+ * 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)
+{
+ g_assert (ds_websocket_url != NULL);
+ 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;
+
+ 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)) {
+ *out_thread_id = thread;
+ return TRUE;
+ }
+ memset(out_thread_id, 0, sizeof(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);
+ }
+}
+
+#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; /* or QUEUE_CLOSE_SENTINEL */
+ int32_t count;
+ volatile int32_t buf_full;
+} WasmIpcStreamQueue;
+
+extern void
+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, 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
+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;
+ /* 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 = 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 (!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);
+ } 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 (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;
+}
+
+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)
+{
+ WasmIpcStream *stream = (WasmIpcStream*)self;
+ // push the special buf value -1 to signal stream close.
+ int r = queue_push_sync (&stream->queue, QUEUE_CLOSE_SENTINEL, 0, NULL);
+ return r == 0;
+}
+
+#endif /* !defined (HOST_WASM) || defined (DISABLE_THREADS) */
+
static bool
diagnostics_server_available (void)
{
#include "mono/component/event_pipe.h"
#include "mono/component/event_pipe-wasm.h"
#include "mono/metadata/components.h"
-#ifdef HOST_WASM
-#include <emscripten/emscripten.h>
-#endif
static EventPipeSessionID _dummy_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, */
/* 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)
{
g_assert_not_reached ();
}
-
#endif /* HOST_WASM */
#ifdef HOST_WASM
+#include <pthread.h>
#include <emscripten.h>
G_BEGIN_DECLS
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);
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);
+
+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);
+
G_END_DECLS
#endif /* HOST_WASM */
EventPipeSessionID session_id,
uint32_t timeout);
+#ifdef HOST_WASM
+static 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,
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)
{
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,
session_type,
format,
!!rundown_requested,
- /* stream */NULL,
+ ipc_stream,
/* callback*/ NULL,
/* callback_data*/ NULL);
return TRUE;
}
+// 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 ();
+ mono_wasm_event_pipe_early_startup_callback ();
+}
+
#endif /* HOST_WASM */
void
mono_threads_wasm_async_run_in_main_thread_vii (void (*func)(gpointer, gpointer), gpointer user_data1, gpointer user_data2);
+
+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 /* DISABLE_THREADS */
// Called from register_thread when a pthread attaches to the runtime
<NoWarn>CA2007</NoWarn> <!-- consider ConfigureAwait() -->
</PropertyGroup>
+ <PropertyGroup>
+ <MonoDiagnosticsMock Condition="'$(MonoDiagnosticsMock)' == ''">false</MonoDiagnosticsMock>
+ </PropertyGroup>
+
<ItemGroup>
<WasmExtraFilesToDeploy Include="index.html" />
- <WasmExtraConfig Condition="false" Include="environment_variables" Value='
+ <WasmExtraFilesToDeploy Include="mock.js" Condition="'$(MonoDiagnosticsMock)' == 'true'"/>
+ <WasmExtraConfig Condition="true" Include="environment_variables" Value='
+{
+ "MONO_LOG_LEVEL": "warning",
+ "MONO_LOG_MASK": "all"
+}' />
+ <!-- this option requires running dotnet-dsrouter and a real dotnet-trace client -->
+ <WasmExtraConfig Condition="true and '$(MonoDiagnosticsMock)' != 'true'" Include="diagnostic_options" Value='
+{
+ "server": { "suspend": true, "connect_url": "ws://localhost:8088/diagnostics" }
+}' />
+ <!-- this option requires compiling the runtime with /p:MonoDiagnosticsMock=true and also building this project with the same property-->
+ <WasmExtraConfig Condition="true and '$(MonoDiagnosticsMock)' == 'true'" Include="diagnostic_options" Value='
+{
+ "server": { "suspend": false, "connect_url": "mock:./mock.js" }
+}' />
+ <!-- this option will create an EventPipe session at startup, that will dump its data into the Emscripten VFS -->
+ <WasmExtraConfig Condition="false" Include="diagnostic_options" Value='
{
- "MONO_LOG_LEVEL": "debug",
- "MONO_LOG_MASK": "diagnostics"
+ "sessions": [ { "collectRundownEvents": "true", "providers": "WasmHello::5:EventCounterIntervalSec=1" } ]
}' />
</ItemGroup>
<label for="inputN">N:</label> <input type="number" id="inputN" value="10"/>
</div>
<div>Computing Fib(N) repeatedly: <span id="out"></span>
- <script type="module" src="./dotnet.worker.js"></script>
<script type="module" src="./main.js"></script>
</body>
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 eventSession = MONO.diagnostics.createEventPipeSession(options);
+ let sessions = MONO.diagnostics.getStartupSessions();
+
+ if (typeof (sessions) !== "object" || sessions.length === "undefined")
+ console.error("expected an array of sessions, got ", sessions);
+ 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
+ }
- eventSession.start();
const ret = await doWork(startWork, stopWork, getIterationsDone);
- eventSession.stop();
- const filename = "dotnet-wasm-" + makeTimestamp() + ".nettrace";
+ if (eventSession !== null) {
+ eventSession.stop();
+
+ const filename = "dotnet-wasm-" + makeTimestamp() + ".nettrace";
+
+ const blob = eventSession.getTraceBlob();
+ const uri = URL.createObjectURL(blob);
+ downloadData(uri, filename);
+ }
- const blob = eventSession.getTraceBlob();
- const uri = URL.createObjectURL(blob);
- downloadData(uri, filename);
+ console.debug("sample onclick handler done");
}
}
--- /dev/null
+/// @ts-check
+
+/**
+ * @typedef { import("../../../wasm/runtime/diagnostics-mock").MockEnvironment } MockEnvironment
+ * @typedef { import("../../../wasm/runtime/diagnostics-mock").MockScriptConnection } MockScriptConnection
+ * @typedef { import("../../../wasm/runtime/diagnostics-mock").PromiseAndController } PromiseAndController
+ * @typedef { import("../../../wasm/runtime/diagnostics-mock").PromiseAndController<number> } PromiseAndControllerNumber
+ * @typedef { import("../../../wasm/runtime/diagnostics-mock").PromiseAndController<void> } PromiseAndControllerVoid
+ */
+
+/**
+ * @param {MockEnvironment} env
+ * @returns {((conn: MockScriptConnection) => Promise<void>)[]}
+ * */
+function script(env) {
+ /** @type { PromiseAndControllerNumber } */
+ const sessionStarted = env.createPromiseController();
+ /** @type { PromiseAndControllerVoid } */
+ const runtimeResumed = env.createPromiseController();
+ /** @type { PromiseAndControllerVoid } */
+ const waitForever = env.createPromiseController();
+ return [
+ async (conn) => {
+ await conn.waitForSend(env.expectAdvertise);
+ conn.reply(env.command.makeEventPipeCollectTracing2({
+ circularBufferMB: 1,
+ format: 1,
+ requestRundown: true,
+ providers: [
+ {
+ keywords: [0, 0],
+ logLevel: 5,
+ provider_name: "WasmHello",
+ filter_data: "EventCounterIntervalSec=1"
+ }
+ ]
+ }));
+ const sessionID = await conn.waitForSend(env.reply.expectOk(4), env.reply.extractOkSessionID);
+ sessionStarted.promise_control.resolve(sessionID);
+ },
+ async (conn) => {
+ await Promise.all([conn.waitForSend(env.expectAdvertise), sessionStarted.promise]);
+ conn.reply(env.command.makeProcessResumeRuntime());
+ runtimeResumed.promise_control.resolve();
+ },
+ async (conn) => {
+ await Promise.all([conn.waitForSend(env.expectAdvertise), runtimeResumed.promise, sessionStarted.promise]);
+ const sessionID = await sessionStarted.promise;
+ await env.delay(5000);
+ conn.reply(env.command.makeEventPipeStopTracing({ sessionID }));
+ },
+ async (conn) => {
+ await conn.waitForSend(env.expectAdvertise);
+ await waitForever.promise;
+ }
+ ];
+};
+
+export default script;
<!-- This is temporary hack for https://github.com/dotnet/runtime/issues/61294 -->
<ItemGroup>
<_MonoRuntimeComponentDontLink Include="libmono-component-debugger-stub-static.a" />
- <_MonoRuntimeComponentDontLink Include="libmono-component-diagnostics_tracing-static.a" Condition="'$(FeatureWasmPerfTracing)' != 'true'"/>
+ <!-- FIXME: This will exclude the diagnostics component if tracing is not on and threads are not on. Which means that if you turn on threading, you will get diagnostics. Is this what we want? -->
+ <_MonoRuntimeComponentDontLink Include="libmono-component-diagnostics_tracing-static.a" Condition="'$(FeatureWasmPerfTracing)' != 'true' and $(FeatureWasmThreads) != 'true'"/>
<_MonoRuntimeComponentDontLink Include="libmono-component-hot_reload-stub-static.a" />
</ItemGroup>
</Project>
"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",
"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",
+ "mono_wasm_diagnostic_server_on_runtime_server_init",
+ "mono_wasm_diagnostic_server_stream_signal_work_available",
#endif
];
["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"]],
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
/**
NONE = 0,
NON_INTEGRAL = 1,
OUT_OF_RANGE = 2,
-}
\ No newline at end of file
+}
--- /dev/null
+//! Licensed to the .NET Foundation under one or more agreements.
+//! The .NET Foundation licenses this file to you under the MIT license.
+//!
+//! This is generated file, see src/mono/wasm/runtime/rollup.config.js
+
+//! This is not considered public API with backward compatibility guarantees.
+
+declare const promise_control_symbol: unique symbol;
+interface PromiseController<T = any> {
+ isDone: boolean;
+ readonly promise: Promise<T>;
+ resolve: (value: T | PromiseLike<T>) => void;
+ reject: (reason?: any) => void;
+}
+interface ControllablePromise<T = any> extends Promise<T> {
+ [promise_control_symbol]: PromiseController<T>;
+}
+interface PromiseAndController<T> {
+ promise: ControllablePromise<T>;
+ promise_control: PromiseController<T>;
+}
+
+interface ProtocolClientCommandBase {
+ command_set: string;
+ command: string;
+}
+interface EventPipeClientCommandBase extends ProtocolClientCommandBase {
+ command_set: "EventPipe";
+}
+interface EventPipeCommandCollectTracing2 extends EventPipeClientCommandBase {
+ command: "CollectTracing2";
+ circularBufferMB: number;
+ format: number;
+ requestRundown: boolean;
+ providers: EventPipeCollectTracingCommandProvider[];
+}
+interface EventPipeCommandStopTracing extends EventPipeClientCommandBase {
+ command: "StopTracing";
+ sessionID: number;
+}
+interface EventPipeCollectTracingCommandProvider {
+ keywords: [number, number];
+ logLevel: number;
+ provider_name: string;
+ filter_data: string;
+}
+declare type RemoveCommandSetAndId<T extends ProtocolClientCommandBase> = Omit<T, "command_set" | "command">;
+
+declare type FilterPredicate = (data: ArrayBuffer) => boolean;
+interface MockScriptConnection {
+ waitForSend(filter: FilterPredicate): Promise<void>;
+ waitForSend<T>(filter: FilterPredicate, extract: (data: ArrayBuffer) => T): Promise<T>;
+ reply(data: ArrayBuffer): void;
+}
+interface MockEnvironmentCommand {
+ makeEventPipeCollectTracing2(payload: RemoveCommandSetAndId<EventPipeCommandCollectTracing2>): Uint8Array;
+ makeEventPipeStopTracing(payload: RemoveCommandSetAndId<EventPipeCommandStopTracing>): Uint8Array;
+ makeProcessResumeRuntime(): Uint8Array;
+}
+interface MockEnvironmentReply {
+ expectOk(extraPayload?: number): FilterPredicate;
+ extractOkSessionID(data: ArrayBuffer): number;
+}
+interface MockEnvironment {
+ createPromiseController<T>(): PromiseAndController<T>;
+ delay: (ms: number) => Promise<void>;
+ command: MockEnvironmentCommand;
+ reply: MockEnvironmentReply;
+ expectAdvertise: FilterPredicate;
+}
+
+export { MockEnvironment, MockScriptConnection, PromiseAndController };
--- /dev/null
+4a0d47e4e5fabdc42443a54d452be697161a5053d1e24c867200e42742427e3f
\ No newline at end of file
+++ /dev/null
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-import { Module } from "./imports";
-import cwraps from "./cwraps";
-import type { EventPipeSessionOptions } from "./types";
-import type { VoidPtr } from "./types/emscripten";
-import * as memory from "./memory";
-
-const sizeOfInt32 = 4;
-
-export type EventPipeSessionID = bigint;
-type EventPipeSessionIDImpl = number;
-
-/// An EventPipe session object represents a single diagnostic tracing session that is collecting
-/// events from the runtime and managed libraries. There may be multiple active sessions at the same time.
-/// Each session subscribes to a number of providers and will collect events from the time that start() is called, until stop() is called.
-/// Upon completion the session saves the events to a file on the VFS.
-/// The data can then be retrieved as Blob.
-export interface EventPipeSession {
- // session ID for debugging logging only
- get sessionID(): EventPipeSessionID;
- start(): void;
- stop(): void;
- getTraceBlob(): Blob;
-}
-
-// internal session state of the JS instance
-enum State {
- Initialized,
- Started,
- Done,
-}
-
-function start_streaming(sessionID: EventPipeSessionIDImpl): void {
- cwraps.mono_wasm_event_pipe_session_start_streaming(sessionID);
-}
-
-function stop_streaming(sessionID: EventPipeSessionIDImpl): void {
- cwraps.mono_wasm_event_pipe_session_disable(sessionID);
-}
-
-/// An EventPipe session that saves the event data to a file in the VFS.
-class EventPipeFileSession implements EventPipeSession {
- private _state: State;
- private _sessionID: EventPipeSessionIDImpl;
- private _tracePath: string; // VFS file path to the trace file
-
- get sessionID(): bigint { return BigInt(this._sessionID); }
-
- constructor(sessionID: EventPipeSessionIDImpl, tracePath: string) {
- this._state = State.Initialized;
- this._sessionID = sessionID;
- this._tracePath = tracePath;
- console.debug(`EventPipe session ${this.sessionID} created`);
- }
-
- start = () => {
- if (this._state !== State.Initialized) {
- throw new Error(`EventPipe session ${this.sessionID} already started`);
- }
- this._state = State.Started;
- start_streaming(this._sessionID);
- console.debug(`EventPipe session ${this.sessionID} started`);
- };
-
- stop = () => {
- if (this._state !== State.Started) {
- throw new Error(`cannot stop an EventPipe session in state ${this._state}, not 'Started'`);
- }
- this._state = State.Done;
- stop_streaming(this._sessionID);
- console.debug(`EventPipe session ${this.sessionID} stopped`);
- };
-
- getTraceBlob = () => {
- if (this._state !== State.Done) {
- throw new Error(`session is in state ${this._state}, not 'Done'`);
- }
- const data = Module.FS_readFile(this._tracePath, { encoding: "binary" }) as Uint8Array;
- return new Blob([data], { type: "application/octet-stream" });
- };
-}
-
-const eventLevel = {
- LogAlways: 0,
- Critical: 1,
- Error: 2,
- Warning: 3,
- Informational: 4,
- Verbose: 5,
-} as const;
-
-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 SessionOptionsBuilder.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;
-
-function createSessionWithPtrCB(sessionIdOutPtr: VoidPtr, options: EventPipeSessionOptions | undefined, 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;
-
- memory.setI32(sessionIdOutPtr, 0);
- if (!cwraps.mono_wasm_event_pipe_enable(tracePath, defaultBufferSizeInMB, providers, rundown, sessionIdOutPtr)) {
- return false;
- } else {
- return memory.getI32(sessionIdOutPtr);
- }
-}
-
-export interface Diagnostics {
- EventLevel: EventLevel;
- SessionOptionsBuilder: typeof SessionOptionsBuilder;
-
- createEventPipeSession(options?: EventPipeSessionOptions): EventPipeSession | null;
-}
-
-
-/// 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(options?: EventPipeSessionOptions): EventPipeSession | null {
- // The session trace is saved to a file in the VFS. The file name doesn't matter,
- // but we'd like it to be distinct from other traces.
- const tracePath = `/trace-${totalSessions++}.nettrace`;
-
- const success = memory.withStackAlloc(sizeOfInt32, createSessionWithPtrCB, options, tracePath);
-
- if (success === false)
- return null;
- const sessionID = success;
-
- const session = new EventPipeFileSession(sessionID, tracePath);
- return session;
- },
-};
-
-export default diagnostics;
--- /dev/null
+# Diagnostic Server and EventPipe
+
+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`
+- `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.
+
+## Mocking diagnostics clients
+
+In `diagnostics/mock` we provide a framework for mocking a connection between the diagnostic server and a diagnostic client.
+Instead of creating tests around a real WebSocket connection to `dotnet-dsrouter` and a tool such as `dotnet-trace collect`, we
+can simulate a connection by playing back a script. The script represents the commands and responses of a client such as `dotnet-trace` that is connected to `dotnet-dsrouter`.
+
+Build the runtime with `/p:MonoDiagnosticsMock=true`.
+
+To use mocking:
+
+1. create a `mock.js` file in your project,
+
+2. add it using `<WasmExtraFilesToDeploy Include="mock.js" />` to your `.csproj`
+
+3. configure the diagnostics server with a `mock:relative_url_of/mock.js`
+
+ ```xml
+ <WasmExtraConfig Include="diagnostic_options" Value='
+ {
+ "server": { "suspend": false, "connect_url": "mock:./mock.js" }
+ }' />
+ ```
+
+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<void>`.
+
+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<void>` or `waitForSend<T>(filter: (data: string | ArrayBuffer) => boolean, extract: (data: string | ArrayBuffer) => T): Promise<T>`. 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 */
+ ]
+}
+```
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+import cwraps from "../../cwraps";
+import { withStackAlloc, getI32 } from "../../memory";
+import { Thread, waitForThread } from "../../pthreads/browser";
+import { isDiagnosticMessage, makeDiagnosticServerControlCommand } from "../shared/controller-commands";
+
+/// An object that can be used to control the diagnostic server.
+export interface ServerController {
+ postServerAttachToRuntime(): void;
+}
+
+class ServerControllerImpl implements ServerController {
+ constructor(private server: Thread) {
+ server.port.addEventListener("message", this.onServerReply.bind(this));
+ }
+ start(): void {
+ console.debug("MONO_WASM: signaling the diagnostic server to start");
+ this.server.postMessageToWorker(makeDiagnosticServerControlCommand("start"));
+ }
+ stop(): void {
+ console.debug("MONO_WASM: signaling the diagnostic server to stop");
+ this.server.postMessageToWorker(makeDiagnosticServerControlCommand("stop"));
+ }
+ postServerAttachToRuntime(): void {
+ console.debug("MONO_WASM: 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) {
+ default:
+ console.warn("MONO_WASM: Unknown control reply command: ", <any>d);
+ break;
+ }
+ }
+ }
+}
+
+let serverController: ServerController | null = null;
+
+export function getController(): ServerController {
+ if (serverController)
+ return serverController;
+ throw new Error("unexpected no server controller");
+}
+
+export async function startDiagnosticServer(websocket_url: string): Promise<ServerController | null> {
+ const sizeOfPthreadT = 4;
+ 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;
+ const pthreadId = getI32(pthreadIdPtr);
+ return pthreadId;
+ });
+ if (result === undefined) {
+ console.warn("MONO_WASM: diagnostic server failed to start");
+ return null;
+ }
+ // 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");
+ }
+ const serverControllerImpl = new ServerControllerImpl(thread);
+ serverController = serverControllerImpl;
+ serverControllerImpl.start();
+ return serverControllerImpl;
+}
+
--- /dev/null
+// 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(`MONO_WASM: EventPipe session ${this.sessionID} created`);
+ }
+
+ start = () => {
+ if (this._state !== State.Initialized) {
+ throw new Error(`MONO_WASM: EventPipe session ${this.sessionID} already started`);
+ }
+ this._state = State.Started;
+ start_streaming(this._sessionID);
+ console.debug(`MONO_WASM: 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(`MONO_WASM: 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);
+}
+
+
--- /dev/null
+// 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 SessionOptionsBuilder.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(",")
+ };
+ }
+}
+
--- /dev/null
+# 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.
+
+- 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
+
+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.
+
+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.
+
+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.
+
+## 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 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.
--- /dev/null
+// 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,
+} 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 "./browser/session-options-builder";
+import {
+ eventLevel, EventLevel,
+ SessionOptionsBuilder,
+} from "./browser/session-options-builder";
+import { EventPipeSession, makeEventPipeSession } from "./browser/file-session";
+
+export interface Diagnostics {
+ EventLevel: EventLevel;
+ SessionOptionsBuilder: typeof SessionOptionsBuilder;
+
+ createEventPipeSession(options?: EventPipeSessionOptions): EventPipeSession | null;
+ getStartupSessions(): (EventPipeSession | null)[];
+}
+
+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 (monoWasmThreads) {
+ if (startup_session_configs === null || startup_session_configs.length == 0) {
+ return;
+ }
+ 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 = [];
+ }
+}
+
+
+function createAndStartEventPipeSession(options: (EventPipeSessionOptions)): EventPipeSession | null {
+ const session = makeEventPipeSession(options);
+ if (session === null) {
+ return null;
+ }
+ session.start();
+
+ 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 = getDiagnostics();
+
+// 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
+
+let suspendOnStartup = false;
+let diagnosticsServerEnabled = false;
+
+export async function mono_wasm_init_diagnostics(options: DiagnosticOptions): Promise<void> {
+ if (!monoWasmThreads) {
+ console.warn("MONO_WASM: 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);
+ }
+}
+
+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 {
+ 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;
--- /dev/null
+// 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<EventPipeCommandCollectTracing2>): 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<EventPipeCommandCollectTracing2>): 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<EventPipeCommandStopTracing>): 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
+ };
+}
--- /dev/null
+// 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";
--- /dev/null
+// 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";
+
+import { createMockEnvironment } from "./environment";
+import type { MockEnvironment, MockScriptConnection } from "./export-types";
+import { assertNever } from "../../types";
+
+export interface MockRemoteSocket extends EventTarget {
+ addEventListener<T extends keyof WebSocketEventMap>(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;
+}
+
+export interface Mock {
+ open(): MockRemoteSocket;
+ run(): Promise<void>;
+}
+
+interface MockOptions {
+ readonly trace: boolean;
+}
+
+type MockConnectionScript = (engine: MockScriptConnection) => Promise<void>;
+export type MockScript = (env: MockEnvironment) => MockConnectionScript[];
+
+let MockImplConstructor: new (script: MockScript, options?: MockOptions) => Mock;
+export function mock(script: MockScript, options?: MockOptions): Mock {
+ 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<string | ArrayBuffer> | 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<T extends keyof WebSocketEventMap>(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 MockScriptConnection {
+ 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: ArrayBuffer | Uint8Array) {
+ if (this.trace) {
+ console.debug(`mock ${this.ident} reply:`, 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<T = void>(filter: (data: ArrayBuffer) => boolean, extract?: (data: ArrayBuffer) => T): Promise<T> {
+ const trace = this.trace;
+ if (trace) {
+ console.debug(`mock ${this.ident} waitForSend`);
+ }
+ const event = await new Promise<MessageEvent<string | ArrayBuffer>>((resolve) => {
+ this.mockReplyEventTarget.addEventListener("message", (event) => {
+ if (trace) {
+ console.debug(`mock ${this.ident} waitForSend got:`, event);
+ }
+ resolve(event as MessageEvent<string | ArrayBuffer>);
+ }, { once: true });
+ });
+ 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");
+ }
+ 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 mockScript: MockScript, options?: MockOptions) {
+ const env: MockEnvironment = createMockEnvironment();
+ this.connectionScripts = mockScript(env);
+ this.openCount = 0;
+ this.trace = options?.trace ?? false;
+ const count = this.connectionScripts.length;
+ this.engines = new Array<MockScriptEngineImpl>(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<void> {
+ const scripts = this.connectionScripts;
+ await Promise.all(scripts.map((script, i) => script(this.engines[i])));
+ }
+ };
+ }
+ return new MockImplConstructor(script, options);
+ } else {
+ return undefined as unknown as Mock;
+ }
+}
+
+
--- /dev/null
+{
+ "extends": "../../tsconfig.worker.json",
+ "include": [
+ "../../**/*.ts",
+ "../../**/*.d.ts"
+ ]
+}
--- /dev/null
+// 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<void>;
+ waitForSend<T>(filter: FilterPredicate, extract: (data: ArrayBuffer) => T): Promise<T>;
+ reply(data: ArrayBuffer): void;
+}
+
+interface MockEnvironmentCommand {
+ makeEventPipeCollectTracing2(payload: RemoveCommandSetAndId<EventPipeCommandCollectTracing2>): Uint8Array;
+ makeEventPipeStopTracing(payload: RemoveCommandSetAndId<EventPipeCommandStopTracing>): Uint8Array;
+ makeProcessResumeRuntime(): Uint8Array;
+}
+
+interface MockEnvironmentReply {
+
+ expectOk(extraPayload?: number): FilterPredicate;
+
+ extractOkSessionID(data: ArrayBuffer): number;
+
+}
+
+export interface MockEnvironment {
+ createPromiseController<T>(): PromiseAndController<T>;
+ delay: (ms: number) => Promise<void>;
+ command: MockEnvironmentCommand;
+ reply: MockEnvironmentReply;
+ expectAdvertise: FilterPredicate;
+}
--- /dev/null
+// 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<T extends keyof WebSocketEventMap>(type: T, listener: (this: CommonSocket, ev: WebSocketEventMap[T]) => any, options?: boolean | AddEventListenerOptions): void;
+ addEventListener(event: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
+ removeEventListener<T extends keyof WebSocketEventMap>(type: T, listener: (this: CommonSocket, ev: WebSocketEventMap[T]) => any): void;
+ removeEventListener(event: string, listener: EventListenerOrEventListenerObject): void;
+ dispatchEvent(evt: Event): boolean;
+ // 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;
+}
+
+
+type AssignableTo<T, Q> = Q extends T ? true : false;
+
+function static_assert<Cond extends boolean>(x: Cond): asserts x is Cond { /*empty*/ }
+
+{
+ static_assert<AssignableTo<CommonSocket, WebSocket>>(true);
+ static_assert<AssignableTo<CommonSocket, MockRemoteSocket>>(true);
+
+ static_assert<AssignableTo<{ x: number }, { y: number }>>(false); // sanity check that static_assert works
+}
+
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+/// <reference lib="webworker" />
+
+import monoDiagnosticsMock from "consts:monoDiagnosticsMock";
+import { assertNever } from "../../types";
+import { pthread_self } from "../../pthreads/worker";
+import { Module } from "../../imports";
+import cwraps from "../../cwraps";
+import { EventPipeSessionIDImpl } from "../shared/types";
+import { CharPtr } from "../../types/emscripten";
+import {
+ DiagnosticServerControlCommand,
+ isDiagnosticMessage
+} from "../shared/controller-commands";
+
+import { importAndInstantiateMock } from "./mock-remote";
+import type { Mock, MockRemoteSocket } from "../mock";
+import { PromiseAndController, createPromiseController } from "../../promise-controller";
+import {
+ isEventPipeCommand,
+ isProcessCommand,
+ ProtocolClientCommandBase,
+ EventPipeClientCommandBase,
+ ProcessClientCommandBase,
+ isEventPipeCommandCollectTracing2,
+ isEventPipeCommandStopTracing,
+ isProcessCommandResumeRuntime,
+ EventPipeCommandCollectTracing2,
+} from "./protocol-client-commands";
+import { makeEventPipeStreamingSession } from "./streaming-session";
+import { CommonSocket } from "./common-socket";
+import {
+ createProtocolSocket, dotnetDiagnosticsServerProtocolCommandEvent,
+ ProtocolCommandEvent,
+} from "./protocol-socket";
+import {
+ BinaryProtocolCommand,
+ isBinaryProtocolCommand,
+} from "./ipc-protocol/types";
+import {
+ parseBinaryProtocolCommand,
+ ParseClientCommandResult,
+} from "./ipc-protocol/parser";
+import {
+ createAdvertise,
+ createBinaryCommandOKReply,
+} from "./ipc-protocol/serializer";
+
+function addOneShotProtocolCommandEventListener(src: EventTarget): Promise<ProtocolCommandEvent> {
+ return new Promise((resolve) => {
+ const listener = (event: Event) => { resolve(event as ProtocolCommandEvent); };
+ src.addEventListener(dotnetDiagnosticsServerProtocolCommandEvent, listener, { once: true });
+ });
+}
+
+function addOneShotOpenEventListenr(src: EventTarget): Promise<Event> {
+ 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: Promise<Mock> | undefined;
+ runtimeResumed = false;
+
+ constructor(websocketUrl: string, mockPromise?: Promise<Mock>) {
+ this.websocketUrl = websocketUrl;
+ pthread_self.addEventListenerFromBrowser(this.onMessageFromMainThread.bind(this));
+ this.mocked = monoDiagnosticsMock ? mockPromise : undefined;
+ }
+
+ private startRequestedController = createPromiseController<void>().promise_control;
+ private stopRequested = false;
+ private stopRequestedController = createPromiseController<void>().promise_control;
+
+ private attachToRuntimeController = createPromiseController<void>().promise_control;
+
+ start(): void {
+ console.log(`MONO_WASM: starting diagnostic server with url: ${this.websocketUrl}`);
+ this.startRequestedController.resolve();
+ }
+ stop(): void {
+ this.stopRequested = true;
+ this.stopRequestedController.resolve();
+ }
+
+ attachToRuntime(): void {
+ cwraps.mono_wasm_diagnostic_server_thread_attach_to_runtime();
+ this.attachToRuntimeController.resolve();
+ }
+
+ async serverLoop(this: DiagnosticServerImpl): Promise<void> {
+ await this.startRequestedController.promise;
+ await this.attachToRuntimeController.promise; // can't start tracing until we've attached to the runtime
+ while (!this.stopRequested) {
+ 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]);
+ switch (result) {
+ case "first":
+ break;
+ case "second":
+ console.debug("MONO_WASM: stop requested");
+ break;
+ default:
+ assertNever(result);
+ }
+ }
+ }
+
+ async openSocket(): Promise<CommonSocket> {
+ if (monoDiagnosticsMock && this.mocked) {
+ 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.
+ await addOneShotOpenEventListenr(sock);
+ return sock;
+ }
+ }
+
+ private openCount = 0;
+
+ async advertiseAndWaitForClient(): Promise<void> {
+ try {
+ const connNum = this.openCount++;
+ console.debug("MONO_WASM: opening websocket and sending ADVR_V1", connNum);
+ const ws = await this.openSocket();
+ const p = addOneShotProtocolCommandEventListener(createProtocolSocket(ws));
+ this.sendAdvertise(ws);
+ const message = await p;
+ 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, connNum: number, message: ProtocolCommandEvent): Promise<void> {
+ try {
+ const cmd = this.parseCommand(message, connNum);
+ if (cmd === null) {
+ console.error("MONO_WASM: unexpected message from client", message, connNum);
+ return;
+ } else if (isEventPipeCommand(cmd)) {
+ await this.dispatchEventPipeCommand(ws, cmd);
+ } else if (isProcessCommand(cmd)) {
+ await this.dispatchProcessCommand(ws, cmd); // resume
+ } else {
+ console.warn("MONO_WASM Client sent unknown command", cmd);
+ }
+ } finally {
+ // if there were errors, resume the runtime anyway
+ this.resumeRuntime();
+ }
+ }
+
+ sendAdvertise(ws: CommonSocket) {
+ /* 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";
+ const processIdLo = 0;
+ const processIdHi = 1234;
+ const buf = createAdvertise(guid, [processIdLo, processIdHi]);
+ ws.send(buf);
+ }
+
+ 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.warn("MONO_WASM: failed to parse command: ", result.error, connNum);
+ return null;
+ }
+ }
+
+ onMessageFromMainThread(this: DiagnosticServerImpl, event: MessageEvent<unknown>): void {
+ const d = event.data;
+ if (d && isDiagnosticMessage(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;
+ case "attach_to_runtime":
+ this.attachToRuntime();
+ break;
+ default:
+ console.warn("MONO_WASM: Unknown control command: ", <any>cmd);
+ break;
+ }
+ }
+
+ // dispatch EventPipe commands received from the diagnostic client
+ async dispatchEventPipeCommand(ws: CommonSocket, cmd: EventPipeClientCommandBase): Promise<void> {
+ if (isEventPipeCommandCollectTracing2(cmd)) {
+ await this.collectTracingEventPipe(ws, cmd);
+ } else if (isEventPipeCommandStopTracing(cmd)) {
+ await this.stopEventPipe(ws, cmd.sessionID);
+ } else {
+ console.warn("MONO_WASM: unknown EventPipe command: ", cmd);
+ }
+ }
+
+ postClientReplyOK(ws: CommonSocket, payload?: Uint8Array): void {
+ // FIXME: send a binary response for non-mock sessions!
+ ws.send(createBinaryCommandOKReply(payload));
+ }
+
+ async stopEventPipe(ws: WebSocket | MockRemoteSocket, sessionID: EventPipeSessionIDImpl): Promise<void> {
+ 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.
+ this.postClientReplyOK(ws);
+ }
+
+ async collectTracingEventPipe(ws: WebSocket | MockRemoteSocket, cmd: EventPipeCommandCollectTracing2): Promise<void> {
+ 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("MONO_WASM: 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<void> {
+ if (isProcessCommandResumeRuntime(cmd)) {
+ this.processResumeRuntime(ws);
+ } else {
+ console.warn("MONO_WASM: unknown Process command", cmd);
+ }
+ }
+
+ processResumeRuntime(ws: WebSocket | MockRemoteSocket): void {
+ this.postClientReplyOK(ws);
+ this.resumeRuntime();
+ }
+
+ resumeRuntime(): void {
+ if (!this.runtimeResumed) {
+ console.info("MONO_WASM: resuming runtime startup");
+ cwraps.mono_wasm_diagnostic_server_post_resume_runtime();
+ this.runtimeResumed = true;
+ }
+ }
+}
+
+function parseProtocolCommand(data: ArrayBuffer | BinaryProtocolCommand): ParseClientCommandResult<ProtocolClientCommandBase> {
+ 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);
+ console.debug(`mono_wasm_diagnostic_server_on_server_thread_created, url ${websocketUrl}`);
+ let mock: PromiseAndController<Mock> | undefined = undefined;
+ if (monoDiagnosticsMock && websocketUrl.startsWith("mock:")) {
+ mock = createPromiseController<Mock>();
+ queueMicrotask(async () => {
+ const m = await importAndInstantiateMock(websocketUrl);
+ mock!.promise_control.resolve(m);
+ m.run();
+ });
+ }
+ const server = new DiagnosticServerImpl(websocketUrl, mock?.promise);
+ queueMicrotask(() => {
+ server.serverLoop();
+ });
+}
--- /dev/null
+// 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<number>(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;
--- /dev/null
+// 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, EventPipeCommandId, ProcessCommandId } from "./types";
+import Magic from "./magic";
+
+function advancePos(pos: { pos: number }, count: number): void {
+ pos.pos += count;
+}
+
+
+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?: number | Uint8Array): number {
+ const fullHeaderSize = Magic.MinimalHeaderSize // magic, len
+ + 2 // commandSet, command
+ + 2; // 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 {
+ 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;
+ },
+ 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;
--- /dev/null
+// 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;
--- /dev/null
+// 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<C = ProtocolClientCommandBase> extends ParseResultOk {
+ readonly result: C;
+}
+
+export type ParseClientCommandResult<C = ProcessClientCommandBase> = ParseClientCommandResultOk<C> | ParseResultFail;
+
+export function parseBinaryProtocolCommand(cmd: BinaryProtocolCommand): ParseClientCommandResult<ProtocolClientCommandBase> {
+ 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<EventPipeClientCommandBase> {
+ 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("MONO_WASM: 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<EventPipeCommandCollectTracing2> {
+ 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<EventPipeCollectTracingCommandProvider>(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<EventPipeCollectTracingCommandProvider> {
+ 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<EventPipeCommandStopTracing> {
+ 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<ProcessClientCommandBase> {
+ 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("MMONO_WASM: 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<ProcessCommandResumeRuntime> {
+ 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 };
+}
--- /dev/null
+// 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";
+import { mono_assert } 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;
+}
+
+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;
+}
--- /dev/null
+// 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,
+}
--- /dev/null
+// 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";
+import type { Mock } from "../mock";
+import { mock } from "../mock";
+
+export function importAndInstantiateMock(mockURL: string): Promise<Mock> {
+ if (monoDiagnosticsMock) {
+ const mockPrefix = "mock:";
+ const scriptURL = mockURL.substring(mockPrefix.length);
+ // revisit this if we ever have a need to mock using CJS, for now we just support ESM
+ return import(scriptURL).then((mockModule) => {
+ const script = mockModule.default;
+ return mock(script, { trace: true });
+ });
+ } else {
+ return Promise.resolve(undefined as unknown as Mock);
+ }
+}
+
--- /dev/null
+// 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, number]; // lo,hi. FIXME: this is ugly
+ logLevel: number;
+ provider_name: string;
+ filter_data: string;
+}
+
+export type RemoveCommandSetAndId<T extends ProtocolClientCommandBase> = Omit<T, "command_set" | "command">;
+
+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((<any>x).providers) && (<any>x).providers.every(isEventPipeCollectTracingCommandProvider);
+}
+
+export function isEventPipeCommandStopTracing(x: object): x is EventPipeCommandStopTracing {
+ return isEventPipeCommand(x) && x.command === "StopTracing" && "sessionID" in x;
+}
--- /dev/null
+// 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";
+import {
+ BinaryProtocolCommand,
+ ParseResultFail,
+ ParseResultOk,
+} 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;
+
+export interface ProtocolCommandEvent extends Event {
+ type: typeof dotnetDiagnosticsServerProtocolCommandEvent;
+ data: BinaryProtocolCommand;
+}
+
+export interface ProtocolSocketEventMap extends WebSocketEventMap {
+ [dotnetDiagnosticsServerProtocolCommandEvent]: ProtocolCommandEvent;
+}
+
+/// An adapter that takes a websocket connection and converts MessageEvent into ProtocolCommandEvent by
+/// parsing the command.
+interface ProtocolSocket {
+ addEventListener<K extends keyof ProtocolSocketEventMap>(type: K, listener: (this: ProtocolSocket, ev: ProtocolSocketEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
+ addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
+ removeEventListener<K extends keyof ProtocolSocketEventMap>(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 {
+ 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 */
+}
+
+
+export interface ParseResultBinaryCommandOk extends ParseResultOk {
+ readonly command: BinaryProtocolCommand | undefined;
+ readonly newState: State;
+}
+
+export type ParseResult = ParseResultBinaryCommandOk | ParseResultFail;
+
+/// A helper object that accumulates command data that is received and provides parsed commands
+class StatefulParser {
+ private state: State = { state: InState.Idle };
+
+ constructor(private readonly emitCommandCallback: (command: BinaryProtocolCommand) => void) { }
+
+ /// process the data in the given buffer and update the state.
+ receiveBuffer(buf: ArrayBuffer): void {
+ 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) {
+ 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("MONO_WASM: 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 < Magic.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 === 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
+ // field filled in.
+ const parsedSize = pos.pos;
+ const partialBuf = new ArrayBuffer(size);
+ const partialBufView = new Uint8Array(partialBuf);
+ 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 {
+ 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: Magic.MinimalHeaderSize };
+ let result = this.tryParseCompletedBuffer(buf, pos);
+ if (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;
+ }
+ }
+ 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 } };
+ }
+
+ 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<ArrayBuffer | Blob | string>): void {
+ const data = ev.data;
+ console.debug("MONO_WASM: protocol socket received message", ev.data);
+ 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);
+ }
+ }
+
+ dispatchEvent(evt: Event): boolean {
+ return this.sock.dispatchEvent(evt);
+ }
+
+ onArrayBuffer(this: ProtocolSocketImpl, buf: ArrayBuffer) {
+ 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("MONO_WASM: protocol-socket: queueing command", command);
+ queueMicrotask(() => {
+ console.debug("MONO_WASM: dispatching protocol event with command", command);
+ this.dispatchProtocolCommandEvent(command);
+ });
+ }
+
+
+ dispatchProtocolCommandEvent(cmd: BinaryProtocolCommand): void {
+ const ev = new Event(dotnetDiagnosticsServerProtocolCommandEvent);
+ (<any>ev).data = cmd; // FIXME: use a proper event subclass
+ this.sock.dispatchEvent(ev);
+ }
+
+ addEventListener<K extends keyof ProtocolSocketEventMap>(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) {
+ console.debug("MONO_WASM: adding protocol listener, with a message chaser");
+ this.sock.addEventListener("message", this.messageListener);
+ }
+ this.protocolListeners++;
+ }
+ }
+
+ removeEventListener<K extends keyof ProtocolSocketEventMap>(type: K, listener: (this: ProtocolSocket, ev: ProtocolSocketEventMap[K]) => any): void;
+ removeEventListener(type: string, listener: EventListenerOrEventListenerObject): void {
+ if (type === dotnetDiagnosticsServerProtocolCommandEvent) {
+ console.debug("MONO_WASM: removing protocol listener and message chaser");
+ this.protocolListeners--;
+ if (this.protocolListeners === 0) {
+ this.sock.removeEventListener("message", this.messageListener);
+ this.statefulParser.reset();
+ }
+ }
+ this.sock.removeEventListener(type, listener);
+ }
+
+ send(buf: Uint8Array) {
+ this.sock.send(buf);
+ }
+
+ close() {
+ this.sock.close();
+ this.statefulParser.reset();
+ }
+
+}
+
+export function createProtocolSocket(socket: CommonSocket): ProtocolSocket {
+ return new ProtocolSocketImpl(socket);
+}
+
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+import { assertNever } from "../../types";
+import { VoidPtr } from "../../types/emscripten";
+import { Module } from "../../imports";
+import type { CommonSocket } from "./common-socket";
+enum ListenerState {
+ Sending,
+ Closed,
+ Error
+}
+
+class SocketGuts {
+ constructor(public readonly socket: CommonSocket) { }
+ close(): void {
+ 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.socket.send(buf);
+ }
+}
+
+
+/// 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(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;
+ case ListenerState.Closed:
+ return;
+ default:
+ this._state = ListenerState.Closed;
+ this.stream.close();
+ return;
+ }
+ }
+
+ write(ptr: VoidPtr, len: number): boolean {
+ switch (this._state) {
+ case ListenerState.Sending:
+ this.stream.write(ptr, len);
+ return true;
+ case ListenerState.Closed:
+ // ignore
+ return false;
+ case ListenerState.Error:
+ return false;
+ }
+ }
+
+ private _onMessage(event: MessageEvent): void {
+ switch (this._state) {
+ case ListenerState.Sending:
+ /* unexpected message */
+ 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;
+ case ListenerState.Closed:
+ /* ignore */
+ break;
+ case ListenerState.Error:
+ /* ignore */
+ break;
+ default:
+ assertNever(this._state);
+ }
+
+ }
+
+ private _onClose(/*event: CloseEvent*/) {
+ switch (this._state) {
+ case ListenerState.Closed:
+ return; /* do nothing */
+ case ListenerState.Error:
+ return; /* do nothing */
+ default:
+ this._state = ListenerState.Closed;
+ this.stream.close();
+ // TODO: notify runtime that connection is closed
+ return;
+ }
+ }
+
+ 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 {
+ const socket = this.stream.socket;
+ socket.addEventListener("message", this._onMessage.bind(this));
+ addEventListener("close", this._onClose.bind(this));
+ addEventListener("error", this._onError.bind(this));
+ }
+}
+
+/// 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();
+ return connection;
+}
--- /dev/null
+// 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 {
+// union { void* buf; intptr_t close_msg; /* -1 */ };
+// int32_t count;
+// volatile int32_t buf_full;
+// }
+//
+// 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;
+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);
+
+ constructor(readonly queue_addr: VoidPtr, readonly syncSendBuffer: SyncSendBuffer, readonly syncSendClose: SyncSendClose) {
+ this.workAvailable.addEventListener("workAvailable", this.onWorkAvailable.bind(this));
+ }
+
+ private get buf_addr(): VoidPtr {
+ return <any>this.queue_addr + BUF_OFFSET;
+ }
+ private get count_addr(): VoidPtr {
+ return <any>this.queue_addr + COUNT_OFFSET;
+ }
+ private get buf_full_addr(): VoidPtr {
+ return <any>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);
+ }
+
+ 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.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 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);
+ /* wake up the writer thread */
+ Memory.Atomics.notifyI32(this.buf_full_addr, 1);
+ }
+}
+
+// maps stream queue addresses to StreamQueue instances
+const streamQueueMap = new Map<VoidPtr, StreamQueue>();
+
+export function allocateQueue(nativeQueueAddr: VoidPtr, syncSendBuffer: SyncSendBuffer, syncSendClose: SyncSendClose): StreamQueue {
+ const queue = new StreamQueue(nativeQueueAddr, syncSendBuffer, syncSendClose);
+ 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_diagnostic_server_stream_signal_work_available(nativeQueueAddr: VoidPtr, current_thread: number): void {
+ const queue = streamQueueMap.get(nativeQueueAddr);
+ if (queue) {
+ if (current_thread === 0) {
+ queue.wakeup();
+ } else {
+ queue.workAvailableNow();
+ }
+ }
+}
--- /dev/null
+// 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";
+
+/// 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 queue: StreamQueue, readonly connection: EventPipeSocketConnection) { }
+}
+
+export async function makeEventPipeStreamingSession(ws: WebSocket | MockRemoteSocket, cmd: EventPipeCommandCollectTracing2): Promise<EventPipeStreamingSession> {
+ // 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 = getQueueAddrFromStreamAddr(ipcStreamAddr);
+ // then take over the websocket connection
+ const conn = takeOverSocket(ws);
+ // and set up queue notifications
+ const queue = allocateQueue(queueAddr, conn.write.bind(conn), conn.close.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, 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] === 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 getQueueAddrFromStreamAddr(streamAddr: VoidPtr): VoidPtr {
+ return <any>streamAddr + IPC_STREAM_QUEUE_OFFSET;
+}
--- /dev/null
+{
+ "extends": "../../tsconfig.worker.json",
+ "compilerOptions": {
+ "module": "commonjs",
+ },
+ "include": [
+ "../../**/*.ts",
+ "../../**/*.d.ts"
+ ]
+}
--- /dev/null
+// 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";
+
+// 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
+ | DiagnosticServerControlCommandAttachToRuntime
+ ;
+
+interface DiagnosticServerControlCommandSpecific<Cmd extends string> extends DiagnosticMessage {
+ cmd: Cmd;
+}
+
+export type DiagnosticServerControlCommandStart = DiagnosticServerControlCommandSpecific<"start">;
+export type DiagnosticServerControlCommandStop = DiagnosticServerControlCommandSpecific<"stop">;
+export type DiagnosticServerControlCommandAttachToRuntime = DiagnosticServerControlCommandSpecific<"attach_to_runtime">;
+
+export function makeDiagnosticServerControlCommand<T extends DiagnosticServerControlCommand["cmd"]>(cmd: T): DiagnosticServerControlCommandSpecific<T> {
+ return {
+ type: "diagnostic_server",
+ cmd: cmd,
+ };
+}
--- /dev/null
+// 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.getU32(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 });
+}
--- /dev/null
+{
+ "extends": "../../tsconfig.shared.json"
+}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+export type EventPipeSessionIDImpl = number;
+
runtime_options?: string[];
aot_profiler_options?: AOTProfilerOptions;
coverage_profiler_options?: CoverageProfilerOptions;
+ diagnostic_options?: DiagnosticOptions;
ignore_pdb_load_errors?: boolean;
wait_for_debugger?: number;
};
write_at?: string;
send_to?: string;
};
+declare type DiagnosticOptions = {
+ sessions?: EventPipeSessionOptions[];
+ server?: DiagnosticServerOptions;
+};
interface EventPipeSessionOptions {
collectRundownEvents?: boolean;
providers: string;
}
+declare type DiagnosticServerOptions = {
+ connect_url: string;
+ suspend: string | boolean;
+};
declare type DotnetModuleConfig = {
disableDotnet6Compatibility?: boolean;
config?: MonoConfig | MonoConfigError;
};
url?: any;
};
-
declare type EventPipeSessionID = bigint;
-interface EventPipeSession {
- get sessionID(): EventPipeSessionID;
- start(): void;
- stop(): void;
- getTraceBlob(): Blob;
-}
+
declare const eventLevel: {
readonly LogAlways: 0;
readonly Critical: 1;
addSampleProfilerProvider(overrideOptions?: UnnamedProviderConfiguration): SessionOptionsBuilder;
build(): EventPipeSessionOptions;
}
+
+interface EventPipeSession {
+ get sessionID(): EventPipeSessionID;
+ start(): void;
+ stop(): void;
+ getTraceBlob(): Blob;
+}
+
interface Diagnostics {
EventLevel: EventLevel;
SessionOptionsBuilder: typeof SessionOptionsBuilder;
createEventPipeSession(options?: EventPipeSessionOptions): EventPipeSession | null;
+ getStartupSessions(): (EventPipeSession | null)[];
}
declare function mono_wasm_runtime_ready(): void;
*destination = (int64_t)value;
return I52_ERROR_NONE;
-}
\ No newline at end of file
+}
// 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",
"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",
+ "mono_wasm_diagnostic_server_on_runtime_server_init",
+ "mono_wasm_diagnostic_server_stream_signal_work_available",
#endif
];
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,
+ mono_wasm_diagnostic_server_on_runtime_server_init
+} from "./diagnostics";
+import {
+ mono_wasm_diagnostic_server_on_server_thread_created,
+} from "./diagnostics/server_pthread";
+import {
+ mono_wasm_diagnostic_server_stream_signal_work_available
+} from "./diagnostics/server_pthread/stream-queue";
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";
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,
+ mono_wasm_diagnostic_server_on_runtime_server_init,
+ mono_wasm_diagnostic_server_stream_signal_work_available,
};
// the methods would be visible to EMCC linker
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,
+import monoWasmThreads from "consts:monoWasmThreads";
import { Module, runtimeHelpers } from "./imports";
import { mono_assert } from "./types";
import { VoidPtr, NativePointer, ManagedPointer } from "./types/emscripten";
}
}
+const BuiltinAtomics = globalThis.Atomics;
+
+export const Atomics = monoWasmThreads ? {
+ storeI32(offset: _MemOffset, value: number): void {
+
+ BuiltinAtomics.store(Module.HEAP32, <any>offset >>> 2, value);
+ },
+ notifyI32(offset: _MemOffset, count: number): void {
+ BuiltinAtomics.notify(Module.HEAP32, <any>offset >>> 2, count);
+ }
+} : {
+ storeI32: setI32,
+ notifyI32: () => { /*empty*/ }
+};
}
if (MonoWasmThreads && typeof globalThis.EventTarget === "undefined") {
globalThis.EventTarget = class EventTarget {
- private listeners = new Map<string, Array<EventListenerOrEventListenerObject>>();
+ private subscribers = new Map<string, Array<{ listener: EventListenerOrEventListenerObject, oneShot: boolean }>>();
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}'`);
+ }
+ }
+ if (!this.subscribers.has(type)) {
+ this.subscribers.set(type, []);
}
- const listeners = this.listeners.get(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)
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) {
+ let subscribers = this.subscribers.get(event.type);
+ if (subscribers === undefined) {
return true;
}
- for (const listener of listeners) {
+ let needsCopy = false;
+ for (const sub of subscribers) {
+ if (sub.oneShot) {
+ needsCopy = true;
+ break;
+ }
+ }
+ if (needsCopy) {
+ subscribers = subscribers.slice(0);
+ }
+ 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 {
--- /dev/null
+// 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<void> {
+ return new Promise(resolve => setTimeout(resolve, ms));
+}
+
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<pthread_ptr, Thread> = new Map();
readonly pthread_ptr: pthread_ptr;
readonly worker: Worker;
readonly port: MessagePort;
+ postMessageToWorker<T extends MonoThreadMessage>(message: T): void;
+}
+
+class ThreadImpl implements Thread {
+ constructor(readonly pthread_ptr: pthread_ptr, readonly worker: Worker, readonly port: MessagePort) { }
+ postMessageToWorker<T extends MonoThreadMessage>(message: T): void {
+ this.port.postMessage(message);
+ }
+}
+
+const thread_promises: Map<pthread_ptr, PromiseController<Thread>[]> = 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<Thread> {
+ if (threads.has(pthread_ptr)) {
+ return Promise.resolve(threads.get(pthread_ptr)!);
+ }
+ const promiseAndController = createPromiseController<Thread>();
+ 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((controller) => controller.resolve(thread));
+ thread_promises.delete(pthread_ptr);
+ }
}
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;
}
function monoDedicatedChannelMessageFromWorkerToMain(event: MessageEvent<unknown>, 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
/// N.B. important to ignore messages we don't recognize - Emscripten uses the message event to send internal messages
const data = ev.data;
if (isMonoWorkerMessageChannelCreated(data)) {
- console.debug("received the channel created message", data, worker);
+ console.debug("MONO_WASM: received the channel created message", data, worker);
const port = data[monoSymbol].port;
const pthread_id = data[monoSymbol].thread_id;
const thread = addThread(pthread_id, worker, port);
port.addEventListener("message", (ev) => monoDedicatedChannelMessageFromWorkerToMain(ev, thread));
port.start();
+ resolvePromises(pthread_id, thread);
}
}
/// At this point the worker doesn't have any pthread assigned to it, yet.
export function afterLoadWasmModuleToWorker(worker: Worker): void {
worker.addEventListener("message", (ev) => 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
// 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 isBrowserThread: boolean;
+}
+
+export const MainThread: PThreadInfo = {
+ get pthread_id(): pthread_ptr {
+ return getBrowserThreadID();
+ },
+ isBrowserThread: true
+};
+
+let browser_thread_id_lazy: pthread_ptr | undefined;
+export function getBrowserThreadID(): pthread_ptr {
+ if (browser_thread_id_lazy === undefined) {
+ browser_thread_id_lazy = (<any>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");
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: <T extends MonoThreadMessage>(message: T, transfer?: Transferable[]) => void;
+ addEventListenerFromBrowser: (listener: <T extends MonoThreadMessage>(event: MessageEvent<T>) => void) => void;
+}
export const dotnetPthreadCreated = "dotnet:pthread:created" as const;
export const dotnetPthreadAttached = "dotnet:pthread:attached" as const;
}
export interface WorkerThreadEvent extends Event {
- readonly pthread_ptr: pthread_ptr;
- readonly portToMain: MessagePort;
+ readonly pthread_self: PThreadSelf;
}
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);
});
+
import { Module, ENVIRONMENT_IS_PTHREAD } from "../../imports";
import { makeChannelCreatedMonoMessage, pthread_ptr } from "../shared";
import { mono_assert, is_nullish } from "../../types";
+import type { MonoThreadMessage } from "../shared";
import {
+ PThreadSelf,
makeWorkerThreadEvent,
dotnetPthreadCreated,
dotnetPthreadAttached,
WorkerThreadEventTarget,
} from "./events";
+class WorkerSelf implements PThreadSelf {
+ readonly isBrowserThread = false;
+ constructor(readonly pthread_id: pthread_ptr, readonly portToBrowser: MessagePort) { }
+ postMessageToBrowser(message: MonoThreadMessage, transfer?: Transferable[]) {
+ if (transfer) {
+ this.portToBrowser.postMessage(message, transfer);
+ } else {
+ this.portToBrowser.postMessage(message);
+ }
+ }
+ addEventListenerFromBrowser(listener: (event: MessageEvent<MonoThreadMessage>) => void) {
+ this.portToBrowser.addEventListener("message", listener);
+ }
+}
+
+// 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.
/// 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<string>): 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);
}
-let portToMain: MessagePort | null = null;
-
-function setupChannelToMainThread(pthread_ptr: pthread_ptr): MessagePort {
- console.debug("creating a channel", pthread_ptr);
+function setupChannelToMainThread(pthread_ptr: pthread_ptr): PThreadSelf {
+ console.debug("MONO_WASM: 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");
- console.debug("attaching pthread to runtime", pthread_id);
- currentWorkerThreadEvents.dispatchEvent(makeWorkerThreadEvent(dotnetPthreadAttached, pthread_id, port));
+ const self = pthread_self;
+ mono_assert(self !== null && self.pthread_id == pthread_id, "expected pthread_self to be set already when attaching");
+ console.debug("MONO_WASM: attaching pthread to runtime", pthread_id);
+ currentWorkerThreadEvents.dispatchEvent(makeWorkerThreadEvent(dotnetPthreadAttached, self));
}
/// This is an implementation detail function.
if (ENVIRONMENT_IS_PTHREAD) {
const pthread_ptr = (<any>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));
+ console.debug("MONO_WASM: after thread init, pthread ptr", pthread_ptr);
+ const self = setupChannelToMainThread(pthread_ptr);
+ currentWorkerThreadEvents.dispatchEvent(makeWorkerThreadEvent(dotnetPthreadCreated, self));
}
}
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
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"
+];
const iffeConfig = {
treeshake: !isDebug,
plugins,
}
],
+ external: externalDependencies,
plugins: outputCodePlugins
};
const typesConfig = {
plugins: [writeOnChangePlugin()],
}
],
+ external: externalDependencies,
plugins: [dts()],
};
+
+let diagnosticMockTypesConfig = undefined;
+
if (isDebug) {
// export types also into the source code and commit to git
// so that we could notice that the API changed and review it
banner: banner_dts,
plugins: [alwaysLF(), writeOnChangePlugin()],
});
+
+ // export types into the source code and commit to git
+ diagnosticMockTypesConfig = {
+ input: "./diagnostics/mock/export-types.ts",
+ output: [
+ {
+ format: "es",
+ file: "./diagnostics-mock.d.ts",
+ banner: banner_dts,
+ plugins: [alwaysLF(), writeOnChangePlugin()],
+ }
+ ],
+ external: externalDependencies,
+ plugins: [dts()],
+ };
}
/* Web Workers */
plugins
},
],
+ external: externalDependencies,
plugins: outputCodePlugins,
};
return workerConfig;
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.
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";
Module.removeRunDependency("mono_wasm_pre_init");
}
-function mono_wasm_after_runtime_initialized(): void {
+async function mono_wasm_after_runtime_initialized(): Promise<void> {
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");
}
}
}
-function _apply_configuration_from_args(config: MonoConfig) {
+async function _apply_configuration_from_args(config: MonoConfig): Promise<void> {
const envars = (config.environment_variables || {});
if (typeof (envars) !== "object")
throw new Error("Expected config.environment_variables to be unset or a dictionary-style object");
if (config.coverage_profiler_options)
mono_wasm_init_coverage_profiler(config.coverage_profiler_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<void> {
const globalThisAny = globalThis as any;
try {
}
try {
- _apply_configuration_from_args(config);
-
+ await _apply_configuration_from_args(config);
mono_wasm_globalization_init(config.globalization_mode!, config.diagnostic_tracing!);
cwraps.mono_wasm_load_runtime("unused", config.debug_level || 0);
runtimeHelpers.wait_for_debugger = config.wait_for_debugger;
const wasm_exit = cwraps.mono_wasm_exit;
wasm_exit(1);
}
+ return;
}
bindings_lazy_init();
/// 3. At the point when this executes there is no pthread assigned to the worker yet.
async function mono_wasm_pthread_worker_init(): Promise<void> {
// This is a good place for subsystems to attach listeners for pthreads_worker.currentWorkerThreadEvents
- console.debug("mono_wasm_pthread_worker_init");
pthreads_worker.currentWorkerThreadEvents.addEventListener(pthreads_worker.dotnetPthreadCreated, (ev) => {
- console.debug("thread created", ev.pthread_ptr);
+ console.debug("MONO_WASM: pthread created", ev.pthread_self.pthread_id);
});
}
},
"exclude": [
"dotnet.d.ts",
+ "diagnostics-mock.d.ts",
"bin"
]
}
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
};
send_to?: string // should be in the format <CLASS>::<METHODNAME>, default: 'WebAssembly.Runtime::DumpCoverageProfileData' (DumpCoverageProfileData stores the data into INTERNAL.coverage_profile_data.)
}
+/// Options to configure 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,
+}
+
/// 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 {
providers: string;
}
+/// Options to configure the diagnostic server
+export type DiagnosticServerOptions = {
+ connect_url: string, // websocket URL to connect to.
+ suspend: string | 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;
export function is_nullish<T>(value: T | null | undefined): value is null | undefined {
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.
+export function notThenable<T>(x: T | PromiseLike<T>): x is T {
+ return typeof x !== "object" || typeof ((<PromiseLike<T>>x).then) !== "function";
+}
+
+/// 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;
+
const constant: boolean;
export default constant;
}
+
+/* if true, include mock impplementations of diagnostics sockets */
+declare module "consts:monoDiagnosticsMock" {
+ const constant: boolean;
+ export default constant;
+}
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
{
- "extends": "../tsconfig",
- "compilerOptions": {
- "lib": [
- "esnext",
- "webworker"
- ],
- },
- "exclude": [
- "../dotnet.d.ts",
- "../bin"
- ]
+ "extends": "../tsconfig.worker.json"
}
<PropertyGroup>
<MonoWasmThreads Condition="'$(WasmEnableThreads)' == 'true' or '$(WasmEnablePerfTracing)' == 'true' or '$(MonoWasmBuildVariant)' == 'multithread' or '$(MonoWasmBuildVariant)' == 'perftrace'">true</MonoWasmThreads>
<MonoWasmThreadsNoUser Condition="('$(WasmEnableThreads)' != 'true' and '$(WasmEnablePerfTracing)' == 'true') or '$(MonoWasmBuildVariant)' == 'perftrace'">true</MonoWasmThreadsNoUser>
+ <MonoDiagnosticsMock Condition="'$(MonoDiagnosticsMock)' == '' and '$(Configuration)' == 'Release'">false</MonoDiagnosticsMock>
+ <MonoDiagnosticsMock Condition="'$(MonoDiagnosticsMock)' == '' and '$(Configuration)' == 'Debug'">true</MonoDiagnosticsMock>
</PropertyGroup>
<PropertyGroup>
<_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" /> <!-- hard error if worker pool is exhausted -->
<!--
<_EmccLinkFlags Include="-s EXPORT_ES6=1" Condition="'$(WasmEnableES6)' == 'true'" />
-->
<ItemGroup>
<_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.*" />
+ Exclude="$(MonoProjectRoot)wasm/runtime/node_modules/**/tsconfig.*" />
<_RollupInputs Include="$(MonoProjectRoot)wasm/runtime/workers/**/*.js"/>
<_RollupInputs Include="$(MonoProjectRoot)wasm/runtimetypes/*.d.ts"/>
<_RollupInputs Include="$(MonoProjectRoot)wasm/runtime/*.json"/>
<Target Name="SetMonoRollupEnvironment">
<PropertyGroup>
- <MonoRollupEnvironment>Configuration:$(Configuration),NativeBinDir:$(NativeBinDir),ProductVersion:$(ProductVersion),MonoWasmThreads:$(MonoWasmThreads)</MonoRollupEnvironment>
+ <MonoRollupEnvironment>Configuration:$(Configuration),NativeBinDir:$(NativeBinDir),ProductVersion:$(ProductVersion),MonoWasmThreads:$(MonoWasmThreads),MonoDiagnosticsMock:$(MonoDiagnosticsMock)</MonoRollupEnvironment>
</PropertyGroup>
<PropertyGroup>