[browser][MT] Calling JS functions from workers + config loading (#81273)
authorPavel Savara <pavel.savara@gmail.com>
Tue, 31 Jan 2023 18:07:33 +0000 (19:07 +0100)
committerGitHub <noreply@github.com>
Tue, 31 Jan 2023 18:07:33 +0000 (19:07 +0100)
* calling JS functions from workers
* remove workaround
* config loading earlier

src/mono/wasm/runtime/dotnet.d.ts
src/mono/wasm/runtime/es6/dotnet.es6.lib.js
src/mono/wasm/runtime/pthreads/browser/index.ts
src/mono/wasm/runtime/pthreads/shared/index.ts
src/mono/wasm/runtime/pthreads/worker/index.ts
src/mono/wasm/runtime/startup.ts
src/mono/wasm/runtime/types/emscripten.ts

index c510643..80fab37 100644 (file)
@@ -75,7 +75,7 @@ declare interface EmscriptenModule {
         (error: any): void;
     };
 }
-type InstantiateWasmSuccessCallback = (instance: WebAssembly.Instance, module: WebAssembly.Module) => void;
+type InstantiateWasmSuccessCallback = (instance: WebAssembly.Instance, module: WebAssembly.Module | undefined) => void;
 type InstantiateWasmCallBack = (imports: WebAssembly.Imports, successCallback: InstantiateWasmSuccessCallback) => any;
 declare type TypedArray = Int8Array | Uint8Array | Uint8ClampedArray | Int16Array | Uint16Array | Int32Array | Uint32Array | Float32Array | Float64Array;
 
