[wasm] making JSImport work on worker thread
authorPavel Savara <pavel.savara@gmail.com>
Wed, 30 Nov 2022 19:06:59 +0000 (20:06 +0100)
committerGitHub <noreply@github.com>
Wed, 30 Nov 2022 19:06:59 +0000 (20:06 +0100)
- sample: added JSImport which would be called from thread (#78847)
- fixed missing await on wasm instantiation
- added preInitWorkerAsync - called from Module.preInit which is atually called
- added preRunWorker - called after the thread is mono attached
- remove forced lazy from cwraps (when called from preInit, the memory+wasm+Module is attached already)

src/mono/.gitignore
src/mono/sample/wasm/browser-threads/Program.cs
src/mono/sample/wasm/browser-threads/Wasm.Browser.Threads.Sample.csproj
src/mono/wasm/runtime/assets.ts
src/mono/wasm/runtime/cwraps.ts
src/mono/wasm/runtime/exports.ts
src/mono/wasm/runtime/profiler.ts
src/mono/wasm/runtime/pthreads/worker/index.ts
src/mono/wasm/runtime/startup.ts

index d00c48d7e23f26bbcfb0e94c1c39bb745bae26dd..4b98459105e52ae68ab332a444b726fe6fffd6a6 100644 (file)
@@ -1 +1,2 @@
-mono_crash*
\ No newline at end of file
+mono_crash*
+wasi-sdk
\ No newline at end of file
index d1c9e655394f23b5dc7c215d17d904f6e2ffba4c..8fb461a854676e798e4a81060aea5d9f3c3d1726 100644 (file)
@@ -18,6 +18,9 @@ namespace Sample
             return 0;
         }
 
+        [JSImport("globalThis.console.log")]
+        public static partial void ConsoleLog(string status);
+
         [JSImport("Sample.Test.updateProgress", "main.js")]
         static partial void updateProgress(string status);
 
