Skip to content

Commit

Permalink
[browser][MT] Calling JS functions from workers + config loading (#81273
Browse files Browse the repository at this point in the history
)

* calling JS functions from workers
* remove workaround
* config loading earlier
  • Loading branch information
pavelsavara authored Jan 31, 2023
1 parent 37b12da commit e831dab
Show file tree
Hide file tree
Showing 7 changed files with 117 additions and 66 deletions.
2 changes: 1 addition & 1 deletion src/mono/wasm/runtime/dotnet.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
5 changes: 0 additions & 5 deletions src/mono/wasm/runtime/es6/dotnet.es6.lib.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
11 changes: 7 additions & 4 deletions src/mono/wasm/runtime/pthreads/browser/index.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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);
}
}
Expand Down
58 changes: 42 additions & 16 deletions src/mono/wasm/runtime/pthreads/shared/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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",
Expand All @@ -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;
}
}
Expand Down
27 changes: 18 additions & 9 deletions src/mono/wasm/runtime/pthreads/worker/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand All @@ -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;
Expand All @@ -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);
}
}
Expand Down
78 changes: 48 additions & 30 deletions src/mono/wasm/runtime/startup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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();
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -564,29 +578,30 @@ export async function mono_wasm_load_config(configFilePath?: string): Promise<vo
}
configLoaded = true;
if (!configFilePath) {
normalize();
normalizeConfig();
afterConfigLoaded.promise_control.resolve(runtimeHelpers.config);
return;
}
if (runtimeHelpers.diagnosticTracing) console.debug("MONO_WASM: mono_wasm_load_config");
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);
Expand All @@ -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)
Expand Down Expand Up @@ -665,15 +683,15 @@ 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);
});

// this is the only event which is called on worker
module.preInit = [() => preInitWorkerAsync()];
module.instantiateWasm = instantiateWasmWorker;

await afterPreInit.promise;
return exportedAPI.Module;
Expand Down
2 changes: 1 addition & 1 deletion src/mono/wasm/runtime/types/emscripten.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

0 comments on commit e831dab

Please sign in to comment.