index 19f877f..8eafb1d 100644 (file)
@@ -121,12 +121,7 @@ const linked_functions = [
 // -- this javascript file is evaluated by emcc during compilation! --
 // we generate simple proxy for each exported function so that emcc will include them in the final output
 for (let linked_function of linked_functions) {
-    #if USE_PTHREADS
-    const fn_template = `return __dotnet_runtime.__linker_exports.${linked_function}.apply(__dotnet_runtime, arguments)`;
-    DotnetSupportLib[linked_function] = new Function(fn_template);
-    #else
     DotnetSupportLib[linked_function] = new Function('throw new Error("unreachable");');
-    #endif
 }
 
 autoAddDeps(DotnetSupportLib, "$DOTNET");
index b7c62e9..7816adf 100644 (file)
@@ -1,7 +1,7 @@
 // Licensed to the .NET Foundation under one or more agreements.
 // The .NET Foundation licenses this file to you under the MIT license.
 
-import { MonoWorkerMessageChannelCreated, isMonoWorkerMessageChannelCreated, monoSymbol, makeMonoThreadMessageApplyMonoConfig } from "../shared";
+import { isMonoWorkerMessageChannelCreated, monoSymbol, makeMonoThreadMessageApplyMonoConfig, isMonoWorkerMessagePreload, MonoWorkerMessage } from "../shared";
 import { pthread_ptr } from "../shared/types";
 import { MonoThreadMessage } from "../shared";
 import { PromiseController, createPromiseController } from "../../promise-controller";
@@ -85,17 +85,20 @@ function monoDedicatedChannelMessageFromWorkerToMain(event: MessageEvent<unknown
 }
 
 // handler that runs in the main thread when a message is received from a pthread worker
-function monoWorkerMessageHandler(worker: Worker, ev: MessageEvent<MonoWorkerMessageChannelCreated<MessagePort>>): void {
+function monoWorkerMessageHandler(worker: Worker, ev: MessageEvent<MonoWorkerMessage<MessagePort>>): void {
     /// 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)) {
+    if (isMonoWorkerMessagePreload(data)) {
+        const port = data[monoSymbol].port;
+        port.postMessage(makeMonoThreadMessageApplyMonoConfig(runtimeHelpers.config));
+    }
+    else if (isMonoWorkerMessageChannelCreated(data)) {
         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();
-        port.postMessage(makeMonoThreadMessageApplyMonoConfig(runtimeHelpers.config));
         resolvePromises(pthread_id, thread);
     }
 }
index b03207a..c71a27f 100644 (file)
@@ -25,6 +25,11 @@ export function getBrowserThreadID(): pthread_ptr {
     return browser_thread_id_lazy;
 }
 
+const enum WorkerMonoCommandType {
+    channel_created = "channel_created",
+    preload = "preload",
+}
+
 /// 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.
@@ -50,14 +55,6 @@ export interface MonoThreadMessageApplyMonoConfig extends MonoThreadMessage {
     config: string;
 }
 
-export function isMonoThreadMessageApplyMonoConfig(x: unknown): x is MonoThreadMessageApplyMonoConfig {
-    if (!isMonoThreadMessage(x)) {
-        return false;
-    }
-    const xmsg = x as MonoThreadMessageApplyMonoConfig;
-    return xmsg.type === "pthread" && xmsg.cmd === "apply_mono_config" && typeof (xmsg.config) === "string";
-}
-
 export function makeMonoThreadMessageApplyMonoConfig(config: MonoConfig): MonoThreadMessageApplyMonoConfig {
     return {
         type: "pthread",
@@ -75,37 +72,66 @@ export const monoSymbol = "__mono_message_please_dont_collide__"; //Symbol("mono
 /// Messages sent from the main thread using Worker.postMessage or from the worker using DedicatedWorkerGlobalScope.postMessage
 /// should use this interface.  The message event is also used by emscripten internals (and possibly by 3rd party libraries targeting Emscripten).
 /// We should just use this to establish a dedicated MessagePort for Mono's uses.
-export interface MonoWorkerMessage {
-    [monoSymbol]: object;
+export interface MonoWorkerMessage<TPort> {
+    [monoSymbol]: {
+        mono_cmd: WorkerMonoCommandType;
+        port: TPort;
+    };
 }
 
 /// The message sent early during pthread creation to set up a dedicated MessagePort for Mono between the main thread and the pthread.
-export interface MonoWorkerMessageChannelCreated<TPort> extends MonoWorkerMessage {
+export interface MonoWorkerMessageChannelCreated<TPort> extends MonoWorkerMessage<TPort> {
     [monoSymbol]: {
-        mono_cmd: "channel_created";
+        mono_cmd: WorkerMonoCommandType.channel_created;
         thread_id: pthread_ptr;
         port: TPort;
     };
 }
 
+export interface MonoWorkerMessagePreload<TPort> extends MonoWorkerMessage<TPort> {
+    [monoSymbol]: {
+        mono_cmd: WorkerMonoCommandType.preload;
+        port: TPort;
+    };
+}
+
 export function makeChannelCreatedMonoMessage<TPort>(thread_id: pthread_ptr, port: TPort): MonoWorkerMessageChannelCreated<TPort> {
     return {
         [monoSymbol]: {
-            mono_cmd: "channel_created",
+            mono_cmd: WorkerMonoCommandType.channel_created,
             thread_id,
             port
         }
     };
 }
 
-export function isMonoWorkerMessage(message: unknown): message is MonoWorkerMessage {
+export function makePreloadMonoMessage<TPort>(port: TPort): MonoWorkerMessagePreload<TPort> {
+    return {
+        [monoSymbol]: {
+            mono_cmd: WorkerMonoCommandType.preload,
+            port
+        }
+    };
+}
+
+export function isMonoWorkerMessage(message: unknown): message is MonoWorkerMessage<any> {
     return message !== undefined && typeof message === "object" && message !== null && monoSymbol in message;
 }
 
-export function isMonoWorkerMessageChannelCreated<TPort>(message: MonoWorkerMessageChannelCreated<TPort>): message is MonoWorkerMessageChannelCreated<TPort> {
+export function isMonoWorkerMessageChannelCreated<TPort>(message: MonoWorkerMessage<TPort>): message is MonoWorkerMessageChannelCreated<TPort> {
+    if (isMonoWorkerMessage(message)) {
+        const monoMessage = message[monoSymbol];
+        if (monoMessage.mono_cmd === WorkerMonoCommandType.channel_created) {
+            return true;
+        }
+    }
+    return false;
+}
+
+export function isMonoWorkerMessagePreload<TPort>(message: MonoWorkerMessage<TPort>): message is MonoWorkerMessagePreload<TPort> {
     if (isMonoWorkerMessage(message)) {
         const monoMessage = message[monoSymbol];
-        if (monoMessage.mono_cmd === "channel_created") {
+        if (monoMessage.mono_cmd === WorkerMonoCommandType.preload) {
             return true;
         }
     }
index 1322a11..ba411a7 100644 (file)
@@ -4,10 +4,10 @@
 /// <reference lib="webworker" />
 
 import MonoWasmThreads from "consts:monoWasmThreads";
-import { Module, ENVIRONMENT_IS_PTHREAD, runtimeHelpers } from "../../imports";
-import { isMonoThreadMessageApplyMonoConfig, makeChannelCreatedMonoMessage } from "../shared";
+import { Module, ENVIRONMENT_IS_PTHREAD, runtimeHelpers, ENVIRONMENT_IS_WEB } from "../../imports";
+import { makeChannelCreatedMonoMessage, makePreloadMonoMessage } from "../shared";
 import type { pthread_ptr } from "../shared/types";
-import { mono_assert, is_nullish, MonoConfig } from "../../types";
+import { mono_assert, is_nullish, MonoConfig, MonoConfigInternal } from "../../types";
 import type { MonoThreadMessage } from "../shared";
 import {
     PThreadSelf,
@@ -60,13 +60,22 @@ export const currentWorkerThreadEvents: WorkerThreadEventTarget =
 // this is the message handler for the worker that receives messages from the main thread
 // extend this with new cases as needed
 function monoDedicatedChannelMessageFromMainToWorker(event: MessageEvent<string>): void {
-    if (isMonoThreadMessageApplyMonoConfig(event.data)) {
+    console.debug("MONO_WASM: got message from main on the dedicated channel", event.data);
+}
+
+export function setupPreloadChannelToMainThread() {
+    const channel = new MessageChannel();
+    const workerPort = channel.port1;
+    const mainPort = channel.port2;
+    workerPort.addEventListener("message", (event) => {
         const config = JSON.parse(event.data.config) as MonoConfig;
         console.debug("MONO_WASM: applying mono config from main", event.data.config);
         onMonoConfigReceived(config);
-        return;
-    }
-    console.debug("MONO_WASM: got message from main on the dedicated channel", event.data);
+        workerPort.close();
+        mainPort.close();
+    }, { once: true });
+    workerPort.start();
+    self.postMessage(makePreloadMonoMessage(mainPort), [mainPort]);
 }
 
 function setupChannelToMainThread(pthread_ptr: pthread_ptr): PThreadSelf {
@@ -84,7 +93,7 @@ function setupChannelToMainThread(pthread_ptr: pthread_ptr): PThreadSelf {
 let workerMonoConfigReceived = false;
 
 // called when the main thread sends us the mono config
-function onMonoConfigReceived(config: MonoConfig): void {
+function onMonoConfigReceived(config: MonoConfigInternal): void {
     if (workerMonoConfigReceived) {
         console.debug("MONO_WASM: mono config already received");
         return;
@@ -96,7 +105,7 @@ function onMonoConfigReceived(config: MonoConfig): void {
 
     afterConfigLoaded.promise_control.resolve(config);
 
-    if (config.diagnosticTracing) {
+    if (ENVIRONMENT_IS_WEB && config.forwardConsoleLogsToWS && typeof globalThis.WebSocket != "undefined") {
         setup_proxy_console("pthread-worker", console, self.location.href);
     }
 }
index 71a51bc..25bf207 100644 (file)
@@ -52,7 +52,7 @@ export function configure_emscripten_startup(module: DotnetModule, exportedAPI:
     const mark = startMeasure();
     // these all could be overridden on DotnetModuleConfig, we are chaing them to async below, as opposed to emscripten
     // when user set configSrc or config, we are running our default startup sequence.
-    const userInstantiateWasm: undefined | ((imports: WebAssembly.Imports, successCallback: (instance: WebAssembly.Instance, module: WebAssembly.Module) => void) => any) = module.instantiateWasm;
+    const userInstantiateWasm: undefined | ((imports: WebAssembly.Imports, successCallback: InstantiateWasmSuccessCallback) => any) = module.instantiateWasm;
     const userPreInit: (() => void)[] = !module.preInit ? [] : typeof module.preInit === "function" ? [module.preInit] : module.preInit;
     const userPreRun: (() => void)[] = !module.preRun ? [] : typeof module.preRun === "function" ? [module.preRun] : module.preRun as any;
     const userpostRun: (() => void)[] = !module.postRun ? [] : typeof module.postRun === "function" ? [module.postRun] : module.postRun as any;
@@ -102,16 +102,11 @@ function instantiateWasm(
     if (!Module.configSrc && !Module.config && !userInstantiateWasm) {
         Module.print("MONO_WASM: configSrc nor config was specified");
     }
-    if (Module.config) {
-        config = runtimeHelpers.config = Module.config as MonoConfig;
-    } else {
-        config = runtimeHelpers.config = Module.config = {} as any;
-    }
-    runtimeHelpers.diagnosticTracing = !!config.diagnosticTracing;
+    normalizeConfig();
 
     const mark = startMeasure();
     if (userInstantiateWasm) {
-        const exports = userInstantiateWasm(imports, (instance: WebAssembly.Instance, module: WebAssembly.Module) => {
+        const exports = userInstantiateWasm(imports, (instance: WebAssembly.Instance, module: WebAssembly.Module | undefined) => {
             endMeasure(mark, MeasuredBlock.instantiateWasm);
             afterInstantiateWasm.promise_control.resolve();
             successCallback(instance, module);
@@ -123,6 +118,24 @@ function instantiateWasm(
     return []; // No exports
 }
 
+async function instantiateWasmWorker(
+    imports: WebAssembly.Imports,
+    successCallback: InstantiateWasmSuccessCallback
+): Promise<void> {
+    // wait for the config to arrive by message from the main thread
+    await afterConfigLoaded.promise;
+
+    const anyModule = Module as any;
+    normalizeConfig();
+    replace_linker_placeholders(imports, export_linker());
+
+    // Instantiate from the module posted from the main thread.
+    // We can just use sync instantiation in the worker.
+    const instance = new WebAssembly.Instance(anyModule.wasmModule, imports);
+    successCallback(instance, undefined);
+    anyModule.wasmModule = null;
+}
+
 function preInit(userPreInit: (() => void)[]) {
     Module.addRunDependency("mono_pre_init");
     const mark = startMeasure();
@@ -160,6 +173,7 @@ function preInit(userPreInit: (() => void)[]) {
 }
 
 async function preInitWorkerAsync() {
+    console.debug("MONO_WASM: worker initializing essential C exports and APIs");
     const mark = startMeasure();
     try {
         if (runtimeHelpers.diagnosticTracing) console.debug("MONO_WASM: preInitWorker");
@@ -564,7 +578,7 @@ export async function mono_wasm_load_config(configFilePath?: string): Promise<vo
     }
     configLoaded = true;
     if (!configFilePath) {
-        normalize();
+        normalizeConfig();
         afterConfigLoaded.promise_control.resolve(runtimeHelpers.config);
         return;
     }
@@ -572,21 +586,22 @@ export async function mono_wasm_load_config(configFilePath?: string): Promise<vo
     try {
         const resolveSrc = runtimeHelpers.locateFile(configFilePath);
         const configResponse = await runtimeHelpers.fetch_like(resolveSrc);
-        const loadedConfig: MonoConfig = (await configResponse.json()) || {};
+        const loadedConfig: MonoConfigInternal = (await configResponse.json()) || {};
         if (loadedConfig.environmentVariables && typeof (loadedConfig.environmentVariables) !== "object")
             throw new Error("Expected config.environmentVariables to be unset or a dictionary-style object");
 
         // merge
         loadedConfig.assets = [...(loadedConfig.assets || []), ...(config.assets || [])];
         loadedConfig.environmentVariables = { ...(loadedConfig.environmentVariables || {}), ...(config.environmentVariables || {}) };
+        loadedConfig.runtimeOptions = [...(loadedConfig.runtimeOptions || []), ...(config.runtimeOptions || [])];
         config = runtimeHelpers.config = Module.config = Object.assign(Module.config as any, loadedConfig);
 
-        normalize();
+        normalizeConfig();
 
         if (Module.onConfigLoaded) {
             try {
                 await Module.onConfigLoaded(<MonoConfig>runtimeHelpers.config);
-                normalize();
+                normalizeConfig();
             }
             catch (err: any) {
                 _print_error("MONO_WASM: onConfigLoaded() failed", err);
@@ -601,26 +616,29 @@ export async function mono_wasm_load_config(configFilePath?: string): Promise<vo
         throw err;
     }
 
-    function normalize() {
-        // normalize
-        config.environmentVariables = config.environmentVariables || {};
-        config.assets = config.assets || [];
-        config.runtimeOptions = config.runtimeOptions || [];
-        config.globalizationMode = config.globalizationMode || "auto";
-        if (config.debugLevel === undefined && BuildConfiguration === "Debug") {
-            config.debugLevel = -1;
-        }
-        if (config.diagnosticTracing === undefined && BuildConfiguration === "Debug") {
-            config.diagnosticTracing = true;
-        }
-        runtimeHelpers.diagnosticTracing = !!runtimeHelpers.config.diagnosticTracing;
+}
 
-        runtimeHelpers.enablePerfMeasure = !!config.browserProfilerOptions
-            && globalThis.performance
-            && typeof globalThis.performance.measure === "function";
+function normalizeConfig() {
+    // normalize
+    Module.config = config = runtimeHelpers.config = Object.assign(runtimeHelpers.config, Module.config || {});
+    config.environmentVariables = config.environmentVariables || {};
+    config.assets = config.assets || [];
+    config.runtimeOptions = config.runtimeOptions || [];
+    config.globalizationMode = config.globalizationMode || "auto";
+    if (config.debugLevel === undefined && BuildConfiguration === "Debug") {
+        config.debugLevel = -1;
     }
+    if (config.diagnosticTracing === undefined && BuildConfiguration === "Debug") {
+        config.diagnosticTracing = true;
+    }
+    runtimeHelpers.diagnosticTracing = !!runtimeHelpers.config.diagnosticTracing;
+
+    runtimeHelpers.enablePerfMeasure = !!config.browserProfilerOptions
+        && globalThis.performance
+        && typeof globalThis.performance.measure === "function";
 }
 
+
 export function mono_wasm_asm_loaded(assembly_name: CharPtr, assembly_ptr: number, assembly_len: number, pdb_ptr: number, pdb_len: number): void {
     // Only trigger this codepath for assemblies loaded after app is ready
     if (runtimeHelpers.mono_wasm_runtime_is_ready !== true)
@@ -665,8 +683,7 @@ export function mono_wasm_set_main_args(name: string, allRuntimeArguments: strin
 /// 2. Emscripten does not run any event but preInit in the workers.
 /// 3. At the point when this executes there is no pthread assigned to the worker yet.
 export async function mono_wasm_pthread_worker_init(module: DotnetModule, exportedAPI: RuntimeAPI): Promise<DotnetModule> {
-    console.debug("MONO_WASM: worker initializing essential C exports and APIs");
-
+    pthreads_worker.setupPreloadChannelToMainThread();
     // This is a good place for subsystems to attach listeners for pthreads_worker.currentWorkerThreadEvents
     pthreads_worker.currentWorkerThreadEvents.addEventListener(pthreads_worker.dotnetPthreadCreated, (ev) => {
         console.debug("MONO_WASM: pthread created", ev.pthread_self.pthread_id);
@@ -674,6 +691,7 @@ export async function mono_wasm_pthread_worker_init(module: DotnetModule, export
 
     // this is the only event which is called on worker
     module.preInit = [() => preInitWorkerAsync()];
+    module.instantiateWasm = instantiateWasmWorker;
 
     await afterPreInit.promise;
     return exportedAPI.Module;
index 5dfef89..3cc8933 100644 (file)
@@ -68,7 +68,7 @@ export declare interface EmscriptenModule {
     onAbort?: { (error: any): void };
 }
 
-export type InstantiateWasmSuccessCallback = (instance: WebAssembly.Instance, module: WebAssembly.Module) => void;
+export type InstantiateWasmSuccessCallback = (instance: WebAssembly.Instance, module: WebAssembly.Module | undefined) => void;
 export type InstantiateWasmCallBack = (imports: WebAssembly.Imports, successCallback: InstantiateWasmSuccessCallback) => any;
 
 export declare type TypedArray = Int8Array | Uint8Array | Uint8ClampedArray | Int16Array | Uint16Array | Int32Array | Uint32Array | Float32Array | Float64Array;