@@ -72,6 +75,7 @@ public class ExpensiveComputation
 
     public void Run()
     {
+        Sample.Test.ConsoleLog("Hello from ManagedThreadId " + Thread.CurrentThread.ManagedThreadId);
         long result = Fib(UpTo);
         if (result < (long)int.MaxValue)
             _tcs.SetResult((int)result);
index e4cc5d0473542559551028672da270cfe00278c6..5c11649b173205307105384f0b3359ffb40cc328 100644 (file)
@@ -2,12 +2,9 @@
   <Import Project="..\DefaultBrowserSample.targets" />
   <PropertyGroup>
     <_SampleProject>Wasm.Browser.Threads.Sample.csproj</_SampleProject>
+    <WasmEnableThreads>true</WasmEnableThreads>
   </PropertyGroup>
 
-  <Target Name="CheckThreadsEnabled" BeforeTargets="Compile" >
-    <Warning Condition="'$(WasmEnableThreads)' != 'true'" Text="This sample requires threading" />
-  </Target>
-
   <!-- set the condition to false and you will get a CA1416 error about the call to Thread.Start from a browser-wasm project -->
   <ItemGroup Condition="true">
     <!-- TODO: some .props file that automates this.  Unfortunately just adding a ProjectReference to Microsoft.NET.WebAssembly.Threading.proj doesn't work - it ends up bundling the ref assemblies into the publish directory and breaking the app. -->
index 6deb4c3652786da753ee33cffc00117bfc62bc2f..2f6ab851f1bb125a5c471a06104ca1e90ebd04b1 100644 (file)
@@ -432,7 +432,7 @@ export async function instantiate_wasm_asset(
     pendingAsset: AssetEntryInternal,
     wasmModuleImports: WebAssembly.Imports,
     successCallback: InstantiateWasmSuccessCallback,
-) {
+): Promise<void> {
     mono_assert(pendingAsset && pendingAsset.pendingDownloadInternal, "Can't load dotnet.wasm");
     const response = await pendingAsset.pendingDownloadInternal.response;
     const contentType = response.headers ? response.headers.get("Content-Type") : undefined;
index e7cf08aeb3b7858bb48acd6242d35ccf9c2c2266..f1279cd9bf8702df6c30aa56e29a9d1a81356e83 100644 (file)
@@ -6,7 +6,7 @@ import {
     MonoMethod, MonoObject, MonoString,
     MonoType, MonoObjectRef, MonoStringRef, JSMarshalerArguments
 } from "./types";
-import { ENVIRONMENT_IS_PTHREAD, Module } from "./imports";
+import { Module } from "./imports";
 import { VoidPtr, CharPtrPtr, Int32Ptr, CharPtr, ManagedPointer } from "./types/emscripten";
 
 type SigLine = [lazy: boolean, name: string, returnType: string | null, argTypes?: string[], opts?: any];
@@ -235,19 +235,19 @@ export interface t_Cwraps {
     mono_jiterp_get_offset_of_vtable_initialized_flag(): number;
     mono_jiterp_get_offset_of_array_data(): number;
     // Returns bytes written (or 0 if writing failed)
-    mono_jiterp_encode_leb52 (destination: VoidPtr, value: number, valueIsSigned: number): number;
+    mono_jiterp_encode_leb52(destination: VoidPtr, value: number, valueIsSigned: number): number;
     // Returns bytes written (or 0 if writing failed)
     // Source is the address of a 64-bit int or uint
-    mono_jiterp_encode_leb64_ref (destination: VoidPtr, source: VoidPtr, valueIsSigned: number): number;
-    mono_jiterp_type_is_byref (type: MonoType): number;
-    mono_jiterp_get_size_of_stackval (): number;
-    mono_jiterp_type_get_raw_value_size (type: MonoType): number;
-    mono_jiterp_parse_option (name: string): number;
-    mono_jiterp_get_options_as_json (): number;
-    mono_jiterp_get_options_version (): number;
-    mono_jiterp_adjust_abort_count (opcode: number, delta: number): number;
-    mono_jiterp_register_jit_call_thunk (cinfo: number, func: number): void;
-    mono_jiterp_update_jit_call_dispatcher (fn: number): void;
+    mono_jiterp_encode_leb64_ref(destination: VoidPtr, source: VoidPtr, valueIsSigned: number): number;
+    mono_jiterp_type_is_byref(type: MonoType): number;
+    mono_jiterp_get_size_of_stackval(): number;
+    mono_jiterp_type_get_raw_value_size(type: MonoType): number;
+    mono_jiterp_parse_option(name: string): number;
+    mono_jiterp_get_options_as_json(): number;
+    mono_jiterp_get_options_version(): number;
+    mono_jiterp_adjust_abort_count(opcode: number, delta: number): number;
+    mono_jiterp_register_jit_call_thunk(cinfo: number, func: number): void;
+    mono_jiterp_update_jit_call_dispatcher(fn: number): void;
 }
 
 const wrapped_c_functions: t_Cwraps = <any>{};
@@ -262,12 +262,10 @@ export const enum I52Error {
 }
 
 export function init_c_exports(): void {
-    // init_c_exports is called very early in a pthread before Module.cwrap is available
-    const alwaysLazy = !!ENVIRONMENT_IS_PTHREAD;
     for (const sig of fn_signatures) {
         const wf: any = wrapped_c_functions;
         const [lazy, name, returnType, argTypes, opts] = sig;
-        if (lazy || alwaysLazy) {
+        if (lazy) {
             // lazy init on first run
             wf[name] = function (...args: any[]) {
                 const fce = Module.cwrap(name, returnType, argTypes, opts);
index bd6f9bcedcf9e14218713b67a747542e681fd9d9..1950eb7ae692b756f0786ec51d0a7754fab72e4d 100644 (file)
@@ -133,19 +133,7 @@ function initializeImportsAndExports(
     list.registerRuntime(exportedRuntimeAPI);
 
     if (MonoWasmThreads && ENVIRONMENT_IS_PTHREAD) {
-        // eslint-disable-next-line no-inner-declarations
-        async function workerInit(): Promise<DotnetModule> {
-            await mono_wasm_pthread_worker_init();
-
-            // HACK: Emscripten's dotnet.worker.js expects the exports of dotnet.js module to be Module object
-            // until we have our own fix for dotnet.worker.js file
-            // we also skip all emscripten startup event and configuration of worker's JS state
-            // note that emscripten events are not firing either
-
-            return exportedRuntimeAPI.Module;
-        }
-        // Emscripten pthread worker.js is ok with a Promise here.
-        return <any>workerInit();
+        return <any>mono_wasm_pthread_worker_init(module, exportedRuntimeAPI);
     }
 
     configure_emscripten_startup(module, exportedRuntimeAPI);
index c46cd952a911f8abf895734f3e39e88848a87cb8..189c69429158b0bbcdbfbcc24ecabda6aff5e6f3 100644 (file)
@@ -36,7 +36,9 @@ export const enum MeasuredBlock {
     emscriptenStartup = "mono.emscriptenStartup",
     instantiateWasm = "mono.instantiateWasm",
     preInit = "mono.preInit",
+    preInitWorker = "mono.preInitWorker",
     preRun = "mono.preRun",
+    preRunWorker = "mono.preRunWorker",
     onRuntimeInitialized = "mono.onRuntimeInitialized",
     postRun = "mono.postRun",
     loadRuntime = "mono.loadRuntime",
index 69900f8da7fb876e3026170b7e2986d90640ad07..1322a11d2d675699ab7b11b67859e2ba01c57c96 100644 (file)
@@ -4,7 +4,7 @@
 /// <reference lib="webworker" />
 
 import MonoWasmThreads from "consts:monoWasmThreads";
-import { Module, ENVIRONMENT_IS_PTHREAD } from "../../imports";
+import { Module, ENVIRONMENT_IS_PTHREAD, runtimeHelpers } from "../../imports";
 import { isMonoThreadMessageApplyMonoConfig, makeChannelCreatedMonoMessage } from "../shared";
 import type { pthread_ptr } from "../shared/types";
 import { mono_assert, is_nullish, MonoConfig } from "../../types";
@@ -17,6 +17,7 @@ import {
     WorkerThreadEventTarget
 } from "./events";
 import { setup_proxy_console } from "../../logging";
+import { afterConfigLoaded, preRunWorker } from "../../startup";
 
 // re-export some of the events types
 export {
@@ -80,18 +81,22 @@ function setupChannelToMainThread(pthread_ptr: pthread_ptr): PThreadSelf {
     return pthread_self;
 }
 
-// TODO: should we just assign to Module.config here?
-let workerMonoConfig: MonoConfig = null as unknown as MonoConfig;
+let workerMonoConfigReceived = false;
 
 // called when the main thread sends us the mono config
 function onMonoConfigReceived(config: MonoConfig): void {
-    if (workerMonoConfig !== null) {
+    if (workerMonoConfigReceived) {
         console.debug("MONO_WASM: mono config already received");
         return;
     }
-    workerMonoConfig = config;
-    console.debug("MONO_WASM: mono config received", config);
-    if (workerMonoConfig.diagnosticTracing) {
+
+    console.debug("MONO_WASM: mono config received");
+    config = runtimeHelpers.config = Module.config = Object.assign(Module.config || {} as any, config);
+    workerMonoConfigReceived = true;
+
+    afterConfigLoaded.promise_control.resolve(config);
+
+    if (config.diagnosticTracing) {
         setup_proxy_console("pthread-worker", console, self.location.href);
     }
 }
@@ -102,6 +107,7 @@ export function mono_wasm_pthread_on_pthread_attached(pthread_id: pthread_ptr):
     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);
+    preRunWorker();
     currentWorkerThreadEvents.dispatchEvent(makeWorkerThreadEvent(dotnetPthreadAttached, self));
 }
 
index cd022129ef369a13fc70342ff8a5e3e92d3283f3..00eea0f796dfe7650b996af99956b2ebc768eebc 100644 (file)
@@ -34,7 +34,7 @@ let config: MonoConfigInternal = undefined as any;
 let configLoaded = false;
 let isCustomStartup = false;
 export const dotnetReady = createPromiseController<any>();
-export const afterConfigLoaded = createPromiseController<void>();
+export const afterConfigLoaded = createPromiseController<MonoConfig>();
 export const afterInstantiateWasm = createPromiseController<void>();
 export const beforePreInit = createPromiseController<void>();
 export const afterPreInit = createPromiseController<void>();
@@ -159,6 +159,35 @@ function preInit(userPreInit: (() => void)[]) {
     })();
 }
 
+async function preInitWorkerAsync() {
+    const mark = startMeasure();
+    try {
+        if (runtimeHelpers.diagnosticTracing) console.debug("MONO_WASM: preInitWorker");
+        beforePreInit.promise_control.resolve();
+        mono_wasm_pre_init_essential();
+        await init_polyfills_async();
+        afterPreInit.promise_control.resolve();
+        endMeasure(mark, MeasuredBlock.preInitWorker);
+    } catch (err) {
+        _print_error("MONO_WASM: user preInitWorker() failed", err);
+        abort_startup(err, true);
+        throw err;
+    }
+}
+
+export function preRunWorker() {
+    const mark = startMeasure();
+    try {
+        bindings_init();
+        endMeasure(mark, MeasuredBlock.preRunWorker);
+    } catch (err) {
+        abort_startup(err, true);
+        throw err;
+    }
+    // signal next stage
+    afterPreRun.promise_control.resolve();
+}
+
 async function preRunAsync(userPreRun: (() => void)[]) {
     Module.addRunDependency("mono_pre_run_async");
     // wait for previous stages
@@ -419,7 +448,7 @@ async function instantiate_wasm_module(
         await start_asset_download_with_retries(assetToLoad, false);
         await beforePreInit.promise;
         Module.addRunDependency("instantiate_wasm_module");
-        instantiate_wasm_asset(assetToLoad, imports, successCallback);
+        await instantiate_wasm_asset(assetToLoad, imports, successCallback);
 
         if (runtimeHelpers.diagnosticTracing) console.debug("MONO_WASM: instantiate_wasm_module done");
         afterInstantiateWasm.promise_control.resolve();
@@ -529,7 +558,7 @@ export async function mono_wasm_load_config(configFilePath?: string): Promise<vo
     configLoaded = true;
     if (!configFilePath) {
         normalize();
-        afterConfigLoaded.promise_control.resolve();
+        afterConfigLoaded.promise_control.resolve(runtimeHelpers.config);
         return;
     }
     if (runtimeHelpers.diagnosticTracing) console.debug("MONO_WASM: mono_wasm_load_config");
@@ -557,7 +586,7 @@ export async function mono_wasm_load_config(configFilePath?: string): Promise<vo
                 throw err;
             }
         }
-        afterConfigLoaded.promise_control.resolve();
+        afterConfigLoaded.promise_control.resolve(runtimeHelpers.config);
     } catch (err) {
         const errMessage = `Failed to load config file ${configFilePath} ${err}`;
         abort_startup(errMessage, true);
@@ -622,22 +651,25 @@ export function mono_wasm_set_main_args(name: string, allRuntimeArguments: strin
 }
 
 /// Called when dotnet.worker.js receives an emscripten "load" event from the main thread.
+/// This method is comparable to configure_emscripten_startup function
 ///
 /// Notes:
 /// 1. Emscripten skips a lot of initialization on the pthread workers, Module may not have everything you expect.
-/// 2. Emscripten does not run the preInit or preRun functions in the workers.
+/// 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(): Promise<void> {
+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");
-    // FIXME: copy/pasted from mono_wasm_pre_init_essential - can we share this code? Any other global state that needs initialization?
-    init_c_exports();
-    // not initializing INTERNAL, MONO, or BINDING C wrappers here - those legacy APIs are not likely to be needed on pthread workers.
 
     // 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);
     });
 
+    // this is the only event which is called on worker
+    module.preInit = [() => preInitWorkerAsync()];
+
+    await afterPreInit.promise;
+    return exportedAPI.Module;
 }
 
 /**