Skip to content

Commit

Permalink
[wasm-ep] Implement DiagnosticServer and startup sessions for WebAsse…
Browse files Browse the repository at this point in the history
…mbly (#72482)

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 #69674, contributes to #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
  • Loading branch information
lambdageek authored Jul 25, 2022
1 parent 9a461ad commit 7aaa279
Show file tree
Hide file tree
Showing 65 changed files with 3,416 additions and 373 deletions.
1 change: 1 addition & 0 deletions src/mono/mono.proj
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,7 @@
<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 -->
Expand Down
303 changes: 303 additions & 0 deletions src/mono/mono/component/diagnostics_server.c
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,19 @@
#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,
Expand All @@ -19,6 +28,300 @@ static MonoComponentDiagnosticsServer fn_table = {
&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)
{
Expand Down
6 changes: 1 addition & 5 deletions src/mono/mono/component/event_pipe-stub.c
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@
#include "mono/component/event_pipe.h"
#include "mono/component/event_pipe-wasm.h"
#include "mono/metadata/components.h"
#ifdef HOST_WASM
#include <emscripten/emscripten.h>
#endif

static EventPipeSessionID _dummy_session_id;

Expand Down Expand Up @@ -524,12 +521,12 @@ mono_component_event_pipe_init (void)

EMSCRIPTEN_KEEPALIVE gboolean
mono_wasm_event_pipe_enable (const ep_char8_t *output_path,
IpcStream *ipc_stream,
uint32_t circular_buffer_size_in_mb,
const ep_char8_t *providers,
/* EventPipeSessionType session_type = EP_SESSION_TYPE_FILE, */
/* EventPipieSerializationFormat format = EP_SERIALIZATION_FORMAT_NETTRACE_V4, */
/* bool */ gboolean rundown_requested,
/* IpcStream stream = NULL, */
/* EventPipeSessionSycnhronousCallback sync_callback = NULL, */
/* void *callback_additional_data, */
MonoWasmEventPipeSessionID *out_session_id)
Expand All @@ -551,5 +548,4 @@ mono_wasm_event_pipe_session_disable (MonoWasmEventPipeSessionID session_id)
{
g_assert_not_reached ();
}

#endif /* HOST_WASM */
15 changes: 14 additions & 1 deletion src/mono/mono/component/event_pipe-wasm.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

#ifdef HOST_WASM

#include <pthread.h>
#include <emscripten.h>

G_BEGIN_DECLS
Expand All @@ -27,12 +28,12 @@ typedef uint32_t MonoWasmEventPipeSessionID;

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);
Expand All @@ -43,6 +44,18 @@ mono_wasm_event_pipe_session_start_streaming (MonoWasmEventPipeSessionID session
EMSCRIPTEN_KEEPALIVE gboolean
mono_wasm_event_pipe_session_disable (MonoWasmEventPipeSessionID session_id);

EMSCRIPTEN_KEEPALIVE gboolean
mono_wasm_diagnostic_server_create_thread (const char *websocket_url, pthread_t *out_thread_id);

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 */
Expand Down
Loading

0 comments on commit 7aaa279

Please sign in to comment.