diff --git a/src/mono/browser/build/BrowserWasmApp.targets b/src/mono/browser/build/BrowserWasmApp.targets index ec2a81e1e066b..6174769ef02ea 100644 --- a/src/mono/browser/build/BrowserWasmApp.targets +++ b/src/mono/browser/build/BrowserWasmApp.targets @@ -121,7 +121,8 @@ - <_WasmPThreadPoolSize Condition="'$(_WasmPThreadPoolSize)' == ''">-1 + <_WasmPThreadPoolInitialSize Condition="'$(_WasmPThreadPoolInitialSize)' == ''">-1 + <_WasmPThreadPoolUnusedSize Condition="'$(_WasmPThreadPoolUnusedSize)' == ''">-1 @@ -148,7 +149,8 @@ NativeAssets="@(WasmNativeAsset)" DebugLevel="$(WasmDebugLevel)" IncludeThreadsWorker="$(WasmEnableThreads)" - PThreadPoolSize="$(_WasmPThreadPoolSize)" + PThreadPoolInitialSize="$(_WasmPThreadPoolInitialSize)" + PThreadPoolUnusedSize="$(_WasmPThreadPoolUnusedSize)" UseWebcil="$(WasmEnableWebcil)" WasmIncludeFullIcuData="$(WasmIncludeFullIcuData)" WasmIcuDataFileName="$(WasmIcuDataFileName)" diff --git a/src/mono/browser/runtime/assets.ts b/src/mono/browser/runtime/assets.ts index bd88949b6234c..40dc39349ff56 100644 --- a/src/mono/browser/runtime/assets.ts +++ b/src/mono/browser/runtime/assets.ts @@ -1,13 +1,14 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +import type { AssetEntryInternal } from "./types/internal"; + import cwraps from "./cwraps"; import { mono_wasm_load_icu_data } from "./icu"; import { Module, loaderHelpers, mono_assert, runtimeHelpers } from "./globals"; import { mono_log_info, mono_log_debug, parseSymbolMapFile } from "./logging"; import { mono_wasm_load_bytes_into_heap } from "./memory"; import { endMeasure, MeasuredBlock, startMeasure } from "./profiler"; -import { AssetEntryInternal } from "./types/internal"; import { AssetEntry } from "./types"; import { VoidPtr } from "./types/emscripten"; import { setSegmentationRulesFromJson } from "./hybrid-globalization/grapheme-segmenter"; diff --git a/src/mono/browser/runtime/cwraps.ts b/src/mono/browser/runtime/cwraps.ts index 7dc7c207697ec..dbb0babce8231 100644 --- a/src/mono/browser/runtime/cwraps.ts +++ b/src/mono/browser/runtime/cwraps.ts @@ -6,13 +6,12 @@ import WasmEnableThreads from "consts:wasmEnableThreads"; import type { MonoAssembly, MonoClass, MonoMethod, MonoObject, - MonoType, MonoObjectRef, MonoStringRef, JSMarshalerArguments + MonoType, MonoObjectRef, MonoStringRef, JSMarshalerArguments, PThreadPtr } from "./types/internal"; import type { VoidPtr, CharPtrPtr, Int32Ptr, CharPtr, ManagedPointer } from "./types/emscripten"; import { Module, runtimeHelpers } from "./globals"; import { mono_log_error } from "./logging"; import { mono_assert } from "./globals"; -import { PThreadPtr } from "./pthreads/shared/types"; type SigLine = [lazyOrSkip: boolean | (() => boolean), name: string, returnType: string | null, argTypes?: string[], opts?: any]; diff --git a/src/mono/browser/runtime/debug.ts b/src/mono/browser/runtime/debug.ts index 1c919426bc3a8..74c0128f2e4e3 100644 --- a/src/mono/browser/runtime/debug.ts +++ b/src/mono/browser/runtime/debug.ts @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. import BuildConfiguration from "consts:configuration"; + import { INTERNAL, Module, loaderHelpers, runtimeHelpers } from "./globals"; import { toBase64StringImpl } from "./base64"; import cwraps from "./cwraps"; diff --git a/src/mono/browser/runtime/diagnostics/browser/controller.ts b/src/mono/browser/runtime/diagnostics/browser/controller.ts index 7bcdf62b86850..799972b1fd0e6 100644 --- a/src/mono/browser/runtime/diagnostics/browser/controller.ts +++ b/src/mono/browser/runtime/diagnostics/browser/controller.ts @@ -7,10 +7,10 @@ import { threads_c_functions as cwraps } from "../../cwraps"; import { INTERNAL, mono_assert } from "../../globals"; import { mono_log_info, mono_log_debug, mono_log_warn } from "../../logging"; import { withStackAlloc, getI32 } from "../../memory"; -import { Thread, waitForThread } from "../../pthreads/browser"; +import { waitForThread } from "../../pthreads"; import { isDiagnosticMessage, makeDiagnosticServerControlCommand } from "../shared/controller-commands"; import monoDiagnosticsMock from "consts:monoDiagnosticsMock"; -import { PThreadPtr } from "../../pthreads/shared/types"; +import { PThreadPtr, Thread } from "../../types/internal"; /// An object that can be used to control the diagnostic server. export interface ServerController { diff --git a/src/mono/browser/runtime/diagnostics/index.ts b/src/mono/browser/runtime/diagnostics/index.ts index 474fd8a824fd5..a9d6f9414267a 100644 --- a/src/mono/browser/runtime/diagnostics/index.ts +++ b/src/mono/browser/runtime/diagnostics/index.ts @@ -37,12 +37,9 @@ let diagnosticsServerEnabled = false; let diagnosticsInitialized = false; export async function mono_wasm_init_diagnostics(): Promise { - if (diagnosticsInitialized) - return; - if (!WasmEnableThreads) { - mono_log_warn("ignoring diagnostics options because this runtime does not support diagnostics"); - return; - } + if (!WasmEnableThreads) return; + if (diagnosticsInitialized) return; + const options = diagnostic_options_from_environment(); if (!options) return; diff --git a/src/mono/browser/runtime/diagnostics/mock/environment.ts b/src/mono/browser/runtime/diagnostics/mock/environment.ts index bcbdf390a4dfe..0de8c8b7acafc 100644 --- a/src/mono/browser/runtime/diagnostics/mock/environment.ts +++ b/src/mono/browser/runtime/diagnostics/mock/environment.ts @@ -6,7 +6,7 @@ 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 } from "../../types/internal"; -import { pthread_self } from "../../pthreads/worker"; +import { pthread_self } from "../../pthreads"; import { createPromiseController, mono_assert } from "../../globals"; diff --git a/src/mono/browser/runtime/diagnostics/server_pthread/index.ts b/src/mono/browser/runtime/diagnostics/server_pthread/index.ts index 1fbca276f3c89..cba9d5fba7d8d 100644 --- a/src/mono/browser/runtime/diagnostics/server_pthread/index.ts +++ b/src/mono/browser/runtime/diagnostics/server_pthread/index.ts @@ -6,7 +6,7 @@ import WasmEnableThreads from "consts:wasmEnableThreads"; import monoDiagnosticsMock from "consts:monoDiagnosticsMock"; import { PromiseAndController, assertNever } from "../../types/internal"; -import { pthread_self } from "../../pthreads/worker"; +import { pthread_self } from "../../pthreads"; import { createPromiseController, mono_assert } from "../../globals"; import { threads_c_functions as cwraps } from "../../cwraps"; import { EventPipeSessionIDImpl } from "../shared/types"; diff --git a/src/mono/browser/runtime/diagnostics/shared/controller-commands.ts b/src/mono/browser/runtime/diagnostics/shared/controller-commands.ts index 5e08f56c627ea..16aa6ad85944f 100644 --- a/src/mono/browser/runtime/diagnostics/shared/controller-commands.ts +++ b/src/mono/browser/runtime/diagnostics/shared/controller-commands.ts @@ -1,8 +1,8 @@ // 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"; +import { isMonoThreadMessage } from "../../pthreads"; +import type { MonoThreadMessage } from "../../types/internal"; // Messages from the main thread to the diagnostic server thread export interface DiagnosticMessage extends MonoThreadMessage { diff --git a/src/mono/browser/runtime/dotnet.d.ts b/src/mono/browser/runtime/dotnet.d.ts index e60265f04a281..7b901972e8b8f 100644 --- a/src/mono/browser/runtime/dotnet.d.ts +++ b/src/mono/browser/runtime/dotnet.d.ts @@ -189,7 +189,15 @@ type MonoConfig = { /** * initial number of workers to add to the emscripten pthread pool */ - pthreadPoolSize?: number; + pthreadPoolInitialSize?: number; + /** + * number of unused workers kept in the emscripten pthread pool after startup + */ + pthreadPoolUnusedSize?: number; + /** + * Delay in milliseconds before starting the finalizer thread + */ + finalizerThreadStartDelayMs?: number; /** * If true, a list of the methods optimized by the interpreter will be saved and used for faster startup * on future runs of the application diff --git a/src/mono/browser/runtime/exports-binding.ts b/src/mono/browser/runtime/exports-binding.ts index c410dca9e7588..6ab2b430ab048 100644 --- a/src/mono/browser/runtime/exports-binding.ts +++ b/src/mono/browser/runtime/exports-binding.ts @@ -10,8 +10,7 @@ import { mono_interp_tier_prepare_jiterpreter, mono_jiterp_free_method_data_js } import { mono_interp_jit_wasm_entry_trampoline, mono_interp_record_interp_entry } from "./jiterpreter-interp-entry"; import { mono_interp_jit_wasm_jit_call_trampoline, mono_interp_invoke_wasm_jit_call_trampoline, mono_interp_flush_jitcall_queue } from "./jiterpreter-jit-call"; import { mono_wasm_resolve_or_reject_promise } from "./marshal-to-js"; -import { mono_wasm_eventloop_has_unsettled_interop_promises } from "./pthreads/shared/eventloop"; -import { mono_wasm_pthread_on_pthread_attached, mono_wasm_pthread_on_pthread_unregistered, mono_wasm_pthread_on_pthread_registered, mono_wasm_pthread_set_name } from "./pthreads/worker"; +import { mono_wasm_eventloop_has_unsettled_interop_promises } from "./pthreads"; import { mono_wasm_schedule_timer, schedule_background_exec } from "./scheduling"; import { mono_wasm_asm_loaded } from "./startup"; import { mono_wasm_diagnostic_server_on_server_thread_created } from "./diagnostics/server_pthread"; @@ -22,13 +21,15 @@ import { mono_wasm_profiler_leave, mono_wasm_profiler_enter } from "./profiler"; import { mono_wasm_change_case, mono_wasm_change_case_invariant } from "./hybrid-globalization/change-case"; import { mono_wasm_compare_string, mono_wasm_ends_with, mono_wasm_starts_with, mono_wasm_index_of } from "./hybrid-globalization/collations"; import { mono_wasm_get_calendar_info } from "./hybrid-globalization/calendar"; -import { mono_wasm_install_js_worker_interop, mono_wasm_uninstall_js_worker_interop } from "./pthreads/shared"; import { mono_wasm_get_culture_info } from "./hybrid-globalization/culture-info"; import { mono_wasm_get_first_day_of_week, mono_wasm_get_first_week_of_year } from "./hybrid-globalization/locales"; import { mono_wasm_browser_entropy } from "./crypto"; import { mono_wasm_cancel_promise } from "./cancelable-promise"; +import { mono_wasm_pthread_on_pthread_attached, mono_wasm_pthread_on_pthread_unregistered, mono_wasm_pthread_on_pthread_registered, mono_wasm_pthread_set_name } from "./pthreads"; +import { mono_wasm_install_js_worker_interop, mono_wasm_uninstall_js_worker_interop } from "./pthreads"; + // the JS methods would be visible to EMCC linker and become imports of the WASM module export const mono_wasm_threads_imports = !WasmEnableThreads ? [] : [ diff --git a/src/mono/browser/runtime/exports-internal.ts b/src/mono/browser/runtime/exports-internal.ts index 5fe5773f97577..80c52669fb3ad 100644 --- a/src/mono/browser/runtime/exports-internal.ts +++ b/src/mono/browser/runtime/exports-internal.ts @@ -1,6 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +import WasmEnableThreads from "consts:wasmEnableThreads"; + +import { MonoObjectNull, type MonoObject } from "./types/internal"; import cwraps, { profiler_c_functions } from "./cwraps"; import { mono_wasm_send_dbg_command_with_parms, mono_wasm_send_dbg_command, mono_wasm_get_dbg_command_info, mono_wasm_get_details, mono_wasm_release_object, mono_wasm_call_function_on, mono_wasm_debugger_resume, mono_wasm_detach_debugger, mono_wasm_raise_debug_event, mono_wasm_change_debugger_log_level, mono_wasm_debugger_attached } from "./debug"; import { http_wasm_supports_streaming_request, http_wasm_supports_streaming_response, http_wasm_create_controller, http_wasm_abort_request, http_wasm_abort_response, http_wasm_transform_stream_write, http_wasm_transform_stream_close, http_wasm_fetch, http_wasm_fetch_stream, http_wasm_fetch_bytes, http_wasm_get_response_header_names, http_wasm_get_response_header_values, http_wasm_get_response_bytes, http_wasm_get_response_length, http_wasm_get_streamed_response_bytes, http_wasm_get_response_type, http_wasm_get_response_status } from "./http"; @@ -17,16 +20,17 @@ import { loadLazyAssembly } from "./lazyLoading"; import { loadSatelliteAssemblies } from "./satelliteAssemblies"; import { forceDisposeProxies } from "./gc-handles"; import { mono_wasm_get_func_id_to_name_mappings } from "./logging"; -import { MonoObject, MonoObjectNull } from "./types/internal"; import { monoStringToStringUnsafe } from "./strings"; -import { thread_available } from "./pthreads/browser"; import { mono_wasm_bind_cs_function } from "./invoke-cs"; +import { dumpThreads, thread_available } from "./pthreads"; + export function export_internal(): any { return { // tests mono_wasm_exit: (exit_code: number) => { Module.err("early exit " + exit_code); }, forceDisposeProxies, + dumpThreads: WasmEnableThreads ? dumpThreads : undefined, // with mono_wasm_debugger_log and mono_wasm_trace_logger logging: undefined, @@ -57,7 +61,7 @@ export function export_internal(): any { get_global_this, get_dotnet_instance: () => exportedRuntimeAPI, dynamic_import, - thread_available, + thread_available: WasmEnableThreads ? thread_available : undefined, mono_wasm_bind_cs_function, // BrowserWebSocket diff --git a/src/mono/browser/runtime/exports.ts b/src/mono/browser/runtime/exports.ts index 2f3aa96a0ec6b..8b2984f393b1f 100644 --- a/src/mono/browser/runtime/exports.ts +++ b/src/mono/browser/runtime/exports.ts @@ -22,7 +22,7 @@ import { mono_wasm_stringify_as_error_with_stack } from "./logging"; import { instantiate_asset, instantiate_symbols_asset, instantiate_segmentation_rules_asset } from "./assets"; import { jiterpreter_dump_stats } from "./jiterpreter"; import { forceDisposeProxies } from "./gc-handles"; -import { dumpThreads } from "./pthreads/browser"; +import { dumpThreads } from "./pthreads"; export let runtimeList: RuntimeList; diff --git a/src/mono/browser/runtime/interp-pgo.ts b/src/mono/browser/runtime/interp-pgo.ts index dfe894f569826..79ea1e29ab5df 100644 --- a/src/mono/browser/runtime/interp-pgo.ts +++ b/src/mono/browser/runtime/interp-pgo.ts @@ -197,7 +197,9 @@ export async function getCacheKey(prefix: string): Promise { delete inputs.interopCleanupOnExit; delete inputs.dumpThreadsOnNonZeroExit; delete inputs.logExitCode; - delete inputs.pthreadPoolSize; + delete inputs.pthreadPoolInitialSize; + delete inputs.pthreadPoolUnusedSize; + delete inputs.finalizerThreadStartDelayMs; delete inputs.asyncFlushOnExit; delete inputs.remoteSources; delete inputs.ignorePdbLoadErrors; diff --git a/src/mono/browser/runtime/loader/assets.ts b/src/mono/browser/runtime/loader/assets.ts index 624244a63ff9c..49d2f0f8ac6d8 100644 --- a/src/mono/browser/runtime/loader/assets.ts +++ b/src/mono/browser/runtime/loader/assets.ts @@ -3,7 +3,7 @@ import WasmEnableThreads from "consts:wasmEnableThreads"; -import type { AssetEntryInternal, PromiseAndController } from "../types/internal"; +import { PThreadPtrNull, type AssetEntryInternal, type PThreadWorker, type PromiseAndController } from "../types/internal"; import type { AssetBehaviors, AssetEntry, LoadingResource, ResourceList, SingleAssetBehaviors as SingleAssetBehaviors, WebAssemblyBootResourceType } from "../types"; import { ENVIRONMENT_IS_NODE, ENVIRONMENT_IS_SHELL, ENVIRONMENT_IS_WEB, loaderHelpers, mono_assert, runtimeHelpers } from "./globals"; import { createPromiseController } from "./promise-controller"; @@ -20,6 +20,9 @@ let parallel_count = 0; const assetsToLoad: AssetEntryInternal[] = []; const singleAssets: Map = new Map(); +// A duplicate in pthreads/shared.ts +const worker_empty_prefix = " - "; + const jsRuntimeModulesAssetTypes: { [k: string]: boolean } = { @@ -733,4 +736,24 @@ export async function streamingCompileWasm() { catch (err) { loaderHelpers.wasmCompilePromise.promise_control.reject(err); } -} \ No newline at end of file +} + +export function preloadWorkers() { + if (!WasmEnableThreads) return; + const jsModuleWorker = resolve_single_asset_path("js-module-threads"); + for (let i = 0; i < loaderHelpers.config.pthreadPoolInitialSize!; i++) { + const workerNumber = loaderHelpers.workerNextNumber++; + const worker: Partial = new Worker(jsModuleWorker.resolvedUrl!, { + name: "dotnet-worker-" + workerNumber.toString().padStart(3, "0"), + }); + worker.info = { + workerNumber, + pthreadId: PThreadPtrNull, + reuseCount: 0, + updateCount: 0, + threadPrefix: worker_empty_prefix, + threadName: "emscripten-pool", + } as any; + loaderHelpers.loadingWorkers.push(worker as any); + } +} diff --git a/src/mono/browser/runtime/loader/config.ts b/src/mono/browser/runtime/loader/config.ts index 6f64d327fe7ad..8aec355743721 100644 --- a/src/mono/browser/runtime/loader/config.ts +++ b/src/mono/browser/runtime/loader/config.ts @@ -187,9 +187,15 @@ export function normalizeConfig() { config.cachedResourcesPurgeDelay = 10000; } - if (WasmEnableThreads && !Number.isInteger(config.pthreadPoolSize)) { - // ActiveIssue https://github.com/dotnet/runtime/issues/75602 - config.pthreadPoolSize = 7; + // ActiveIssue https://github.com/dotnet/runtime/issues/75602 + if (WasmEnableThreads && !Number.isInteger(config.pthreadPoolInitialSize)) { + config.pthreadPoolInitialSize = 7; + } + if (WasmEnableThreads && !Number.isInteger(config.pthreadPoolUnusedSize)) { + config.pthreadPoolUnusedSize = 3; + } + if (WasmEnableThreads && !Number.isInteger(config.finalizerThreadStartDelayMs)) { + config.finalizerThreadStartDelayMs = 200; } // this is how long the Mono GC will try to wait for all threads to be suspended before it gives up and aborts the process diff --git a/src/mono/browser/runtime/loader/globals.ts b/src/mono/browser/runtime/loader/globals.ts index c9d8ffd8e8c9d..7e9708845e65b 100644 --- a/src/mono/browser/runtime/loader/globals.ts +++ b/src/mono/browser/runtime/loader/globals.ts @@ -92,6 +92,8 @@ export function setLoaderGlobals( loadedFiles: [], loadedAssemblies: [], libraryInitializers: [], + loadingWorkers: [], + workerNextNumber: 1, actual_downloaded_assets_count: 0, actual_instantiated_assets_count: 0, expected_downloaded_assets_count: 0, diff --git a/src/mono/browser/runtime/loader/run.ts b/src/mono/browser/runtime/loader/run.ts index 1f85b4e241200..730a692b4ebf5 100644 --- a/src/mono/browser/runtime/loader/run.ts +++ b/src/mono/browser/runtime/loader/run.ts @@ -10,7 +10,7 @@ import { ENVIRONMENT_IS_WEB, ENVIRONMENT_IS_WORKER, emscriptenModule, exportedRu import { deep_merge_config, deep_merge_module, mono_wasm_load_config } from "./config"; import { installUnhandledErrorHandler, mono_exit, registerEmscriptenExitHandlers } from "./exit"; import { setup_proxy_console, mono_log_info, mono_log_debug } from "./logging"; -import { mono_download_assets, prepareAssets, prepareAssetsWorker, resolve_single_asset_path, streamingCompileWasm } from "./assets"; +import { mono_download_assets, preloadWorkers, prepareAssets, prepareAssetsWorker, resolve_single_asset_path, streamingCompileWasm } from "./assets"; import { detect_features_and_polyfill } from "./polyfills"; import { runtimeHelpers, loaderHelpers } from "./globals"; import { init_globalization } from "./icu"; @@ -487,6 +487,7 @@ async function createEmscriptenMain(): Promise { setTimeout(async () => { try { init_globalization(); + preloadWorkers(); await mono_download_assets(); } catch (err) { diff --git a/src/mono/browser/runtime/loader/worker.ts b/src/mono/browser/runtime/loader/worker.ts index 81a9cecad6a74..baeeaf0165f67 100644 --- a/src/mono/browser/runtime/loader/worker.ts +++ b/src/mono/browser/runtime/loader/worker.ts @@ -1,10 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -import { MonoConfigInternal, WorkerToMainMessageType, monoMessageSymbol } from "../types/internal"; +import { MonoConfigInternal, PThreadInfo, WorkerToMainMessageType, monoMessageSymbol } from "../types/internal"; import { MonoConfig } from "../types"; import { deep_merge_config, normalizeConfig } from "./config"; -import { ENVIRONMENT_IS_WEB, loaderHelpers } from "./globals"; +import { ENVIRONMENT_IS_WEB, loaderHelpers, runtimeHelpers } from "./globals"; import { mono_log_debug } from "./logging"; export function setupPreloadChannelToMainThread() { @@ -13,7 +13,8 @@ export function setupPreloadChannelToMainThread() { const mainPort = channel.port2; workerPort.addEventListener("message", (event) => { const config = JSON.parse(event.data.config) as MonoConfig; - onMonoConfigReceived(config); + const monoThreadInfo = JSON.parse(event.data.monoThreadInfo) as PThreadInfo; + onMonoConfigReceived(config, monoThreadInfo); workerPort.close(); mainPort.close(); }, { once: true }); @@ -30,13 +31,13 @@ export function setupPreloadChannelToMainThread() { let workerMonoConfigReceived = false; // called when the main thread sends us the mono config -function onMonoConfigReceived(config: MonoConfigInternal): void { +function onMonoConfigReceived(config: MonoConfigInternal, monoThreadInfo: PThreadInfo): void { if (workerMonoConfigReceived) { mono_log_debug("mono config already received"); return; } - deep_merge_config(loaderHelpers.config, config); + runtimeHelpers.monoThreadInfo = monoThreadInfo; normalizeConfig(); mono_log_debug("mono config received"); workerMonoConfigReceived = true; diff --git a/src/mono/browser/runtime/managed-exports.ts b/src/mono/browser/runtime/managed-exports.ts index 87da9ac8d259e..c8aabae88e37b 100644 --- a/src/mono/browser/runtime/managed-exports.ts +++ b/src/mono/browser/runtime/managed-exports.ts @@ -11,7 +11,7 @@ import { marshal_array_to_cs, marshal_array_to_cs_impl, marshal_bool_to_cs, mars import { marshal_int32_to_js, end_marshal_task_to_js, marshal_string_to_js, begin_marshal_task_to_js, marshal_exception_to_js } from "./marshal-to-js"; import { do_not_force_dispose } from "./gc-handles"; import { assert_c_interop, assert_js_interop } from "./invoke-js"; -import { mono_wasm_main_thread_ptr } from "./pthreads/shared"; +import { mono_wasm_main_thread_ptr } from "./pthreads"; import { _zero_region } from "./memory"; import { stringToUTF8Ptr } from "./strings"; diff --git a/src/mono/browser/runtime/marshal-to-cs.ts b/src/mono/browser/runtime/marshal-to-cs.ts index fa0f299a5f2b8..a42eb724a9215 100644 --- a/src/mono/browser/runtime/marshal-to-cs.ts +++ b/src/mono/browser/runtime/marshal-to-cs.ts @@ -22,7 +22,7 @@ import { _zero_region, forceThreadMemoryViewRefresh, localHeapViewF64, localHeap import { stringToMonoStringRoot, stringToUTF16 } from "./strings"; import { JSMarshalerArgument, JSMarshalerArguments, JSMarshalerType, MarshalerToCs, MarshalerToJs, BoundMarshalerToCs, MarshalerType } from "./types/internal"; import { TypedArray } from "./types/emscripten"; -import { addUnsettledPromise, settleUnsettledPromise } from "./pthreads/shared/eventloop"; +import { addUnsettledPromise, settleUnsettledPromise } from "./pthreads"; import { mono_log_debug } from "./logging"; import { complete_task } from "./managed-exports"; import { gc_locked } from "./gc-lock"; diff --git a/src/mono/browser/runtime/polyfills.ts b/src/mono/browser/runtime/polyfills.ts index 7aae92566b34a..0f8700f84754e 100644 --- a/src/mono/browser/runtime/polyfills.ts +++ b/src/mono/browser/runtime/polyfills.ts @@ -5,7 +5,8 @@ import WasmEnableThreads from "consts:wasmEnableThreads"; import type { EmscriptenReplacements } from "./types/internal"; import type { TypedArray } from "./types/emscripten"; import { ENVIRONMENT_IS_NODE, ENVIRONMENT_IS_WORKER, INTERNAL, Module, loaderHelpers, runtimeHelpers } from "./globals"; -import { replaceEmscriptenPThreadLibrary } from "./pthreads/shared/emscripten-replacements"; +import { replaceEmscriptenPThreadWorker } from "./pthreads"; +import { replaceEmscriptenPThreadUI } from "./pthreads"; const dummyPerformance = { now: function () { @@ -34,7 +35,11 @@ export function initializeReplacements(replacements: EmscriptenReplacements): vo // threads if (WasmEnableThreads && replacements.modulePThread) { - replaceEmscriptenPThreadLibrary(replacements.modulePThread); + if (ENVIRONMENT_IS_WORKER) { + replaceEmscriptenPThreadWorker(replacements.modulePThread); + } else { + replaceEmscriptenPThreadUI(replacements.modulePThread); + } } } diff --git a/src/mono/browser/runtime/pthreads/README.md b/src/mono/browser/runtime/pthreads/README.md index 34f3508988cc9..757cc73a85e42 100644 --- a/src/mono/browser/runtime/pthreads/README.md +++ b/src/mono/browser/runtime/pthreads/README.md @@ -17,11 +17,11 @@ On the other hand, pthreads in native code have a peer relationship: any two thr ## Main thread API -In the main thread, `pthreads/browser` provides a `getThread` function that returns a `{ pthread_ptr: pthread_ptr, worker: Worker, port: MessagePort }` object that can be used to communicate with the worker thread. +In the main thread, `pthreads/ui-thread` provides a `getThread` function that returns a `{ pthread_ptr: pthread_ptr, worker: Worker, port: MessagePort }` object that can be used to communicate with the worker thread. ## Worker thread API -In the worker threads, `pthread/worker` provides `currentWorkerThreadEvents` which is an [`EventTarget`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget) that fires `'dotnet:pthread:created'` and `'dotnet:pthread:attached'` events when a pthread is started on the worker, and when that pthread attaches to the Mono runtime. A good place to add event listeners is in `mono_wasm_pthread_worker_init` in `startup.ts`. +In the worker threads, `pthread/worker-*` provides `currentWorkerThreadEvents` which is an [`EventTarget`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget) that fires `'dotnet:pthread:created'` and `'dotnet:pthread:attached'` events when a pthread is started on the worker, and when that pthread attaches to the Mono runtime. A good place to add event listeners is in `mono_wasm_pthread_worker_init` in `startup.ts`. The events have a `portToMain` property which is a dotnet-specific `MessagePort` for posting messages to the main thread and for listening for messages from the main thread. ## Implementation diff --git a/src/mono/browser/runtime/pthreads/browser/index.ts b/src/mono/browser/runtime/pthreads/browser/index.ts deleted file mode 100644 index 84c58b694d13a..0000000000000 --- a/src/mono/browser/runtime/pthreads/browser/index.ts +++ /dev/null @@ -1,219 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -import WasmEnableThreads from "consts:wasmEnableThreads"; - -import { MonoWorkerToMainMessage, PThreadInfo, PThreadPtr, PThreadPtrNull } from "../shared/types"; -import { MonoThreadMessage, mono_wasm_pthread_ptr, update_thread_info } from "../shared"; -import { PThreadWorker, allocateUnusedWorker, getRunningWorkers, getUnusedWorkerPool, getWorker, loadWasmModuleToWorker } from "../shared/emscripten-internals"; -import { createPromiseController, mono_assert, runtimeHelpers } from "../../globals"; -import { MainToWorkerMessageType, PromiseAndController, PromiseController, WorkerToMainMessageType, monoMessageSymbol } from "../../types/internal"; -import { mono_log_info } from "../../logging"; -import { monoThreadInfo } from "../worker"; -import { mono_wasm_init_diagnostics } from "../../diagnostics"; - -const threadPromises: Map[]> = new Map(); - -export interface Thread { - readonly pthreadPtr: PThreadPtr; - readonly port: MessagePort; - postMessageToWorker(message: T): void; -} - -class ThreadImpl implements Thread { - constructor(readonly pthreadPtr: PThreadPtr, readonly worker: Worker, readonly port: MessagePort) { } - postMessageToWorker(message: T): void { - this.port.postMessage(message); - } -} - -/// wait until the thread with the given id has set up a message port to the runtime -export function waitForThread(pthreadPtr: PThreadPtr): Promise { - if (!WasmEnableThreads) return null as any; - const worker = getWorker(pthreadPtr); - if (worker?.thread) { - return Promise.resolve(worker?.thread); - } - const promiseAndController = createPromiseController(); - const arr = threadPromises.get(pthreadPtr); - if (arr === undefined) { - threadPromises.set(pthreadPtr, [promiseAndController.promise_control]); - } else { - arr.push(promiseAndController.promise_control); - } - return promiseAndController.promise; -} - -export function resolveThreadPromises(pthreadPtr: PThreadPtr, thread?: Thread): void { - if (!WasmEnableThreads) return; - const arr = threadPromises.get(pthreadPtr); - if (arr !== undefined) { - arr.forEach((controller) => { - if (thread) { - controller.resolve(thread); - } else { - controller.reject(); - } - }); - threadPromises.delete(pthreadPtr); - } -} - -// handler that runs in the main thread when a message is received from a pthread worker -function monoWorkerMessageHandler(worker: PThreadWorker, ev: MessageEvent): void { - if (!WasmEnableThreads) return; - let pthreadId: PThreadPtr; - // this is emscripten message - if (ev.data.cmd === "killThread") { - pthreadId = ev.data["thread"]; - mono_assert(pthreadId == worker.info.pthreadId, "expected pthreadId to match"); - worker.info.isRunning = false; - worker.info.pthreadId = PThreadPtrNull; - return; - } - - const message = ev.data[monoMessageSymbol] as MonoWorkerToMainMessage; - if (message === undefined) { - /// N.B. important to ignore messages we don't recognize - Emscripten uses the message event to send internal messages - return; - } - - let port: MessagePort; - let thread: Thread; - pthreadId = message.info?.pthreadId ?? 0; - - worker.info = Object.assign(worker.info, message.info, {}); - switch (message.monoCmd) { - case WorkerToMainMessageType.preload: - // this one shot port from setupPreloadChannelToMainThread - port = message.port!; - port.postMessage({ - type: "pthread", - cmd: MainToWorkerMessageType.applyConfig, - config: JSON.stringify(runtimeHelpers.config) - }); - port.close(); - break; - case WorkerToMainMessageType.pthreadCreated: - port = message.port!; - thread = new ThreadImpl(pthreadId, worker, port); - worker.thread = thread; - worker.info.isRunning = true; - resolveThreadPromises(pthreadId, thread); - break; - case WorkerToMainMessageType.monoRegistered: - case WorkerToMainMessageType.monoAttached: - case WorkerToMainMessageType.enabledInterop: - case WorkerToMainMessageType.monoUnRegistered: - case WorkerToMainMessageType.updateInfo: - // just worker.info updates above - break; - default: - throw new Error(`Unhandled message from worker: ${message.monoCmd}`); - } -} - -let pendingWorkerLoad: PromiseAndController | undefined; - -/// Called by Emscripten internals on the browser thread when a new pthread worker is created and added to the pthread worker pool. -/// At this point the worker doesn't have any pthread assigned to it, yet. -export function onWorkerLoadInitiated(worker: PThreadWorker, loaded: Promise): void { - if (!WasmEnableThreads) return; - worker.addEventListener("message", (ev) => monoWorkerMessageHandler(worker, ev)); - if (pendingWorkerLoad == undefined) { - pendingWorkerLoad = createPromiseController(); - } - loaded.then(() => { - worker.info.isLoaded = true; - if (pendingWorkerLoad != undefined) { - pendingWorkerLoad.promise_control.resolve(); - pendingWorkerLoad = undefined; - } - }); -} - -export function thread_available(): Promise { - if (!WasmEnableThreads) return null as any; - if (pendingWorkerLoad == undefined) { - return Promise.resolve(); - } - return pendingWorkerLoad.promise; -} - -export async function mono_wasm_init_threads() { - if (!WasmEnableThreads) return; - monoThreadInfo.pthreadId = mono_wasm_pthread_ptr(); - monoThreadInfo.threadName = "UI Thread"; - monoThreadInfo.isUI = true; - monoThreadInfo.isRunning = true; - update_thread_info(); - await instantiateWasmPThreadWorkerPool(); - await mono_wasm_init_diagnostics(); -} - -/// We call on the main thread this during startup to pre-allocate a pool of pthread workers. -/// At this point asset resolution needs to be working (ie we loaded MonoConfig). -/// This is used instead of the Emscripten PThread.initMainThread because we call it later. -export function preAllocatePThreadWorkerPool(pthreadPoolSize: number): void { - if (!WasmEnableThreads) return; - for (let i = 0; i < pthreadPoolSize; i++) { - allocateUnusedWorker(); - } -} - -/// We call this on the main thread during startup once we fetched WasmModule. -/// This sends a message to each pre-allocated worker to load the WasmModule and dotnet.js and to set up -/// message handling. -/// This is used instead of the Emscripten "receiveInstance" in "createWasm" because that code is -/// conditioned on a non-zero PTHREAD_POOL_SIZE (but we set it to 0 to avoid early worker allocation). -export async function instantiateWasmPThreadWorkerPool(): Promise { - if (!WasmEnableThreads) return null as any; - // this is largely copied from emscripten's "receiveInstance" in "createWasm" in "src/preamble.js" - const workers = getUnusedWorkerPool(); - if (workers.length > 0) { - const promises = workers.map(loadWasmModuleToWorker); - await Promise.all(promises); - } -} - -// when we create threads with browser event loop, it's not able to be joined by mono's thread join during shutdown and blocks process exit -export function cancelThreads() { - const workers: PThreadWorker[] = getRunningWorkers(); - for (const worker of workers) { - if (worker.info.isExternalEventLoop) { - worker.postMessage({ cmd: "cancel" }); - } - } -} - -export function dumpThreads(): void { - if (!WasmEnableThreads) return; - mono_log_info("Dumping web worker info as seen by UI thread, it could be stale: "); - const emptyInfo = { - pthreadId: 0, - threadPrefix: " - ", - threadName: "????", - isRunning: false, - isAttached: false, - isExternalEventLoop: false, - reuseCount: 0, - }; - const threadInfos: PThreadInfo[] = [ - Object.assign({}, emptyInfo, monoThreadInfo), // UI thread - ]; - for (const worker of getRunningWorkers()) { - threadInfos.push(Object.assign({}, emptyInfo, worker.info)); - } - for (const worker of getUnusedWorkerPool()) { - threadInfos.push(Object.assign({}, emptyInfo, worker.info)); - } - threadInfos.forEach((info, i) => { - const idx = (i + "").padStart(2, "0"); - const isRunning = (info.isRunning + "").padStart(5, " "); - const isAttached = (info.isAttached + "").padStart(5, " "); - const isEventLoop = (info.isExternalEventLoop + "").padStart(5, " "); - const reuseCount = (info.reuseCount + "").padStart(3, " "); - // eslint-disable-next-line no-console - console.info(`${idx} | ${info.threadPrefix}: isRunning:${isRunning} isAttached:${isAttached} isEventLoop:${isEventLoop} reuseCount:${reuseCount} - ${info.threadName}`); - }); -} diff --git a/src/mono/browser/runtime/pthreads/index.ts b/src/mono/browser/runtime/pthreads/index.ts new file mode 100644 index 0000000000000..a7b5da11e03fb --- /dev/null +++ b/src/mono/browser/runtime/pthreads/index.ts @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +export { + mono_wasm_main_thread_ptr, mono_wasm_install_js_worker_interop, mono_wasm_uninstall_js_worker_interop, + mono_wasm_pthread_ptr, update_thread_info, isMonoThreadMessage, monoThreadInfo, +} from "./shared"; +export { + dumpThreads, thread_available, cancelThreads, is_thread_available, + populateEmscriptenPool, mono_wasm_init_threads, init_finalizer_thread, + waitForThread, replaceEmscriptenPThreadUI +} from "./ui-thread"; +export { addUnsettledPromise, settleUnsettledPromise, mono_wasm_eventloop_has_unsettled_interop_promises } from "./worker-eventloop"; +export { + mono_wasm_pthread_on_pthread_attached, mono_wasm_pthread_on_pthread_unregistered, + mono_wasm_pthread_on_pthread_registered, mono_wasm_pthread_set_name, currentWorkerThreadEvents, + dotnetPthreadCreated, initWorkerThreadEvents, replaceEmscriptenPThreadWorker, pthread_self +} from "./worker-thread"; diff --git a/src/mono/browser/runtime/pthreads/shared/index.ts b/src/mono/browser/runtime/pthreads/shared.ts similarity index 75% rename from src/mono/browser/runtime/pthreads/shared/index.ts rename to src/mono/browser/runtime/pthreads/shared.ts index aadab84fa1206..97c41197d35e0 100644 --- a/src/mono/browser/runtime/pthreads/shared/index.ts +++ b/src/mono/browser/runtime/pthreads/shared.ts @@ -4,23 +4,25 @@ import WasmEnableThreads from "consts:wasmEnableThreads"; import BuildConfiguration from "consts:configuration"; -import { ENVIRONMENT_IS_PTHREAD, Module, loaderHelpers, mono_assert, runtimeHelpers } from "../../globals"; -import { set_thread_prefix } from "../../logging"; -import { bindings_init } from "../../startup"; -import { forceDisposeProxies } from "../../gc-handles"; -import { GCHandle, GCHandleNull, WorkerToMainMessageType, monoMessageSymbol } from "../../types/internal"; -import { MonoWorkerToMainMessage, PThreadPtr, PThreadPtrNull } from "./types"; -import { monoThreadInfo } from "../worker"; - -/// 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; -} +import type { GCHandle, MonoThreadMessage, PThreadInfo, PThreadPtr } from "../types/internal"; + +import { ENVIRONMENT_IS_PTHREAD, Module, loaderHelpers, mono_assert, runtimeHelpers } from "../globals"; +import { set_thread_prefix } from "../logging"; +import { bindings_init } from "../startup"; +import { forceDisposeProxies } from "../gc-handles"; +import { monoMessageSymbol, GCHandleNull, PThreadPtrNull, WorkerToMainMessageType } from "../types/internal"; + +// A duplicate in loader/assets.ts +export const worker_empty_prefix = " - "; + +const monoThreadInfoPartial: Partial = { + pthreadId: PThreadPtrNull, + reuseCount: 0, + updateCount: 0, + threadPrefix: worker_empty_prefix, + threadName: "emscripten-loaded", +}; +export const monoThreadInfo: PThreadInfo = monoThreadInfoPartial as PThreadInfo; export function isMonoThreadMessage(x: unknown): x is MonoThreadMessage { if (typeof (x) !== "object" || x === null) { @@ -65,6 +67,7 @@ export function mono_wasm_uninstall_js_worker_interop(): void { // this is just for Debug build of the runtime, making it easier to debug worker threads export function update_thread_info(): void { + if (!WasmEnableThreads) return; const threadType = !monoThreadInfo.isRegistered ? "emsc" : monoThreadInfo.isUI ? "-UI-" : monoThreadInfo.isTimer ? "timr" @@ -110,4 +113,18 @@ export function postMessageToMain(message: MonoWorkerToMainMessage, transfer?: T self.postMessage({ [monoMessageSymbol]: message }, transfer ? transfer : []); -} \ No newline at end of file +} + +export interface MonoWorkerToMainMessage { + monoCmd: WorkerToMainMessageType; + info: PThreadInfo; + port?: MessagePort; +} + +/// Identification of the current thread executing on a worker +export interface PThreadSelf { + info: PThreadInfo; + portToBrowser: MessagePort; + postMessageToBrowser: (message: T, transfer?: Transferable[]) => void; + addEventListenerFromBrowser: (listener: (event: MessageEvent) => void) => void; +} diff --git a/src/mono/browser/runtime/pthreads/shared/emscripten-internals.ts b/src/mono/browser/runtime/pthreads/shared/emscripten-internals.ts deleted file mode 100644 index 0529744570efd..0000000000000 --- a/src/mono/browser/runtime/pthreads/shared/emscripten-internals.ts +++ /dev/null @@ -1,65 +0,0 @@ -// 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 "../../globals"; -import { Thread } from "../browser"; -import { PThreadInfo, PThreadPtr } from "./types"; - -/** @module emscripten-internals accessors to the functions in the emscripten PThreads library, including - * the low-level representations of {@linkcode PThreadPtr} thread info structs, etc. - * Additionally, note that some of these functions are replaced by {@linkcode file://./emscripten-replacements.ts}. - * These have a hard dependency on the version of Emscripten that we are using and may need to be kept in sync with - * {@linkcode file://./../../../emsdk/upstream/emscripten/src/library_pthread.js} - */ - -// This is what we know about the Emscripten PThread library -export interface PThreadLibrary { - unusedWorkers: PThreadWorker[]; - runningWorkers: PThreadWorker[]; - pthreads: PThreadInfoMap; - allocateUnusedWorker: () => void; - loadWasmModuleToWorker: (worker: PThreadWorker) => Promise; - threadInitTLS: () => void, - getNewWorker: () => PThreadWorker, - returnWorkerToPool: (worker: PThreadWorker) => void, -} - - -/// N.B. emscripten deletes the `pthread` property from the worker when it is not actively running a pthread -export interface PThreadWorker extends Worker { - pthread_ptr: PThreadPtr; - loaded: boolean; - // this info is updated via async messages from the worker, it could be stale - info: PThreadInfo; - thread?: Thread; -} - -interface PThreadInfoMap { - [key: number]: PThreadWorker; -} - - -export function getWorker(pthreadPtr: PThreadPtr): PThreadWorker | undefined { - return getModulePThread().pthreads[pthreadPtr as any]; -} - -export function allocateUnusedWorker(): void { - /// See library_pthread.js in Emscripten. - /// This function allocates a new worker and adds it to the pool of workers. - /// It's called when the pool of workers is empty and a new thread is created. - getModulePThread().allocateUnusedWorker(); -} -export function getUnusedWorkerPool(): PThreadWorker[] { - return getModulePThread().unusedWorkers; -} -export function getRunningWorkers(): PThreadWorker[] { - return getModulePThread().runningWorkers; -} - -export function loadWasmModuleToWorker(worker: PThreadWorker): Promise { - return getModulePThread().loadWasmModuleToWorker(worker); -} - -export function getModulePThread(): PThreadLibrary { - return (Module).PThread as PThreadLibrary; -} diff --git a/src/mono/browser/runtime/pthreads/shared/emscripten-replacements.ts b/src/mono/browser/runtime/pthreads/shared/emscripten-replacements.ts deleted file mode 100644 index 0acb8b6615c69..0000000000000 --- a/src/mono/browser/runtime/pthreads/shared/emscripten-replacements.ts +++ /dev/null @@ -1,137 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -import WasmEnableThreads from "consts:wasmEnableThreads"; -import BuildConfiguration from "consts:configuration"; - -import { dumpThreads, onWorkerLoadInitiated, resolveThreadPromises } from "../browser"; -import { mono_wasm_pthread_on_pthread_created, onRunMessage } from "../worker"; -import { PThreadLibrary, PThreadWorker, getModulePThread, getUnusedWorkerPool } from "./emscripten-internals"; -import { Module, loaderHelpers, mono_assert } from "../../globals"; -import { mono_log_warn } from "../../logging"; -import { PThreadPtr, PThreadPtrNull } from "./types"; - -/** @module emscripten-replacements Replacements for individual functions in the emscripten PThreads library. - * These have a hard dependency on the version of Emscripten that we are using and may need to be kept in sync with - * {@linkcode file://./../../../emsdk/upstream/emscripten/src/library_pthread.js} - */ - -export function replaceEmscriptenPThreadLibrary(modulePThread: PThreadLibrary): void { - if (!WasmEnableThreads) return; - - const originalLoadWasmModuleToWorker = modulePThread.loadWasmModuleToWorker; - const originalThreadInitTLS = modulePThread.threadInitTLS; - const originalReturnWorkerToPool = modulePThread.returnWorkerToPool; - const original_emscripten_thread_init = (Module as any)["__emscripten_thread_init"]; - - - (Module as any)["__emscripten_thread_init"] = (pthread_ptr: PThreadPtr, isMainBrowserThread: number, isMainRuntimeThread: number, canBlock: number) => { - onRunMessage(pthread_ptr); - original_emscripten_thread_init(pthread_ptr, isMainBrowserThread, isMainRuntimeThread, canBlock); - }; - - modulePThread.loadWasmModuleToWorker = (worker: PThreadWorker): Promise => { - const afterLoaded = originalLoadWasmModuleToWorker(worker); - afterLoaded.then(() => { - availableThreadCount++; - }); - onWorkerLoadInitiated(worker, afterLoaded); - if (loaderHelpers.config.exitOnUnhandledError) { - worker.onerror = (e) => { - loaderHelpers.mono_exit(1, e); - }; - } - return afterLoaded; - }; - modulePThread.threadInitTLS = (): void => { - originalThreadInitTLS(); - mono_wasm_pthread_on_pthread_created(); - }; - modulePThread.allocateUnusedWorker = allocateUnusedWorker; - modulePThread.getNewWorker = () => getNewWorker(modulePThread); - modulePThread.returnWorkerToPool = (worker: PThreadWorker) => { - // when JS interop is installed on JSWebWorker - // we can't reuse the worker, because user code could leave the worker JS globals in a dirty state - worker.info.isRunning = false; - resolveThreadPromises(worker.pthread_ptr, undefined); - worker.info.pthreadId = PThreadPtrNull; - if (worker.thread?.port) { - worker.thread.port.close(); - } - worker.thread = undefined; - if (worker.info && worker.info.isDirtyBecauseOfInterop) { - // we are on UI thread, invoke the handler directly to destroy the dirty worker - worker.onmessage!(new MessageEvent("message", { - data: { - "cmd": "killThread", - thread: worker.pthread_ptr - } - })); - } else { - availableThreadCount++; - originalReturnWorkerToPool(worker); - } - }; - if (BuildConfiguration === "Debug") { - (globalThis as any).dumpThreads = dumpThreads; - (globalThis as any).getModulePThread = getModulePThread; - } -} - -let availableThreadCount = 0; -export function is_thread_available() { - if (!WasmEnableThreads) return true; - return availableThreadCount > 0; -} - -function getNewWorker(modulePThread: PThreadLibrary): PThreadWorker { - if (!WasmEnableThreads) return null as any; - - if (modulePThread.unusedWorkers.length == 0) { - mono_log_warn(`Failed to find unused WebWorker, this may deadlock. Please increase the pthreadPoolSize. Running threads ${modulePThread.runningWorkers.length}. Loading workers: ${modulePThread.unusedWorkers.length}`); - const worker = allocateUnusedWorker(); - modulePThread.loadWasmModuleToWorker(worker); - availableThreadCount--; - return worker; - } - - // keep them pre-allocated all the time, not just during startup - if (loaderHelpers.config.pthreadPoolSize && modulePThread.unusedWorkers.length <= loaderHelpers.config.pthreadPoolSize) { - const worker = allocateUnusedWorker(); - modulePThread.loadWasmModuleToWorker(worker); - } - - for (let i = 0; i < modulePThread.unusedWorkers.length; i++) { - const worker = modulePThread.unusedWorkers[i]; - if (worker.loaded) { - modulePThread.unusedWorkers.splice(i, 1); - availableThreadCount--; - return worker; - } - } - mono_log_warn(`Failed to find loaded WebWorker, this may deadlock. Please increase the pthreadPoolSize. Running threads ${modulePThread.runningWorkers.length}. Loading workers: ${modulePThread.unusedWorkers.length}`); - availableThreadCount--; // negative value - return modulePThread.unusedWorkers.pop()!; -} - -/// We replace Module["PThreads"].allocateUnusedWorker with this version that knows about assets -function allocateUnusedWorker(): PThreadWorker { - if (!WasmEnableThreads) return null as any; - - const asset = loaderHelpers.resolve_single_asset_path("js-module-threads"); - const uri = asset.resolvedUrl; - mono_assert(uri !== undefined, "could not resolve the uri for the js-module-threads asset"); - const worker = new Worker(uri) as PThreadWorker; - getUnusedWorkerPool().push(worker); - worker.loaded = false; - worker.info = { - pthreadId: PThreadPtrNull, - reuseCount: 0, - updateCount: 0, - threadPrefix: " - ", - threadName: "emscripten-pool", - }; - return worker; -} - - diff --git a/src/mono/browser/runtime/pthreads/shared/tsconfig.json b/src/mono/browser/runtime/pthreads/shared/tsconfig.json deleted file mode 100644 index 8986477dd8fc3..0000000000000 --- a/src/mono/browser/runtime/pthreads/shared/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../tsconfig.shared.json", - "include": [ - "../../**/*.ts", - "../../**/*.d.ts" - ] - -} diff --git a/src/mono/browser/runtime/pthreads/shared/types.ts b/src/mono/browser/runtime/pthreads/shared/types.ts deleted file mode 100644 index 1ea8e0c65e579..0000000000000 --- a/src/mono/browser/runtime/pthreads/shared/types.ts +++ /dev/null @@ -1,44 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -import type { WorkerToMainMessageType } from "../../types/internal"; - -/// pthread_t in C -export type PThreadPtr = { - __brand: "PThreadPtr" -} -export const PThreadPtrNull: PThreadPtr = 0; - -export interface PThreadInfo { - pthreadId: PThreadPtr; - - reuseCount: number, - updateCount: number, - - threadName: string, - threadPrefix: string, - - isLoaded?: boolean, - isRegistered?: boolean, - isRunning?: boolean, - isAttached?: boolean, - isExternalEventLoop?: boolean, - isUI?: boolean; - isBackground?: boolean, - isDebugger?: boolean, - isThreadPoolWorker?: boolean, - isTimer?: boolean, - isLongRunning?: boolean, - isThreadPoolGate?: boolean, - isFinalizer?: boolean, - isDirtyBecauseOfInterop?: boolean, -} - -/// 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 MonoWorkerToMainMessage { - monoCmd: WorkerToMainMessageType; - info: PThreadInfo; - port?: MessagePort; -} diff --git a/src/mono/browser/runtime/pthreads/ui-thread.ts b/src/mono/browser/runtime/pthreads/ui-thread.ts new file mode 100644 index 0000000000000..878ee38eadbb9 --- /dev/null +++ b/src/mono/browser/runtime/pthreads/ui-thread.ts @@ -0,0 +1,351 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import WasmEnableThreads from "consts:wasmEnableThreads"; +import BuildConfiguration from "consts:configuration"; + +import { } from "../globals"; +import { mono_log_warn } from "../logging"; +import { MonoWorkerToMainMessage, monoThreadInfo, mono_wasm_pthread_ptr, update_thread_info, worker_empty_prefix } from "./shared"; +import { Module, ENVIRONMENT_IS_WORKER, createPromiseController, loaderHelpers, mono_assert, runtimeHelpers } from "../globals"; +import { PThreadLibrary, MainToWorkerMessageType, MonoThreadMessage, PThreadInfo, PThreadPtr, PThreadPtrNull, PThreadWorker, PromiseAndController, PromiseController, Thread, WorkerToMainMessageType, monoMessageSymbol } from "../types/internal"; +import { mono_log_error, mono_log_info } from "../logging"; +import { threads_c_functions as cwraps } from "../cwraps"; + +const threadPromises: Map[]> = new Map(); + +class ThreadImpl implements Thread { + constructor(readonly pthreadPtr: PThreadPtr, readonly worker: Worker, readonly port: MessagePort) { } + postMessageToWorker(message: T): void { + this.port.postMessage(message); + } +} + +/// wait until the thread with the given id has set up a message port to the runtime +export function waitForThread(pthreadPtr: PThreadPtr): Promise { + if (!WasmEnableThreads) return null as any; + mono_assert(!ENVIRONMENT_IS_WORKER, "waitForThread should only be called from the UI thread"); + const worker = getWorker(pthreadPtr); + if (worker?.thread) { + return Promise.resolve(worker?.thread); + } + const promiseAndController = createPromiseController(); + const arr = threadPromises.get(pthreadPtr); + if (arr === undefined) { + threadPromises.set(pthreadPtr, [promiseAndController.promise_control]); + } else { + arr.push(promiseAndController.promise_control); + } + return promiseAndController.promise; +} + +export function resolveThreadPromises(pthreadPtr: PThreadPtr, thread?: Thread): void { + if (!WasmEnableThreads) return; + const arr = threadPromises.get(pthreadPtr); + if (arr !== undefined) { + arr.forEach((controller) => { + if (thread) { + controller.resolve(thread); + } else { + controller.reject(); + } + }); + threadPromises.delete(pthreadPtr); + } +} + +// handler that runs in the main thread when a message is received from a pthread worker +function monoWorkerMessageHandler(worker: PThreadWorker, ev: MessageEvent): void { + if (!WasmEnableThreads) return; + let pthreadId: PThreadPtr; + // this is emscripten message + if (ev.data.cmd === "killThread") { + pthreadId = ev.data["thread"]; + mono_assert(pthreadId == worker.info.pthreadId, "expected pthreadId to match"); + worker.info.isRunning = false; + worker.info.pthreadId = PThreadPtrNull; + return; + } + + const message = ev.data[monoMessageSymbol] as MonoWorkerToMainMessage; + if (message === undefined) { + /// N.B. important to ignore messages we don't recognize - Emscripten uses the message event to send internal messages + return; + } + + let port: MessagePort; + let thread: Thread; + pthreadId = message.info?.pthreadId ?? 0; + worker.info = Object.assign({}, worker.info, message.info); + switch (message.monoCmd) { + case WorkerToMainMessageType.preload: + // this one shot port from setupPreloadChannelToMainThread + port = message.port!; + port.postMessage({ + type: "pthread", + cmd: MainToWorkerMessageType.applyConfig, + config: JSON.stringify(runtimeHelpers.config), + monoThreadInfo: JSON.stringify(worker.info), + }); + port.close(); + break; + case WorkerToMainMessageType.pthreadCreated: + port = message.port!; + thread = new ThreadImpl(pthreadId, worker, port); + worker.thread = thread; + worker.info.isRunning = true; + resolveThreadPromises(pthreadId, thread); + break; + case WorkerToMainMessageType.monoRegistered: + case WorkerToMainMessageType.monoAttached: + case WorkerToMainMessageType.enabledInterop: + case WorkerToMainMessageType.monoUnRegistered: + case WorkerToMainMessageType.updateInfo: + // just worker.info updates above + break; + default: + throw new Error(`Unhandled message from worker: ${message.monoCmd}`); + } +} + +let pendingWorkerLoad: PromiseAndController | undefined; + +/// Called by Emscripten internals on the browser thread when a new pthread worker is created and added to the pthread worker pool. +/// At this point the worker doesn't have any pthread assigned to it, yet. +export function onWorkerLoadInitiated(worker: PThreadWorker, loaded: Promise): void { + if (!WasmEnableThreads) return; + worker.addEventListener("message", (ev) => monoWorkerMessageHandler(worker, ev)); + if (pendingWorkerLoad == undefined) { + pendingWorkerLoad = createPromiseController(); + } + loaded.then(() => { + worker.info.isLoaded = true; + if (pendingWorkerLoad != undefined) { + pendingWorkerLoad.promise_control.resolve(); + pendingWorkerLoad = undefined; + } + }); +} + +export function thread_available(): Promise { + if (!WasmEnableThreads) return null as any; + if (pendingWorkerLoad == undefined) { + return Promise.resolve(); + } + return pendingWorkerLoad.promise; +} + +export function populateEmscriptenPool(): void { + if (!WasmEnableThreads) return; + const unused = getUnusedWorkerPool(); + for (const worker of loaderHelpers.loadingWorkers) { + unused.push(worker); + } + loaderHelpers.loadingWorkers = []; +} + +export async function mono_wasm_init_threads() { + if (!WasmEnableThreads) return; + + // setup the UI thread + monoThreadInfo.pthreadId = mono_wasm_pthread_ptr(); + monoThreadInfo.threadName = "UI Thread"; + monoThreadInfo.isUI = true; + monoThreadInfo.isRunning = true; + update_thread_info(); + + // wait until all workers in the pool are loaded - ready to be used as pthread synchronously + const workers = getUnusedWorkerPool(); + if (workers.length > 0) { + const promises = workers.map(loadWasmModuleToWorker); + await Promise.all(promises); + } +} + +// when we create threads with browser event loop, it's not able to be joined by mono's thread join during shutdown and blocks process exit +export function cancelThreads() { + if (!WasmEnableThreads) return; + const workers: PThreadWorker[] = getRunningWorkers(); + for (const worker of workers) { + if (worker.info.isExternalEventLoop) { + worker.postMessage({ cmd: "cancel" }); + } + } +} + +export function dumpThreads(): void { + if (!WasmEnableThreads) return; + mono_log_info("Dumping web worker info as seen by UI thread, it could be stale: "); + const emptyInfo: PThreadInfo = { + workerNumber: -1, + pthreadId: PThreadPtrNull, + threadPrefix: worker_empty_prefix, + threadName: "????", + isRunning: false, + isAttached: false, + isExternalEventLoop: false, + reuseCount: 0, + updateCount: 0, + }; + const threadInfos: PThreadInfo[] = [ + Object.assign({}, emptyInfo, monoThreadInfo), // UI thread + ]; + for (const worker of getRunningWorkers()) { + threadInfos.push(Object.assign({}, emptyInfo, worker.info)); + } + for (const worker of getUnusedWorkerPool()) { + threadInfos.push(Object.assign({}, emptyInfo, worker.info)); + } + threadInfos.forEach((info) => { + const idx = info.workerNumber.toString().padStart(3, "0"); + const isRunning = (info.isRunning + "").padStart(5, " "); + const isAttached = (info.isAttached + "").padStart(5, " "); + const isEventLoop = (info.isExternalEventLoop + "").padStart(5, " "); + const reuseCount = (info.reuseCount + "").padStart(3, " "); + // eslint-disable-next-line no-console + console.info(`${idx} | ${info.threadPrefix}: isRunning:${isRunning} isAttached:${isAttached} isEventLoop:${isEventLoop} reuseCount:${reuseCount} - ${info.threadName}`); + }); +} + +export function init_finalizer_thread() { + // we don't need it immediately, so we can wait a bit, to keep CPU working on normal startup + setTimeout(() => { + try { + cwraps.mono_wasm_init_finalizer_thread(); + } + catch (err) { + mono_log_error("init_finalizer_thread() failed", err); + loaderHelpers.mono_exit(1, err); + } + }, loaderHelpers.config.finalizerThreadStartDelayMs); +} + +export function replaceEmscriptenPThreadUI(modulePThread: PThreadLibrary): void { + if (!WasmEnableThreads) return; + + const originalLoadWasmModuleToWorker = modulePThread.loadWasmModuleToWorker; + const originalReturnWorkerToPool = modulePThread.returnWorkerToPool; + + modulePThread.loadWasmModuleToWorker = (worker: PThreadWorker): Promise => { + const afterLoaded = originalLoadWasmModuleToWorker(worker); + afterLoaded.then(() => { + availableThreadCount++; + }); + onWorkerLoadInitiated(worker, afterLoaded); + if (loaderHelpers.config.exitOnUnhandledError) { + worker.onerror = (e) => { + loaderHelpers.mono_exit(1, e); + }; + } + return afterLoaded; + }; + modulePThread.allocateUnusedWorker = allocateUnusedWorker; + modulePThread.getNewWorker = () => getNewWorker(modulePThread); + modulePThread.returnWorkerToPool = (worker: PThreadWorker) => { + // when JS interop is installed on JSWebWorker + // we can't reuse the worker, because user code could leave the worker JS globals in a dirty state + worker.info.isRunning = false; + resolveThreadPromises(worker.pthread_ptr, undefined); + worker.info.pthreadId = PThreadPtrNull; + if (worker.thread?.port) { + worker.thread.port.close(); + } + worker.thread = undefined; + if (worker.info && worker.info.isDirtyBecauseOfInterop) { + // we are on UI thread, invoke the handler directly to destroy the dirty worker + worker.onmessage!(new MessageEvent("message", { + data: { + "cmd": "killThread", + thread: worker.pthread_ptr + } + })); + } else { + availableThreadCount++; + originalReturnWorkerToPool(worker); + } + }; + if (BuildConfiguration === "Debug") { + (globalThis as any).dumpThreads = dumpThreads; + (globalThis as any).getModulePThread = getModulePThread; + } +} + +let availableThreadCount = 0; +export function is_thread_available() { + if (!WasmEnableThreads) return true; + return availableThreadCount > 0; +} + +function getNewWorker(modulePThread: PThreadLibrary): PThreadWorker { + if (!WasmEnableThreads) return null as any; + + if (modulePThread.unusedWorkers.length == 0) { + mono_log_warn(`Failed to find unused WebWorker, this may deadlock. Please increase the pthreadPoolReady. Running threads ${modulePThread.runningWorkers.length}. Loading workers: ${modulePThread.unusedWorkers.length}`); + const worker = allocateUnusedWorker(); + modulePThread.loadWasmModuleToWorker(worker); + availableThreadCount--; + return worker; + } + + // keep them pre-allocated all the time, not just during startup + if (modulePThread.unusedWorkers.length <= loaderHelpers.config.pthreadPoolUnusedSize!) { + const worker = allocateUnusedWorker(); + modulePThread.loadWasmModuleToWorker(worker); + } + + for (let i = 0; i < modulePThread.unusedWorkers.length; i++) { + const worker = modulePThread.unusedWorkers[i]; + if (worker.loaded) { + modulePThread.unusedWorkers.splice(i, 1); + availableThreadCount--; + return worker; + } + } + mono_log_warn(`Failed to find loaded WebWorker, this may deadlock. Please increase the pthreadPoolReady. Running threads ${modulePThread.runningWorkers.length}. Loading workers: ${modulePThread.unusedWorkers.length}`); + availableThreadCount--; // negative value + return modulePThread.unusedWorkers.pop()!; +} + +/// We replace Module["PThreads"].allocateUnusedWorker with this version that knows about assets +function allocateUnusedWorker(): PThreadWorker { + if (!WasmEnableThreads) return null as any; + + const asset = loaderHelpers.resolve_single_asset_path("js-module-threads"); + const uri = asset.resolvedUrl; + mono_assert(uri !== undefined, "could not resolve the uri for the js-module-threads asset"); + const workerNumber = loaderHelpers.workerNextNumber++; + const worker = new Worker(uri, { + name: "dotnet-worker-" + workerNumber.toString().padStart(3, "0"), + }) as PThreadWorker; + getUnusedWorkerPool().push(worker); + worker.loaded = false; + worker.info = { + workerNumber, + pthreadId: PThreadPtrNull, + reuseCount: 0, + updateCount: 0, + threadPrefix: worker_empty_prefix, + threadName: "emscripten-pool", + }; + return worker; +} + +export function getWorker(pthreadPtr: PThreadPtr): PThreadWorker | undefined { + return getModulePThread().pthreads[pthreadPtr as any]; +} + +export function getUnusedWorkerPool(): PThreadWorker[] { + return getModulePThread().unusedWorkers; +} + +export function getRunningWorkers(): PThreadWorker[] { + return getModulePThread().runningWorkers; +} + +export function loadWasmModuleToWorker(worker: PThreadWorker): Promise { + return getModulePThread().loadWasmModuleToWorker(worker); +} + +export function getModulePThread(): PThreadLibrary { + return (Module).PThread as PThreadLibrary; +} diff --git a/src/mono/browser/runtime/pthreads/shared/eventloop.ts b/src/mono/browser/runtime/pthreads/worker-eventloop.ts similarity index 99% rename from src/mono/browser/runtime/pthreads/shared/eventloop.ts rename to src/mono/browser/runtime/pthreads/worker-eventloop.ts index 602c3fb221fbd..7b8a42b05ac66 100644 --- a/src/mono/browser/runtime/pthreads/shared/eventloop.ts +++ b/src/mono/browser/runtime/pthreads/worker-eventloop.ts @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. - let perThreadUnsettledPromiseCount = 0; export function addUnsettledPromise() { diff --git a/src/mono/browser/runtime/pthreads/worker/events.ts b/src/mono/browser/runtime/pthreads/worker-events.ts similarity index 98% rename from src/mono/browser/runtime/pthreads/worker/events.ts rename to src/mono/browser/runtime/pthreads/worker-events.ts index ace256459d439..709e1980d82f5 100644 --- a/src/mono/browser/runtime/pthreads/worker/events.ts +++ b/src/mono/browser/runtime/pthreads/worker-events.ts @@ -2,7 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. import WasmEnableThreads from "consts:wasmEnableThreads"; -import { PThreadSelf } from "./index"; +import { PThreadSelf } from "./shared"; export const dotnetPthreadCreated = "dotnet:pthread:created" as const; export const dotnetPthreadAttached = "dotnet:pthread:attached" as const; diff --git a/src/mono/browser/runtime/pthreads/worker/index.ts b/src/mono/browser/runtime/pthreads/worker-thread.ts similarity index 83% rename from src/mono/browser/runtime/pthreads/worker/index.ts rename to src/mono/browser/runtime/pthreads/worker-thread.ts index 29229c23e5fd8..083f87d7de025 100644 --- a/src/mono/browser/runtime/pthreads/worker/index.ts +++ b/src/mono/browser/runtime/pthreads/worker-thread.ts @@ -5,22 +5,22 @@ import WasmEnableThreads from "consts:wasmEnableThreads"; -import { ENVIRONMENT_IS_PTHREAD, loaderHelpers, mono_assert } from "../../globals"; -import { mono_wasm_pthread_ptr, postMessageToMain, update_thread_info } from "../shared"; -import { PThreadInfo, PThreadPtr, PThreadPtrNull } from "../shared/types"; -import { WorkerToMainMessageType, is_nullish } from "../../types/internal"; -import { MonoThreadMessage } from "../shared"; +import { Module } from "../globals"; + +import { ENVIRONMENT_IS_PTHREAD, loaderHelpers, mono_assert, runtimeHelpers } from "../globals"; +import { PThreadSelf, monoThreadInfo, mono_wasm_pthread_ptr, postMessageToMain, update_thread_info } from "./shared"; +import { PThreadLibrary, MonoThreadMessage, PThreadInfo, PThreadPtr, WorkerToMainMessageType, is_nullish } from "../types/internal"; import { makeWorkerThreadEvent, dotnetPthreadCreated, dotnetPthreadAttached, WorkerThreadEventTarget -} from "./events"; -import { postRunWorker, preRunWorker } from "../../startup"; -import { mono_log_debug, mono_log_error } from "../../logging"; -import { CharPtr } from "../../types/emscripten"; -import { utf8ToString } from "../../strings"; -import { forceThreadMemoryViewRefresh } from "../../memory"; +} from "./worker-events"; +import { postRunWorker, preRunWorker } from "../startup"; +import { mono_log_debug, mono_log_error } from "../logging"; +import { CharPtr } from "../types/emscripten"; +import { utf8ToString } from "../strings"; +import { forceThreadMemoryViewRefresh } from "../memory"; // re-export some of the events types export { @@ -29,15 +29,9 @@ export { dotnetPthreadCreated, WorkerThreadEvent, WorkerThreadEventTarget, -} from "./events"; - -/// Identification of the current thread executing on a worker -export interface PThreadSelf { - info: PThreadInfo; - portToBrowser: MessagePort; - postMessageToBrowser: (message: T, transfer?: Transferable[]) => void; - addEventListenerFromBrowser: (listener: (event: MessageEvent) => void) => void; -} +} from "./worker-events"; + +export let pthread_self: PThreadSelf = null as any as PThreadSelf; class WorkerSelf implements PThreadSelf { constructor(public info: PThreadInfo, public portToBrowser: MessagePort) { @@ -55,17 +49,6 @@ class WorkerSelf implements PThreadSelf { } } -// 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; -export const monoThreadInfo: PThreadInfo = { - pthreadId: PThreadPtrNull, - reuseCount: 0, - updateCount: 0, - threadPrefix: " - ", - threadName: "emscripten-loaded", -}; - /// This is the "public internal" API for runtime subsystems that wish to be notified about /// pthreads that are running on the current worker. /// Example: @@ -78,6 +61,7 @@ export let currentWorkerThreadEvents: WorkerThreadEventTarget = undefined as any export function initWorkerThreadEvents() { // treeshake if threads are disabled currentWorkerThreadEvents = WasmEnableThreads ? new globalThis.EventTarget() : null as any as WorkerThreadEventTarget; + Object.assign(monoThreadInfo, runtimeHelpers.monoThreadInfo); } // this is the message handler for the worker that receives messages from the main thread @@ -86,7 +70,7 @@ function monoDedicatedChannelMessageFromMainToWorker(event: MessageEvent mono_log_debug("got message from main on the dedicated channel", event.data); } -export function onRunMessage(pthread_ptr: PThreadPtr) { +export function on_emscripten_thread_init(pthread_ptr: PThreadPtr) { monoThreadInfo.pthreadId = pthread_ptr; forceThreadMemoryViewRefresh(); } @@ -221,3 +205,19 @@ export function mono_wasm_pthread_on_pthread_unregistered(pthread_id: PThreadPtr throw err; } } + +export function replaceEmscriptenPThreadWorker(modulePThread: PThreadLibrary): void { + if (!WasmEnableThreads) return; + + const originalThreadInitTLS = modulePThread.threadInitTLS; + const original_emscripten_thread_init = (Module as any)["__emscripten_thread_init"]; + + (Module as any)["__emscripten_thread_init"] = (pthread_ptr: PThreadPtr, isMainBrowserThread: number, isMainRuntimeThread: number, canBlock: number) => { + on_emscripten_thread_init(pthread_ptr); + original_emscripten_thread_init(pthread_ptr, isMainBrowserThread, isMainRuntimeThread, canBlock); + }; + modulePThread.threadInitTLS = (): void => { + originalThreadInitTLS(); + mono_wasm_pthread_on_pthread_created(); + }; +} \ No newline at end of file diff --git a/src/mono/browser/runtime/pthreads/worker/tsconfig.json b/src/mono/browser/runtime/pthreads/worker/tsconfig.json deleted file mode 100644 index 071a4d824c62a..0000000000000 --- a/src/mono/browser/runtime/pthreads/worker/tsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": "../../tsconfig.worker.json", - "include": [ - "../../**/*.ts", - "../../**/*.d.ts" - ] -} diff --git a/src/mono/browser/runtime/run.ts b/src/mono/browser/runtime/run.ts index 8760aab6514ca..e35c6dc833aec 100644 --- a/src/mono/browser/runtime/run.ts +++ b/src/mono/browser/runtime/run.ts @@ -8,7 +8,7 @@ import { mono_wasm_wait_for_debugger } from "./debug"; import { mono_wasm_set_main_args } from "./startup"; import cwraps from "./cwraps"; import { mono_log_info } from "./logging"; -import { cancelThreads } from "./pthreads/browser"; +import { cancelThreads } from "./pthreads"; import { call_entry_point } from "./managed-exports"; /** diff --git a/src/mono/browser/runtime/scheduling.ts b/src/mono/browser/runtime/scheduling.ts index 4b01536b735ee..c9cbf205d42d4 100644 --- a/src/mono/browser/runtime/scheduling.ts +++ b/src/mono/browser/runtime/scheduling.ts @@ -5,8 +5,8 @@ import WasmEnableThreads from "consts:wasmEnableThreads"; import cwraps from "./cwraps"; import { ENVIRONMENT_IS_WORKER, Module, loaderHelpers } from "./globals"; -import { is_thread_available } from "./pthreads/shared/emscripten-replacements"; import { forceThreadMemoryViewRefresh } from "./memory"; +import { is_thread_available } from "./pthreads"; let spread_timers_maximum = 0; let pump_count = 0; diff --git a/src/mono/browser/runtime/startup.ts b/src/mono/browser/runtime/startup.ts index 127d65dea4e81..9759e879254b0 100644 --- a/src/mono/browser/runtime/startup.ts +++ b/src/mono/browser/runtime/startup.ts @@ -5,7 +5,7 @@ import WasmEnableThreads from "consts:wasmEnableThreads"; import { DotnetModuleInternal, CharPtrNull } from "./types/internal"; import { ENVIRONMENT_IS_NODE, exportedRuntimeAPI, INTERNAL, loaderHelpers, Module, runtimeHelpers, createPromiseController, mono_assert, ENVIRONMENT_IS_WORKER } from "./globals"; -import cwraps, { init_c_exports, threads_c_functions as tcwraps } from "./cwraps"; +import cwraps, { init_c_exports } from "./cwraps"; import { mono_wasm_raise_debug_event, mono_wasm_runtime_ready } from "./debug"; import { toBase64StringImpl } from "./base64"; import { mono_wasm_init_aot_profiler, mono_wasm_init_browser_profiler } from "./profiler"; @@ -23,14 +23,15 @@ import { interp_pgo_load_data, interp_pgo_save_data } from "./interp-pgo"; import { mono_log_debug, mono_log_error, mono_log_warn } from "./logging"; // threads -import { preAllocatePThreadWorkerPool, mono_wasm_init_threads } from "./pthreads/browser"; -import { currentWorkerThreadEvents, dotnetPthreadCreated, initWorkerThreadEvents, monoThreadInfo } from "./pthreads/worker"; -import { mono_wasm_pthread_ptr, update_thread_info } from "./pthreads/shared"; +import { populateEmscriptenPool, mono_wasm_init_threads, init_finalizer_thread } from "./pthreads"; +import { currentWorkerThreadEvents, dotnetPthreadCreated, initWorkerThreadEvents, monoThreadInfo } from "./pthreads"; +import { mono_wasm_pthread_ptr, update_thread_info } from "./pthreads"; import { jiterpreter_allocate_tables } from "./jiterpreter-support"; import { localHeapViewU8 } from "./memory"; import { assertNoProxies } from "./gc-handles"; import { runtimeList } from "./exports"; import { nativeAbort, nativeExit } from "./run"; +import { mono_wasm_init_diagnostics } from "./diagnostics"; export async function configureRuntimeStartup(): Promise { await init_polyfills_async(); @@ -268,7 +269,7 @@ async function onRuntimeInitializedAsync(userOnRuntimeInitialized: () => void) { Module.runtimeKeepalivePush(); // load runtime and apply environment settings (if necessary) - start_runtime(); + await start_runtime(); if (runtimeHelpers.config.interpreterPgo) { await interp_pgo_load_data(); @@ -326,10 +327,6 @@ async function postRunAsync(userpostRun: (() => void)[]) { Module["FS_createPath"]("/", "usr", true, true); Module["FS_createPath"]("/", "usr/share", true, true); - if (WasmEnableThreads) { - tcwraps.mono_wasm_init_finalizer_thread(); - } - // all user Module.postRun callbacks userpostRun.map(fn => fn()); endMeasure(mark, MeasuredBlock.postRun); @@ -396,7 +393,7 @@ async function mono_wasm_pre_init_essential_async(): Promise { Module.addRunDependency("mono_wasm_pre_init_essential_async"); if (WasmEnableThreads) { - preAllocatePThreadWorkerPool(runtimeHelpers.config.pthreadPoolSize!); + populateEmscriptenPool(); } Module.removeRunDependency("mono_wasm_pre_init_essential_async"); @@ -483,7 +480,7 @@ async function ensureUsedWasmFeatures() { } } -export function start_runtime() { +export async function start_runtime() { try { const mark = startMeasure(); mono_log_debug("Initializing mono runtime"); @@ -503,6 +500,11 @@ export function start_runtime() { if (runtimeHelpers.config.browserProfilerOptions) mono_wasm_init_browser_profiler(runtimeHelpers.config.browserProfilerOptions); + if (WasmEnableThreads) { + // this is not mono-attached thread, so we can start it earlier + await mono_wasm_init_diagnostics(); + } + mono_wasm_load_runtime(); jiterpreter_allocate_tables(); @@ -514,10 +516,14 @@ export function start_runtime() { if (WasmEnableThreads) { monoThreadInfo.isAttached = true; monoThreadInfo.isRegistered = true; + monoThreadInfo.pthreadId = runtimeHelpers.managedThreadTID = mono_wasm_pthread_ptr(); + monoThreadInfo.workerNumber = 0; update_thread_info(); runtimeHelpers.proxyGCHandle = install_main_synchronization_context(); - runtimeHelpers.managedThreadTID = mono_wasm_pthread_ptr(); runtimeHelpers.isCurrentThread = true; + + // start finalizer thread, lazy + init_finalizer_thread(); } // get GCHandle of the ctx diff --git a/src/mono/browser/runtime/types/index.ts b/src/mono/browser/runtime/types/index.ts index e0f76ea3a546e..8d9c8a28ba148 100644 --- a/src/mono/browser/runtime/types/index.ts +++ b/src/mono/browser/runtime/types/index.ts @@ -143,7 +143,15 @@ export type MonoConfig = { /** * initial number of workers to add to the emscripten pthread pool */ - pthreadPoolSize?: number, + pthreadPoolInitialSize?: number, + /** + * number of unused workers kept in the emscripten pthread pool after startup + */ + pthreadPoolUnusedSize?: number, + /** + * Delay in milliseconds before starting the finalizer thread + */ + finalizerThreadStartDelayMs?: number, /** * If true, a list of the methods optimized by the interpreter will be saved and used for faster startup * on future runs of the application diff --git a/src/mono/browser/runtime/types/internal.ts b/src/mono/browser/runtime/types/internal.ts index 3f021a50a01ce..ae4ce1ff4813a 100644 --- a/src/mono/browser/runtime/types/internal.ts +++ b/src/mono/browser/runtime/types/internal.ts @@ -2,8 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. import type { AssetEntry, DotnetModuleConfig, LoadBootResourceCallback, LoadingResource, MonoConfig, RuntimeAPI, SingleAssetBehaviors } from "."; -import type { PThreadLibrary } from "../pthreads/shared/emscripten-internals"; -import { PThreadPtr } from "../pthreads/shared/types"; import type { CharPtr, EmscriptenModule, ManagedPointer, NativePointer, VoidPtr, Int32Ptr } from "./emscripten"; export type GCHandle = { @@ -15,6 +13,9 @@ export type JSHandle = { export type JSFnHandle = { __brand: "JSFnHandle" } +export type PThreadPtr = { + __brand: "PThreadPtr" // like pthread_t in C +} export interface MonoObject extends ManagedPointer { __brandMonoObject: "MonoObject" } @@ -61,6 +62,7 @@ export const GCHandleInvalid: GCHandle = -1; export const VoidPtrNull: VoidPtr = 0; export const CharPtrNull: CharPtr = 0; export const NativePointerNull: NativePointer = 0; +export const PThreadPtrNull: PThreadPtr = 0; export function coerceNull(ptr: T | null | undefined): T { if ((ptr === null) || (ptr === undefined)) @@ -129,6 +131,8 @@ export type LoaderHelpers = { scriptUrl: string modulesUniqueQuery?: string preferredIcuAsset?: string | null, + loadingWorkers: PThreadWorker[], + workerNextNumber: number, actual_downloaded_assets_count: number, actual_instantiated_assets_count: number, @@ -200,6 +204,7 @@ export type RuntimeHelpers = { getMemory(): WebAssembly.Memory, getWasmIndirectFunctionTable(): WebAssembly.Table, runtimeReady: boolean, + monoThreadInfo: PThreadInfo, proxyGCHandle: GCHandle | undefined, managedThreadTID: PThreadPtr, isCurrentThread: boolean, @@ -488,3 +493,65 @@ export const enum WorkerToMainMessageType { export const enum MainToWorkerMessageType { applyConfig = "apply_mono_config", } + +export interface PThreadWorker extends Worker { + pthread_ptr: PThreadPtr; + loaded: boolean; + // this info is updated via async messages from the worker, it could be stale + info: PThreadInfo; + thread?: Thread; +} + +export interface PThreadInfo { + pthreadId: PThreadPtr; + + workerNumber: number, + reuseCount: number, + updateCount: number, + + threadName: string, + threadPrefix: string, + + isLoaded?: boolean, + isRegistered?: boolean, + isRunning?: boolean, + isAttached?: boolean, + isExternalEventLoop?: boolean, + isUI?: boolean; + isBackground?: boolean, + isDebugger?: boolean, + isThreadPoolWorker?: boolean, + isTimer?: boolean, + isLongRunning?: boolean, + isThreadPoolGate?: boolean, + isFinalizer?: boolean, + isDirtyBecauseOfInterop?: boolean, +} + +export interface PThreadLibrary { + unusedWorkers: PThreadWorker[]; + runningWorkers: PThreadWorker[]; + pthreads: PThreadInfoMap; + allocateUnusedWorker: () => void; + loadWasmModuleToWorker: (worker: PThreadWorker) => Promise; + threadInitTLS: () => void, + getNewWorker: () => PThreadWorker, + returnWorkerToPool: (worker: PThreadWorker) => void, +} + +export interface PThreadInfoMap { + [key: number]: PThreadWorker; +} + +export interface Thread { + readonly pthreadPtr: PThreadPtr; + readonly port: MessagePort; + postMessageToWorker(message: T): void; +} + +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; +} diff --git a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/BootJsonData.cs b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/BootJsonData.cs index 97bd05110bfbf..22edc2c68fbe1 100644 --- a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/BootJsonData.cs +++ b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/BootJsonData.cs @@ -108,9 +108,14 @@ public class BootJsonData public object diagnosticTracing { get; set; } /// - /// Gets or sets pthread pool size. + /// Gets or sets pthread pool initial size. /// - public int? pthreadPoolSize { get; set; } + public int? pthreadPoolInitialSize { get; set; } + + /// + /// Gets or sets pthread pool unused size. + /// + public int? pthreadPoolUnusedSize { get; set; } } public class ResourcesData diff --git a/src/tasks/WasmAppBuilder/WasmAppBuilder.cs b/src/tasks/WasmAppBuilder/WasmAppBuilder.cs index 48ce5d34d04bb..7847039163b11 100644 --- a/src/tasks/WasmAppBuilder/WasmAppBuilder.cs +++ b/src/tasks/WasmAppBuilder/WasmAppBuilder.cs @@ -23,7 +23,8 @@ public class WasmAppBuilder : WasmAppBuilderBaseTask { public ITaskItem[]? RemoteSources { get; set; } public bool IncludeThreadsWorker { get; set; } - public int PThreadPoolSize { get; set; } + public int PThreadPoolInitialSize { get; set; } + public int PThreadPoolUnusedSize { get; set; } public bool UseWebcil { get; set; } public bool WasmIncludeFullIcuData { get; set; } public string? WasmIcuDataFileName { get; set; } @@ -334,13 +335,22 @@ protected override bool ExecuteInternal() var extraConfiguration = new Dictionary(); - if (PThreadPoolSize < -1) + if (PThreadPoolInitialSize < -1) { - throw new LogAsErrorException($"PThreadPoolSize must be -1, 0 or positive, but got {PThreadPoolSize}"); + throw new LogAsErrorException($"PThreadPoolInitialSize must be -1, 0 or positive, but got {PThreadPoolInitialSize}"); } - else if (PThreadPoolSize > -1) + else if (PThreadPoolInitialSize > -1) { - bootConfig.pthreadPoolSize = PThreadPoolSize; + bootConfig.pthreadPoolInitialSize = PThreadPoolInitialSize; + } + + if (PThreadPoolUnusedSize < -1) + { + throw new LogAsErrorException($"PThreadPoolUnusedSize must be -1, 0 or positive, but got {PThreadPoolUnusedSize}"); + } + else if (PThreadPoolUnusedSize > -1) + { + bootConfig.pthreadPoolUnusedSize = PThreadPoolUnusedSize; } foreach (ITaskItem extra in ExtraConfig ?? Enumerable.Empty())