From ea02d94ff84c9b1c836e3fc226fe273a9eab81aa Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Fri, 3 Mar 2023 17:18:55 +0100 Subject: [PATCH 01/45] wip --- src/mono/sample/wasm/browser/main.js | 1 + src/mono/wasm/runtime/assets.ts | 45 +++-------- src/mono/wasm/runtime/dotnet.d.ts | 4 + src/mono/wasm/runtime/icu.ts | 26 +++++++ src/mono/wasm/runtime/imports.ts | 3 +- src/mono/wasm/runtime/managed-exports.ts | 3 +- src/mono/wasm/runtime/marshal.ts | 3 +- src/mono/wasm/runtime/polyfills.ts | 5 +- src/mono/wasm/runtime/run-outer.ts | 13 ++++ src/mono/wasm/runtime/startup.ts | 81 +++++++++++++++----- src/mono/wasm/runtime/storage.ts | 97 ++++++++++++++++++++++++ src/mono/wasm/runtime/strings.ts | 10 +++ src/mono/wasm/runtime/types.ts | 9 +++ 13 files changed, 237 insertions(+), 63 deletions(-) create mode 100644 src/mono/wasm/runtime/storage.ts diff --git a/src/mono/sample/wasm/browser/main.js b/src/mono/sample/wasm/browser/main.js index 27783085b924c..85aa33c9bad3f 100644 --- a/src/mono/sample/wasm/browser/main.js +++ b/src/mono/sample/wasm/browser/main.js @@ -9,6 +9,7 @@ function displayMeaning(meaning) { try { const { setModuleImports } = await dotnet + .withMemoryCache() .withElementOnExit() .create(); diff --git a/src/mono/wasm/runtime/assets.ts b/src/mono/wasm/runtime/assets.ts index 30b43d61c8412..7f0bd68a834f0 100644 --- a/src/mono/wasm/runtime/assets.ts +++ b/src/mono/wasm/runtime/assets.ts @@ -47,35 +47,8 @@ const skipInstantiateByAssetTypes: { "dotnetwasm": true, }; -export function get_preferred_icu_asset(): string | null { - if (!runtimeHelpers.config.assets) - return null; - - // By setting user can define what ICU source file they want to load. - // There is no need to check application's culture when is set. - // If it was not set, then we have 3 "icu" assets in config and we should choose - // only one for loading, the one that matches the application's locale. - const icuAssets = runtimeHelpers.config.assets.filter(a => a["behavior"] == "icu"); - if (icuAssets.length === 1) - return icuAssets[0].name; - - // reads the browsers locale / the OS's locale - const preferredCulture = ENVIRONMENT_IS_WEB ? navigator.language : Intl.DateTimeFormat().resolvedOptions().locale; - const prefix = preferredCulture.split("-")[0]; - const CJK = "icudt_CJK.dat"; - const EFIGS = "icudt_EFIGS.dat"; - const OTHERS = "icudt_no_CJK.dat"; - - // not all "fr-*", "it-*", "de-*", "es-*" are in EFIGS, only the one that is mostly used - if (prefix == "en" || ["fr", "fr-FR", "it", "it-IT", "de", "de-DE", "es", "es-ES"].includes(preferredCulture)) - return EFIGS; - if (["zh", "ko", "ja"].includes(prefix)) - return CJK; - return OTHERS; -} - -export function shouldLoadIcuAsset(asset: AssetEntryInternal, preferredIcuAsset: string | null): boolean { - return !(asset.behavior == "icu" && asset.name != preferredIcuAsset); +export function shouldLoadIcuAsset(asset: AssetEntryInternal): boolean { + return !(asset.behavior == "icu" && asset.name != runtimeHelpers.preferredIcuAsset); } export function resolve_asset_path(behavior: AssetBehaviours) { @@ -87,7 +60,11 @@ export function resolve_asset_path(behavior: AssetBehaviours) { return asset; } export async function mono_download_assets(): Promise { - const preferredIcuAsset = get_preferred_icu_asset(); + if (runtimeHelpers.memoryIsLoaded) { + allAssetsInMemory.promise_control.resolve(); + return; + } + if (runtimeHelpers.diagnosticTracing) console.debug("MONO_WASM: mono_download_assets"); runtimeHelpers.maxParallelDownloads = runtimeHelpers.config.maxParallelDownloads || runtimeHelpers.maxParallelDownloads; runtimeHelpers.enableDownloadRetry = runtimeHelpers.config.enableDownloadRetry || runtimeHelpers.enableDownloadRetry; @@ -102,10 +79,10 @@ export async function mono_download_assets(): Promise { mono_assert(!asset.resolvedUrl || typeof asset.resolvedUrl === "string", "asset resolvedUrl could be string"); mono_assert(!asset.hash || typeof asset.hash === "string", "asset resolvedUrl could be string"); mono_assert(!asset.pendingDownload || typeof asset.pendingDownload === "object", "asset pendingDownload could be object"); - if (!skipInstantiateByAssetTypes[asset.behavior] && shouldLoadIcuAsset(asset, preferredIcuAsset)) { + if (!skipInstantiateByAssetTypes[asset.behavior] && shouldLoadIcuAsset(asset)) { expected_instantiated_assets_count++; } - if (!skipDownloadsByAssetTypes[asset.behavior] && shouldLoadIcuAsset(asset, preferredIcuAsset)) { + if (!skipDownloadsByAssetTypes[asset.behavior] && shouldLoadIcuAsset(asset)) { expected_downloaded_assets_count++; promises_of_assets.push(start_asset_download(asset)); } @@ -133,10 +110,10 @@ export async function mono_download_assets(): Promise { const headersOnly = skipBufferByAssetTypes[asset.behavior]; if (!headersOnly) { mono_assert(asset.isOptional, "Expected asset to have the downloaded buffer"); - if (!skipDownloadsByAssetTypes[asset.behavior] && shouldLoadIcuAsset(asset, preferredIcuAsset)) { + if (!skipDownloadsByAssetTypes[asset.behavior] && shouldLoadIcuAsset(asset)) { expected_downloaded_assets_count--; } - if (!skipInstantiateByAssetTypes[asset.behavior] && shouldLoadIcuAsset(asset, preferredIcuAsset)) { + if (!skipInstantiateByAssetTypes[asset.behavior] && shouldLoadIcuAsset(asset)) { expected_instantiated_assets_count--; } } else { diff --git a/src/mono/wasm/runtime/dotnet.d.ts b/src/mono/wasm/runtime/dotnet.d.ts index d6e84c710f1c4..478059862b989 100644 --- a/src/mono/wasm/runtime/dotnet.d.ts +++ b/src/mono/wasm/runtime/dotnet.d.ts @@ -132,6 +132,10 @@ type MonoConfig = { * initial number of workers to add to the emscripten pthread pool */ pthreadPoolSize?: number; + /** + * If true, the snapshot of runtime's memory will be stored in the browser and used for faster startup next time. + */ + cacheMemory?: boolean; /** * hash of assets */ diff --git a/src/mono/wasm/runtime/icu.ts b/src/mono/wasm/runtime/icu.ts index 4648cadbdf351..69c7cbc179c2f 100644 --- a/src/mono/wasm/runtime/icu.ts +++ b/src/mono/wasm/runtime/icu.ts @@ -50,3 +50,29 @@ export function mono_wasm_globalization_init(): void { } } +export function get_preferred_icu_asset(): string | null { + if (!runtimeHelpers.config.assets) + return null; + + // By setting user can define what ICU source file they want to load. + // There is no need to check application's culture when is set. + // If it was not set, then we have 3 "icu" assets in config and we should choose + // only one for loading, the one that matches the application's locale. + const icuAssets = runtimeHelpers.config.assets.filter(a => a["behavior"] == "icu"); + if (icuAssets.length === 1) + return icuAssets[0].name; + + // reads the browsers locale / the OS's locale + const preferredCulture = ENVIRONMENT_IS_WEB ? navigator.language : Intl.DateTimeFormat().resolvedOptions().locale; + const prefix = preferredCulture.split("-")[0]; + const CJK = "icudt_CJK.dat"; + const EFIGS = "icudt_EFIGS.dat"; + const OTHERS = "icudt_no_CJK.dat"; + + // not all "fr-*", "it-*", "de-*", "es-*" are in EFIGS, only the one that is mostly used + if (prefix == "en" || ["fr", "fr-FR", "it", "it-IT", "de", "de-DE", "es", "es-ES"].includes(preferredCulture)) + return EFIGS; + if (["zh", "ko", "ja"].includes(prefix)) + return CJK; + return OTHERS; +} diff --git a/src/mono/wasm/runtime/imports.ts b/src/mono/wasm/runtime/imports.ts index 1a7af3d7a7c9b..ad4230bada571 100644 --- a/src/mono/wasm/runtime/imports.ts +++ b/src/mono/wasm/runtime/imports.ts @@ -10,6 +10,7 @@ import { EmscriptenModule } from "./types/emscripten"; // these are our public API (except internal) export let Module: EmscriptenModule & DotnetModule; +export let anyModule: any = undefined; export let INTERNAL: any; export let IMPORTS: any; @@ -29,7 +30,7 @@ export function set_imports_exports( ): void { INTERNAL = exports.internal; IMPORTS = exports.marshaled_imports; - Module = exports.module; + anyModule = Module = exports.module; ENVIRONMENT_IS_NODE = imports.isNode; ENVIRONMENT_IS_SHELL = imports.isShell; diff --git a/src/mono/wasm/runtime/managed-exports.ts b/src/mono/wasm/runtime/managed-exports.ts index 65186e4c0b270..7967632df1ff6 100644 --- a/src/mono/wasm/runtime/managed-exports.ts +++ b/src/mono/wasm/runtime/managed-exports.ts @@ -3,14 +3,13 @@ import { GCHandle, MarshalerToCs, MarshalerToJs, MonoMethod, mono_assert } from "./types"; import cwraps from "./cwraps"; -import { Module, runtimeHelpers, ENVIRONMENT_IS_PTHREAD } from "./imports"; +import { runtimeHelpers, ENVIRONMENT_IS_PTHREAD, anyModule } from "./imports"; import { alloc_stack_frame, get_arg, get_arg_gc_handle, MarshalerType, set_arg_type, set_gc_handle } from "./marshal"; import { invoke_method_and_handle_exception } from "./invoke-cs"; import { marshal_array_to_cs_impl, marshal_exception_to_cs, marshal_intptr_to_cs } from "./marshal-to-cs"; import { marshal_int32_to_js, marshal_string_to_js, marshal_task_to_js } from "./marshal-to-js"; export function init_managed_exports(): void { - const anyModule = Module as any; const exports_fqn_asm = "System.Runtime.InteropServices.JavaScript"; runtimeHelpers.runtime_interop_module = cwraps.mono_wasm_assembly_load(exports_fqn_asm); if (!runtimeHelpers.runtime_interop_module) diff --git a/src/mono/wasm/runtime/marshal.ts b/src/mono/wasm/runtime/marshal.ts index 8d3134b867c3e..873449ff52780 100644 --- a/src/mono/wasm/runtime/marshal.ts +++ b/src/mono/wasm/runtime/marshal.ts @@ -2,7 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. import { js_owned_gc_handle_symbol, teardown_managed_proxy } from "./gc-handles"; -import { Module, runtimeHelpers } from "./imports"; +import { anyModule, Module, runtimeHelpers } from "./imports"; import { getF32, getF64, getI16, getI32, getI64Big, getU16, getU32, getU8, setF32, setF64, setI16, setI32, setI64Big, setU16, setU32, setU8 } from "./memory"; import { mono_wasm_new_external_root } from "./roots"; import { mono_assert, GCHandle, JSHandle, MonoObject, MonoString, GCHandleNull, JSMarshalerArguments, JSFunctionSignature, JSMarshalerType, JSMarshalerArgument, MarshalerToJs, MarshalerToCs, WasmRoot } from "./types"; @@ -41,7 +41,6 @@ export const JSMarshalerTypeSize = 32; export const JSMarshalerSignatureHeaderSize = 4 + 4; // without Exception and Result export function alloc_stack_frame(size: number): JSMarshalerArguments { - const anyModule = Module as any; const args = anyModule.stackAlloc(JavaScriptMarshalerArgSize * size); mono_assert(args && (args) % 8 == 0, "Arg alignment"); const exc = get_arg(args, 0); diff --git a/src/mono/wasm/runtime/polyfills.ts b/src/mono/wasm/runtime/polyfills.ts index 50e897a54ded4..71e33b2669e68 100644 --- a/src/mono/wasm/runtime/polyfills.ts +++ b/src/mono/wasm/runtime/polyfills.ts @@ -3,7 +3,7 @@ import BuildConfiguration from "consts:configuration"; import MonoWasmThreads from "consts:monoWasmThreads"; -import { ENVIRONMENT_IS_NODE, ENVIRONMENT_IS_SHELL, ENVIRONMENT_IS_WEB, ENVIRONMENT_IS_WORKER, INTERNAL, Module, runtimeHelpers } from "./imports"; +import { anyModule, ENVIRONMENT_IS_NODE, ENVIRONMENT_IS_SHELL, ENVIRONMENT_IS_WEB, ENVIRONMENT_IS_WORKER, INTERNAL, Module, runtimeHelpers } from "./imports"; import { afterUpdateGlobalBufferAndViews } from "./memory"; import { replaceEmscriptenPThreadLibrary } from "./pthreads/shared/emscripten-replacements"; import { DotnetModuleConfigImports, EarlyReplacements } from "./types"; @@ -13,7 +13,6 @@ let node_fs: any | undefined = undefined; let node_url: any | undefined = undefined; export function init_polyfills(replacements: EarlyReplacements): void { - const anyModule = Module as any; // performance.now() is used by emscripten and doesn't work in JSC if (typeof globalThis.performance === "undefined") { @@ -177,7 +176,7 @@ export function init_polyfills(replacements: EarlyReplacements): void { // memory const originalUpdateGlobalBufferAndViews = replacements.updateGlobalBufferAndViews; - replacements.updateGlobalBufferAndViews = (buffer: ArrayBufferLike) => { + runtimeHelpers.updateGlobalBufferAndViews = replacements.updateGlobalBufferAndViews = (buffer: ArrayBufferLike) => { originalUpdateGlobalBufferAndViews(buffer); afterUpdateGlobalBufferAndViews(buffer); }; diff --git a/src/mono/wasm/runtime/run-outer.ts b/src/mono/wasm/runtime/run-outer.ts index 7f6902e2d9712..cbc5cb653b173 100644 --- a/src/mono/wasm/runtime/run-outer.ts +++ b/src/mono/wasm/runtime/run-outer.ts @@ -137,6 +137,19 @@ class HostBuilder implements DotnetHostBuilder { } } + withMemoryCache(value: boolean): DotnetHostBuilder { + try { + const configInternal: MonoConfigInternal = { + cacheMemory: value + }; + Object.assign(this.moduleConfig.config!, configInternal); + return this; + } catch (err) { + mono_exit(1, err); + throw err; + } + } + withConfig(config: MonoConfig): DotnetHostBuilder { try { const providedConfig = { ...config }; diff --git a/src/mono/wasm/runtime/startup.ts b/src/mono/wasm/runtime/startup.ts index 8f5f30a4b1f2e..ba887c337d05e 100644 --- a/src/mono/wasm/runtime/startup.ts +++ b/src/mono/wasm/runtime/startup.ts @@ -4,10 +4,10 @@ import BuildConfiguration from "consts:configuration"; import MonoWasmThreads from "consts:monoWasmThreads"; import { CharPtrNull, DotnetModule, RuntimeAPI, MonoConfig, MonoConfigInternal } from "./types"; -import { ENVIRONMENT_IS_NODE, ENVIRONMENT_IS_SHELL, INTERNAL, Module, runtimeHelpers } from "./imports"; +import { anyModule, ENVIRONMENT_IS_NODE, ENVIRONMENT_IS_SHELL, INTERNAL, Module, runtimeHelpers } from "./imports"; import cwraps, { init_c_exports } from "./cwraps"; import { mono_wasm_raise_debug_event, mono_wasm_runtime_ready } from "./debug"; -import { mono_wasm_globalization_init } from "./icu"; +import { get_preferred_icu_asset, mono_wasm_globalization_init } from "./icu"; import { toBase64StringImpl } from "./base64"; import { mono_wasm_init_aot_profiler, mono_wasm_init_browser_profiler } from "./profiler"; import { mono_on_abort, mono_exit } from "./run"; @@ -16,7 +16,7 @@ import { initialize_marshalers_to_js } from "./marshal-to-js"; import { init_polyfills_async } from "./polyfills"; import * as pthreads_worker from "./pthreads/worker"; import { createPromiseController } from "./promise-controller"; -import { string_decoder } from "./strings"; +import { get_hash_code, string_decoder } from "./strings"; import { init_managed_exports } from "./managed-exports"; import { init_legacy_exports } from "./net6-legacy/corebindings"; import { cwraps_internal } from "./exports-internal"; @@ -29,11 +29,13 @@ import { mono_wasm_init_diagnostics } from "./diagnostics"; import { preAllocatePThreadWorkerPool, instantiateWasmPThreadWorkerPool } from "./pthreads/browser"; import { export_linker } from "./exports-linker"; import { endMeasure, MeasuredBlock, startMeasure } from "./profiler"; +import { getMemory, storeMemory } from "./storage"; let config: MonoConfigInternal = undefined as any; let configLoaded = false; export const dotnetReady = createPromiseController(); export const afterConfigLoaded = createPromiseController(); +export const beforeInstantiateWasm = createPromiseController(); export const afterInstantiateWasm = createPromiseController(); export const beforePreInit = createPromiseController(); export const afterPreInit = createPromiseController(); @@ -103,6 +105,7 @@ function instantiateWasm( const mark = startMeasure(); if (userInstantiateWasm) { + beforeInstantiateWasm.promise_control.resolve(); const exports = userInstantiateWasm(imports, (instance: WebAssembly.Instance, module: WebAssembly.Module | undefined) => { endMeasure(mark, MeasuredBlock.instantiateWasm); afterInstantiateWasm.promise_control.resolve(); @@ -122,7 +125,6 @@ async function instantiateWasmWorker( // 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()); @@ -280,6 +282,7 @@ async function postRunAsync(userpostRun: (() => void)[]) { export function abort_startup(reason: any, should_exit: boolean): void { if (runtimeHelpers.diagnosticTracing) console.trace("MONO_WASM: abort_startup"); dotnetReady.promise_control.reject(reason); + beforeInstantiateWasm.promise_control.reject(reason); afterInstantiateWasm.promise_control.reject(reason); beforePreInit.promise_control.reject(reason); afterPreInit.promise_control.reject(reason); @@ -334,6 +337,7 @@ async function mono_wasm_pre_init_full(): Promise { if (runtimeHelpers.diagnosticTracing) console.debug("MONO_WASM: mono_wasm_pre_init_full"); Module.addRunDependency("mono_wasm_pre_init_full"); + await beforeInstantiateWasm.promise; await mono_download_assets(); Module.removeRunDependency("mono_wasm_pre_init_full"); @@ -348,6 +352,10 @@ async function mono_wasm_before_user_runtime_initialized(): Promise { mono_wasm_globalization_init(); if (!runtimeHelpers.mono_wasm_load_runtime_done) mono_wasm_load_runtime("unused", config.debugLevel); + if (runtimeHelpers.config.cacheMemory && !runtimeHelpers.memoryIsLoaded) { + await storeMemory(runtimeHelpers.configurationHash, Module.HEAP8.buffer); + } + bindings_init(); if (!runtimeHelpers.mono_wasm_runtime_is_ready) mono_wasm_runtime_ready(); if (!runtimeHelpers.mono_wasm_symbols_are_ready) readSymbolMapFile("dotnet.js.symbols"); @@ -457,20 +465,37 @@ async function instantiate_wasm_module( ): Promise { // this is called so early that even Module exports like addRunDependency don't exist yet try { - replace_linker_placeholders(imports, export_linker()); + let memoryBytes: ArrayBuffer | undefined = undefined; await mono_wasm_load_config(Module.configSrc); if (runtimeHelpers.diagnosticTracing) console.debug("MONO_WASM: instantiate_wasm_module"); const assetToLoad = resolve_asset_path("dotnetwasm"); + + if (runtimeHelpers.config.cacheMemory && config.assetsHash) { + memoryBytes = await getMemory(runtimeHelpers.configurationHash); + runtimeHelpers.memoryIsLoaded = !!memoryBytes; + } + beforeInstantiateWasm.promise_control.resolve(); + // FIXME: this would not apply re-try (on connection reset during download) for dotnet.wasm because we could not download the buffer before we pass it to instantiate_wasm_asset await start_asset_download(assetToLoad); await beforePreInit.promise; Module.addRunDependency("instantiate_wasm_module"); + + replace_linker_placeholders(imports, export_linker()); await instantiate_wasm_asset(assetToLoad, imports, successCallback); assetToLoad.pendingDownloadInternal = null as any; // GC assetToLoad.pendingDownload = null as any; // GC assetToLoad.buffer = null as any; // GC - if (runtimeHelpers.diagnosticTracing) console.debug("MONO_WASM: instantiate_wasm_module done"); + + if (runtimeHelpers.memoryIsLoaded) { + const wasmMemory = anyModule.asm.memory; + // .grow() takes a delta compared to the previous size + wasmMemory.grow((memoryBytes!.byteLength - wasmMemory.buffer.byteLength + 65535) >>> 16); + runtimeHelpers.updateGlobalBufferAndViews(wasmMemory.buffer); + Module.HEAP8.set(new Int8Array(memoryBytes!), 0); + if (runtimeHelpers.diagnosticTracing) console.debug("MONO_WASM: Loaded memory from cache"); + } afterInstantiateWasm.promise_control.resolve(); } catch (err) { _print_error("MONO_WASM: instantiate_wasm_module() failed", err); @@ -482,13 +507,6 @@ async function instantiate_wasm_module( // runs just in non-blazor async function _apply_configuration_from_args() { - try { - const tz = Intl.DateTimeFormat().resolvedOptions().timeZone; - if (tz) mono_wasm_setenv("TZ", tz); - } catch { - console.info("MONO_WASM: failed to detect timezone, will fallback to UTC"); - } - // create /usr/share folder which is SpecialFolder.CommonApplicationData Module["FS_createPath"]("/", "usr", true, true); Module["FS_createPath"]("/", "usr/share", true, true); @@ -524,17 +542,18 @@ export function mono_wasm_load_runtime(unused?: string, debugLevel?: number): vo runtimeHelpers.mono_wasm_load_runtime_done = true; try { const mark = startMeasure(); - if (debugLevel == undefined) { - debugLevel = 0; - if (config && config.debugLevel) { - debugLevel = 0 + debugLevel; + if (!runtimeHelpers.memoryIsLoaded) { + if (debugLevel == undefined) { + debugLevel = 0; + if (config && config.debugLevel) { + debugLevel = 0 + debugLevel; + } } + cwraps.mono_wasm_load_runtime(unused || "unused", debugLevel); } - cwraps.mono_wasm_load_runtime(unused || "unused", debugLevel); endMeasure(mark, MeasuredBlock.loadRuntime); - runtimeHelpers.waitForDebugger = config.waitForDebugger; - if (!runtimeHelpers.mono_wasm_bindings_is_ready) bindings_init(); + if (!runtimeHelpers.config.cacheMemory) bindings_init(); } catch (err: any) { _print_error("MONO_WASM: mono_wasm_load_runtime () failed", err); @@ -548,11 +567,12 @@ export function mono_wasm_load_runtime(unused?: string, debugLevel?: number): vo } export function bindings_init(): void { - if (runtimeHelpers.diagnosticTracing) console.debug("MONO_WASM: bindings_init"); if (runtimeHelpers.mono_wasm_bindings_is_ready) { return; } + if (runtimeHelpers.diagnosticTracing) console.debug("MONO_WASM: bindings_init"); runtimeHelpers.mono_wasm_bindings_is_ready = true; + runtimeHelpers.waitForDebugger = config.waitForDebugger; try { const mark = startMeasure(); init_managed_exports(); @@ -639,6 +659,25 @@ function normalizeConfig() { runtimeHelpers.enablePerfMeasure = !!config.browserProfilerOptions && globalThis.performance && typeof globalThis.performance.measure === "function"; + runtimeHelpers.preferredIcuAsset = get_preferred_icu_asset(); + + if (runtimeHelpers.timezone === undefined && config.environmentVariables["TZ"] === undefined) { + try { + runtimeHelpers.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || null; + if (runtimeHelpers.timezone) config.environmentVariables["TZ"] = runtimeHelpers.timezone; + } catch { + console.info("MONO_WASM: failed to detect timezone, will fallback to UTC"); + } + } + + if (config.cacheMemory) { + // calculate hash of things which affect the memory snapshot + const configCopy = Object.assign({}, config) as any; + configCopy.assets = null; // we have config.assetsHash for this + configCopy.preferredIcuAsset = runtimeHelpers.preferredIcuAsset; + configCopy.timezone = runtimeHelpers.timezone; + runtimeHelpers.configurationHash = get_hash_code(JSON.stringify(configCopy)); + } } diff --git a/src/mono/wasm/runtime/storage.ts b/src/mono/wasm/runtime/storage.ts new file mode 100644 index 0000000000000..1b69174fc28e0 --- /dev/null +++ b/src/mono/wasm/runtime/storage.ts @@ -0,0 +1,97 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import ProductVersion from "consts:productVersion"; +import GitHash from "consts:gitHash"; + +// adapted from Blazor's WebAssemblyResourceLoader.ts +async function open(): Promise { + // caches will be undefined if we're running on an insecure origin (secure means https or localhost) + if (typeof globalThis.caches === "undefined") { + return null; + } + + // cache integrity is compromised if the first request has been served over http (except localhost) + // in this case, we want to disable caching and integrity validation + if (ENVIRONMENT_IS_WEB && globalThis.window.isSecureContext === false) { + return null; + } + + // Define a separate cache for each base href, so we're isolated from any other + // Blazor application running on the same origin. We need this so that we're free + // to purge from the cache anything we're not using and don't let it keep growing, + // since we don't want to be worst offenders for space usage. + const relativeBaseHref = document.baseURI.substring(document.location.origin.length); + const cacheName = `dotnet-resources${relativeBaseHref}`; + + try { + // There's a Chromium bug we need to be aware of here: the CacheStorage APIs say that when + // caches.open(name) returns a promise that succeeds, the value is meant to be a Cache instance. + // However, if the browser was launched with a --user-data-dir param that's "too long" in some sense, + // then even through the promise resolves as success, the value given is `undefined`. + // See https://stackoverflow.com/a/46626574 and https://bugs.chromium.org/p/chromium/issues/detail?id=1054541 + // If we see this happening, return "null" to mean "proceed without caching". + return (await globalThis.caches.open(cacheName)) || null; + } catch { + // There's no known scenario where we should get an exception here, but considering the + // Chromium bug above, let's tolerate it and treat as "proceed without caching". + return null; + } +} +const memoryPrefix = "https://dotnet.generated.invalid/wasm-memory"; +export async function getMemory(configurationHash: number): Promise { + try { + const cacheKey = `${memoryPrefix}-${ProductVersion}-${GitHash}-${configurationHash}`; + const cache = await open(); + if (!cache) { + return undefined; + } + const res = await cache.match(cacheKey); + if (!res) { + return undefined; + } + return res.arrayBuffer(); + } catch (ex) { + return undefined; + } +} + +export async function storeMemory(configurationHash: number, memory: ArrayBuffer) { + try { + const cacheKey = `${memoryPrefix}-${ProductVersion}-${GitHash}-${configurationHash}`; + const cache = await open(); + if (!cache) { + return; + } + + const responseToCache = new Response(memory, { + headers: { + "content-type": "wasm-memory", + "content-length": memory.byteLength.toString(), + }, + }); + + await cache.put(cacheKey, responseToCache); + + cleanupMemory(cacheKey); // no await + } catch (ex) { + return; + } +} + +export async function cleanupMemory(protectKey: string) { + try { + const cache = await open(); + if (!cache) { + return; + } + const items = await cache.keys(); + for (const item of items) { + if (item.url && item.url !== protectKey && item.url.startsWith(memoryPrefix)) { + await cache.delete(item); + } + } + } catch (ex) { + return; + } +} diff --git a/src/mono/wasm/runtime/strings.ts b/src/mono/wasm/runtime/strings.ts index 8eba930124db5..0d53a2fef3dd6 100644 --- a/src/mono/wasm/runtime/strings.ts +++ b/src/mono/wasm/runtime/strings.ts @@ -276,3 +276,13 @@ export function js_string_to_mono_string_new(string: string): MonoString { temp.release(); } } + +export function get_hash_code(string: string) { + let hash = 0; + for (let i = 0, len = string.length; i < len; i++) { + const chr = string.charCodeAt(i); + hash = (hash << 5) - hash + chr; + hash |= 0; + } + return hash; +} \ No newline at end of file diff --git a/src/mono/wasm/runtime/types.ts b/src/mono/wasm/runtime/types.ts index 5896e9736f924..224a0fc2f2583 100644 --- a/src/mono/wasm/runtime/types.ts +++ b/src/mono/wasm/runtime/types.ts @@ -119,6 +119,10 @@ export type MonoConfig = { * initial number of workers to add to the emscripten pthread pool */ pthreadPoolSize?: number, + /** + * If true, the snapshot of runtime's memory will be stored in the browser and used for faster startup next time. Default is true. + */ + cacheMemory?: boolean, /** * hash of assets */ @@ -228,6 +232,11 @@ export type RuntimeHelpers = { quit: Function, locateFile: (path: string, prefix?: string) => string, javaScriptExports: JavaScriptExports, + memoryIsLoaded: boolean, + configurationHash: number, + preferredIcuAsset: string | null, + timezone: string | null, + updateGlobalBufferAndViews: (buffer: ArrayBufferLike) => void } export type GlobalizationMode = From c1aa6c3f6b6120442dc00f86a22e4f2390c87beb Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Fri, 3 Mar 2023 17:29:07 +0100 Subject: [PATCH 02/45] wip --- src/mono/sample/wasm/browser/main.js | 1 - src/mono/wasm/runtime/run-outer.ts | 1 + src/mono/wasm/runtime/startup.ts | 15 ++++++++------- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/mono/sample/wasm/browser/main.js b/src/mono/sample/wasm/browser/main.js index 85aa33c9bad3f..27783085b924c 100644 --- a/src/mono/sample/wasm/browser/main.js +++ b/src/mono/sample/wasm/browser/main.js @@ -9,7 +9,6 @@ function displayMeaning(meaning) { try { const { setModuleImports } = await dotnet - .withMemoryCache() .withElementOnExit() .create(); diff --git a/src/mono/wasm/runtime/run-outer.ts b/src/mono/wasm/runtime/run-outer.ts index cbc5cb653b173..dd4be9b379e16 100644 --- a/src/mono/wasm/runtime/run-outer.ts +++ b/src/mono/wasm/runtime/run-outer.ts @@ -18,6 +18,7 @@ export interface DotnetHostBuilder { withDebugging(level: number): DotnetHostBuilder withMainAssembly(mainAssemblyName: string): DotnetHostBuilder withApplicationArgumentsFromQuery(): DotnetHostBuilder + withMemoryCache(value: boolean): DotnetHostBuilder create(): Promise run(): Promise } diff --git a/src/mono/wasm/runtime/startup.ts b/src/mono/wasm/runtime/startup.ts index ba887c337d05e..c38df52f77db8 100644 --- a/src/mono/wasm/runtime/startup.ts +++ b/src/mono/wasm/runtime/startup.ts @@ -352,7 +352,7 @@ async function mono_wasm_before_user_runtime_initialized(): Promise { mono_wasm_globalization_init(); if (!runtimeHelpers.mono_wasm_load_runtime_done) mono_wasm_load_runtime("unused", config.debugLevel); - if (runtimeHelpers.config.cacheMemory && !runtimeHelpers.memoryIsLoaded) { + if (config.cacheMemory && !runtimeHelpers.memoryIsLoaded) { await storeMemory(runtimeHelpers.configurationHash, Module.HEAP8.buffer); } bindings_init(); @@ -470,7 +470,7 @@ async function instantiate_wasm_module( if (runtimeHelpers.diagnosticTracing) console.debug("MONO_WASM: instantiate_wasm_module"); const assetToLoad = resolve_asset_path("dotnetwasm"); - if (runtimeHelpers.config.cacheMemory && config.assetsHash) { + if (config.cacheMemory && config.assetsHash) { memoryBytes = await getMemory(runtimeHelpers.configurationHash); runtimeHelpers.memoryIsLoaded = !!memoryBytes; } @@ -553,7 +553,7 @@ export function mono_wasm_load_runtime(unused?: string, debugLevel?: number): vo } endMeasure(mark, MeasuredBlock.loadRuntime); - if (!runtimeHelpers.config.cacheMemory) bindings_init(); + if (!config.cacheMemory) bindings_init(); } catch (err: any) { _print_error("MONO_WASM: mono_wasm_load_runtime () failed", err); @@ -602,7 +602,7 @@ export async function mono_wasm_load_config(configFilePath?: string): PromiseruntimeHelpers.config); + await Module.onConfigLoaded(config); normalizeConfig(); } catch (err: any) { @@ -631,7 +631,7 @@ export async function mono_wasm_load_config(configFilePath?: string): Promise Date: Fri, 3 Mar 2023 17:32:07 +0100 Subject: [PATCH 03/45] wip --- src/mono/wasm/runtime/dotnet.d.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/mono/wasm/runtime/dotnet.d.ts b/src/mono/wasm/runtime/dotnet.d.ts index 478059862b989..e6837f4b0ec6e 100644 --- a/src/mono/wasm/runtime/dotnet.d.ts +++ b/src/mono/wasm/runtime/dotnet.d.ts @@ -18,6 +18,7 @@ interface DotnetHostBuilder { withDebugging(level: number): DotnetHostBuilder; withMainAssembly(mainAssemblyName: string): DotnetHostBuilder; withApplicationArgumentsFromQuery(): DotnetHostBuilder; + withMemoryCache(value: boolean): DotnetHostBuilder; create(): Promise; run(): Promise; } @@ -133,7 +134,7 @@ type MonoConfig = { */ pthreadPoolSize?: number; /** - * If true, the snapshot of runtime's memory will be stored in the browser and used for faster startup next time. + * If true, the snapshot of runtime's memory will be stored in the browser and used for faster startup next time. Default is true. */ cacheMemory?: boolean; /** From 3fe9aba209bf71a85e14e12f8f4b3b802b912a68 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Fri, 3 Mar 2023 23:57:21 +0100 Subject: [PATCH 04/45] wip --- src/mono/wasm/runtime/polyfills.ts | 1 + src/mono/wasm/runtime/startup.ts | 16 +++---------- src/mono/wasm/runtime/storage.ts | 37 ++++++++++++++++++++++++++---- src/mono/wasm/runtime/strings.ts | 10 -------- src/mono/wasm/runtime/types.ts | 2 +- 5 files changed, 38 insertions(+), 28 deletions(-) diff --git a/src/mono/wasm/runtime/polyfills.ts b/src/mono/wasm/runtime/polyfills.ts index 71e33b2669e68..a24943946f09b 100644 --- a/src/mono/wasm/runtime/polyfills.ts +++ b/src/mono/wasm/runtime/polyfills.ts @@ -216,6 +216,7 @@ export async function init_polyfills_async(): Promise { }; } } + runtimeHelpers.subtle = globalThis.crypto.subtle || null; } } diff --git a/src/mono/wasm/runtime/startup.ts b/src/mono/wasm/runtime/startup.ts index a6df84fa549c5..5daa43047788b 100644 --- a/src/mono/wasm/runtime/startup.ts +++ b/src/mono/wasm/runtime/startup.ts @@ -17,7 +17,7 @@ import { initialize_marshalers_to_js } from "./marshal-to-js"; import { init_polyfills_async } from "./polyfills"; import * as pthreads_worker from "./pthreads/worker"; import { createPromiseController } from "./promise-controller"; -import { get_hash_code, string_decoder } from "./strings"; +import { string_decoder } from "./strings"; import { init_managed_exports } from "./managed-exports"; import { cwraps_internal } from "./exports-internal"; import { CharPtr, InstantiateWasmCallBack, InstantiateWasmSuccessCallback } from "./types/emscripten"; @@ -357,7 +357,7 @@ async function mono_wasm_before_user_runtime_initialized(): Promise { if (!runtimeHelpers.mono_wasm_load_runtime_done) mono_wasm_load_runtime("unused", config.debugLevel); if (config.cacheMemory && !runtimeHelpers.memoryIsLoaded) { - await storeMemory(runtimeHelpers.configurationHash, Module.HEAP8.buffer); + await storeMemory(Module.HEAP8.buffer); } bindings_init(); if (!runtimeHelpers.mono_wasm_runtime_is_ready) mono_wasm_runtime_ready(); @@ -475,7 +475,7 @@ async function instantiate_wasm_module( const assetToLoad = resolve_asset_path("dotnetwasm"); if (config.cacheMemory && config.assetsHash) { - memoryBytes = await getMemory(runtimeHelpers.configurationHash); + memoryBytes = await getMemory(); runtimeHelpers.memoryIsLoaded = !!memoryBytes; } beforeInstantiateWasm.promise_control.resolve(); @@ -676,18 +676,8 @@ function normalizeConfig() { console.info("MONO_WASM: failed to detect timezone, will fallback to UTC"); } } - - if (config.cacheMemory) { - // calculate hash of things which affect the memory snapshot - const configCopy = Object.assign({}, config) as any; - configCopy.assets = null; // we have config.assetsHash for this - configCopy.preferredIcuAsset = runtimeHelpers.preferredIcuAsset; - configCopy.timezone = runtimeHelpers.timezone; - runtimeHelpers.configurationHash = get_hash_code(JSON.stringify(configCopy)); - } } - 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) diff --git a/src/mono/wasm/runtime/storage.ts b/src/mono/wasm/runtime/storage.ts index 1b69174fc28e0..de2c3575272a3 100644 --- a/src/mono/wasm/runtime/storage.ts +++ b/src/mono/wasm/runtime/storage.ts @@ -3,6 +3,7 @@ import ProductVersion from "consts:productVersion"; import GitHash from "consts:gitHash"; +import { runtimeHelpers } from "./imports"; // adapted from Blazor's WebAssemblyResourceLoader.ts async function open(): Promise { @@ -39,9 +40,13 @@ async function open(): Promise { } } const memoryPrefix = "https://dotnet.generated.invalid/wasm-memory"; -export async function getMemory(configurationHash: number): Promise { +export async function getMemory(): Promise { try { - const cacheKey = `${memoryPrefix}-${ProductVersion}-${GitHash}-${configurationHash}`; + const inputsHash = await getInputsHash(); + if (!inputsHash) { + return undefined; + } + const cacheKey = `${memoryPrefix}-${ProductVersion}-${GitHash}-${inputsHash}`; const cache = await open(); if (!cache) { return undefined; @@ -56,9 +61,13 @@ export async function getMemory(configurationHash: number): Promise { + if (!runtimeHelpers.subtle) { + return null; + } + const inputs = Object.assign({}, runtimeHelpers.config) as any; + // above already has env variables, runtime options, etc + // above also already has config.assetsHash for this. It has all the asserts (DLLs, ICU, .wasms, etc). + // So we could remove assets collectionfrom the hash. + inputs.assets = null; + // some things are calculated at runtime, so we need to add them to the hash + inputs.preferredIcuAsset = runtimeHelpers.preferredIcuAsset; + inputs.timezone = runtimeHelpers.timezone; + const inputsJson = JSON.stringify(inputs); + const sha256Buffer = await runtimeHelpers.subtle.digest("SHA-256", new TextEncoder().encode(inputsJson)); + const uint8ViewOfHash = new Uint8Array(sha256Buffer); + const hashAsString = Array.from(uint8ViewOfHash).map((b) => b.toString(16).padStart(2, "0")).join(""); + return hashAsString; +} \ No newline at end of file diff --git a/src/mono/wasm/runtime/strings.ts b/src/mono/wasm/runtime/strings.ts index 0d53a2fef3dd6..e9a6fe78e7b6f 100644 --- a/src/mono/wasm/runtime/strings.ts +++ b/src/mono/wasm/runtime/strings.ts @@ -275,14 +275,4 @@ export function js_string_to_mono_string_new(string: string): MonoString { } finally { temp.release(); } -} - -export function get_hash_code(string: string) { - let hash = 0; - for (let i = 0, len = string.length; i < len; i++) { - const chr = string.charCodeAt(i); - hash = (hash << 5) - hash + chr; - hash |= 0; - } - return hash; } \ No newline at end of file diff --git a/src/mono/wasm/runtime/types.ts b/src/mono/wasm/runtime/types.ts index 1ec28fd4296e4..6c680248841db 100644 --- a/src/mono/wasm/runtime/types.ts +++ b/src/mono/wasm/runtime/types.ts @@ -234,7 +234,7 @@ export type RuntimeHelpers = { javaScriptExports: JavaScriptExports, loadedFiles: string[], memoryIsLoaded: boolean, - configurationHash: number, + subtle: SubtleCrypto | null, preferredIcuAsset: string | null, timezone: string | null, updateGlobalBufferAndViews: (buffer: ArrayBufferLike) => void From d2e5b49e44e5e66a05a5d97d087b58069fc66ae7 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Sat, 4 Mar 2023 00:17:17 +0100 Subject: [PATCH 05/45] fix --- src/mono/wasm/runtime/polyfills.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mono/wasm/runtime/polyfills.ts b/src/mono/wasm/runtime/polyfills.ts index a24943946f09b..9d43364deff7d 100644 --- a/src/mono/wasm/runtime/polyfills.ts +++ b/src/mono/wasm/runtime/polyfills.ts @@ -216,8 +216,8 @@ export async function init_polyfills_async(): Promise { }; } } - runtimeHelpers.subtle = globalThis.crypto.subtle || null; } + runtimeHelpers.subtle = globalThis.crypto?.subtle; } const dummyPerformance = { From e695c421f4ff4950697b9434bd5025830f10d858 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Mon, 6 Mar 2023 16:40:52 +0100 Subject: [PATCH 06/45] feedback --- src/mono/wasm/memory-snapshot.md | 27 +++++++++++++++++++++++++++ src/mono/wasm/runtime/startup.ts | 23 ++++------------------- src/mono/wasm/runtime/storage.ts | 7 +++++-- src/mono/wasm/runtime/strings.ts | 2 +- 4 files changed, 37 insertions(+), 22 deletions(-) create mode 100644 src/mono/wasm/memory-snapshot.md diff --git a/src/mono/wasm/memory-snapshot.md b/src/mono/wasm/memory-snapshot.md new file mode 100644 index 0000000000000..15e3782e1b04d --- /dev/null +++ b/src/mono/wasm/memory-snapshot.md @@ -0,0 +1,27 @@ +# Memory snapshot of the Mono runtime # + +We take snapshot of WASM memory after first cold start at run time​. +We store it on the client side in the browser cache. +For subsequent start of the application with the same configuration and same assets, we could use the snapshot. +Instead of downloading all the assets and doing the runtime startup again. +Such subsequent start is significantly faster. + +### Implementation details + +- the consistency of inputs (configuration and assets) with the snapshot is done by calculating SHA256 of the inputs. + - the DLLs and other downloaded assets each have SHA256 which is used to validate 'integrity' by the browser. + - the mono-config has field `assetsHash` which is summary SHA256 of all the assets. + - the configuration could be changed programmatically and se we calculate the hash at the runtime, just before taking snapshot. +- the snapshot is taken just after we initialize the Mono runtime + - cwraps initialized (they would be initialized again on subsequent start) + - after loading all the DLLs into memory. + - after loading ICU and timezone data + - after applying environment variables and other runtime options. + - before any worker threads init + - before any JavaScript interop initialization + - before any Managed code initialization + - therefore we do not expect to store any application state in the snapshot. + +### How to opt out + - you can use new API `withMemoryCache(false)` to opt out from the feature. + diff --git a/src/mono/wasm/runtime/startup.ts b/src/mono/wasm/runtime/startup.ts index 5daa43047788b..6910054ee6fa0 100644 --- a/src/mono/wasm/runtime/startup.ts +++ b/src/mono/wasm/runtime/startup.ts @@ -298,7 +298,6 @@ export function abort_startup(reason: any, should_exit: boolean): void { } } -// runs in both blazor and non-blazor function mono_wasm_pre_init_essential(isWorker: boolean): void { if (!isWorker) Module.addRunDependency("mono_wasm_pre_init_essential"); @@ -321,7 +320,6 @@ function mono_wasm_pre_init_essential(isWorker: boolean): void { Module.removeRunDependency("mono_wasm_pre_init_essential"); } -// runs in both blazor and non-blazor async function mono_wasm_pre_init_essential_async(): Promise { if (runtimeHelpers.diagnosticTracing) console.debug("MONO_WASM: mono_wasm_pre_init_essential_async"); Module.addRunDependency("mono_wasm_pre_init_essential_async"); @@ -329,14 +327,9 @@ async function mono_wasm_pre_init_essential_async(): Promise { await init_polyfills_async(); await mono_wasm_load_config(Module.configSrc); - if (MonoWasmThreads) { - preAllocatePThreadWorkerPool(MONO_PTHREAD_POOL_SIZE, config); - } - Module.removeRunDependency("mono_wasm_pre_init_essential_async"); } -// runs just in non-blazor async function mono_wasm_pre_init_full(): Promise { if (runtimeHelpers.diagnosticTracing) console.debug("MONO_WASM: mono_wasm_pre_init_full"); Module.addRunDependency("mono_wasm_pre_init_full"); @@ -347,7 +340,6 @@ async function mono_wasm_pre_init_full(): Promise { Module.removeRunDependency("mono_wasm_pre_init_full"); } -// runs just in non-blazor async function mono_wasm_before_user_runtime_initialized(): Promise { if (runtimeHelpers.diagnosticTracing) console.debug("MONO_WASM: mono_wasm_before_user_runtime_initialized"); @@ -359,6 +351,10 @@ async function mono_wasm_before_user_runtime_initialized(): Promise { if (config.cacheMemory && !runtimeHelpers.memoryIsLoaded) { await storeMemory(Module.HEAP8.buffer); } + if (MonoWasmThreads) { + preAllocatePThreadWorkerPool(MONO_PTHREAD_POOL_SIZE, config); + await mono_wasm_init_diagnostics(); + } bindings_init(); if (!runtimeHelpers.mono_wasm_runtime_is_ready) mono_wasm_runtime_ready(); if (!runtimeHelpers.mono_wasm_symbols_are_ready) readSymbolMapFile("dotnet.js.symbols"); @@ -373,7 +369,6 @@ async function mono_wasm_before_user_runtime_initialized(): Promise { } } -// runs in both blazor and non-blazor async function mono_wasm_after_user_runtime_initialized(): Promise { if (runtimeHelpers.diagnosticTracing) console.debug("MONO_WASM: mono_wasm_after_user_runtime_initialized"); try { @@ -394,10 +389,6 @@ async function mono_wasm_after_user_runtime_initialized(): Promise { } } } - // for Blazor, init diagnostics after their "onRuntimeInitalized" sets env variables, but before their postRun callback (which calls mono_wasm_load_runtime) - if (MonoWasmThreads) { - await mono_wasm_init_diagnostics(); - } if (runtimeHelpers.diagnosticTracing) console.debug("MONO_WASM: Initializing mono runtime"); @@ -509,7 +500,6 @@ async function instantiate_wasm_module( Module.removeRunDependency("instantiate_wasm_module"); } -// runs just in non-blazor async function _apply_configuration_from_args() { // create /usr/share folder which is SpecialFolder.CommonApplicationData Module["FS_createPath"]("/", "usr", true, true); @@ -531,11 +521,6 @@ async function _apply_configuration_from_args() { if (config.browserProfilerOptions) mono_wasm_init_browser_profiler(config.browserProfilerOptions); - - // for non-Blazor, init diagnostics after environment variables are set - if (MonoWasmThreads) { - await mono_wasm_init_diagnostics(); - } } export function mono_wasm_load_runtime(unused?: string, debugLevel?: number): void { diff --git a/src/mono/wasm/runtime/storage.ts b/src/mono/wasm/runtime/storage.ts index de2c3575272a3..501fbc0b0bef6 100644 --- a/src/mono/wasm/runtime/storage.ts +++ b/src/mono/wasm/runtime/storage.ts @@ -5,6 +5,8 @@ import ProductVersion from "consts:productVersion"; import GitHash from "consts:gitHash"; import { runtimeHelpers } from "./imports"; +const memoryPrefix = "https://dotnet.generated.invalid/wasm-memory"; + // adapted from Blazor's WebAssemblyResourceLoader.ts async function open(): Promise { // caches will be undefined if we're running on an insecure origin (secure means https or localhost) @@ -36,10 +38,11 @@ async function open(): Promise { } catch { // There's no known scenario where we should get an exception here, but considering the // Chromium bug above, let's tolerate it and treat as "proceed without caching". + console.warn("MONO_WASM: Failed to open cache"); return null; } } -const memoryPrefix = "https://dotnet.generated.invalid/wasm-memory"; + export async function getMemory(): Promise { try { const inputsHash = await getInputsHash(); @@ -123,4 +126,4 @@ export async function getInputsHash(): Promise { const uint8ViewOfHash = new Uint8Array(sha256Buffer); const hashAsString = Array.from(uint8ViewOfHash).map((b) => b.toString(16).padStart(2, "0")).join(""); return hashAsString; -} \ No newline at end of file +} diff --git a/src/mono/wasm/runtime/strings.ts b/src/mono/wasm/runtime/strings.ts index e9a6fe78e7b6f..8eba930124db5 100644 --- a/src/mono/wasm/runtime/strings.ts +++ b/src/mono/wasm/runtime/strings.ts @@ -275,4 +275,4 @@ export function js_string_to_mono_string_new(string: string): MonoString { } finally { temp.release(); } -} \ No newline at end of file +} From 11890152304fa21fcdf0ef5ba5c912db827abae2 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Mon, 6 Mar 2023 16:46:52 +0100 Subject: [PATCH 07/45] lint --- src/mono/wasm/memory-snapshot.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mono/wasm/memory-snapshot.md b/src/mono/wasm/memory-snapshot.md index 15e3782e1b04d..f75a65260ec8b 100644 --- a/src/mono/wasm/memory-snapshot.md +++ b/src/mono/wasm/memory-snapshot.md @@ -11,7 +11,7 @@ Such subsequent start is significantly faster. - the consistency of inputs (configuration and assets) with the snapshot is done by calculating SHA256 of the inputs. - the DLLs and other downloaded assets each have SHA256 which is used to validate 'integrity' by the browser. - the mono-config has field `assetsHash` which is summary SHA256 of all the assets. - - the configuration could be changed programmatically and se we calculate the hash at the runtime, just before taking snapshot. + - the configuration could be changed programmatically and se we calculate the hash at the runtime, just before taking snapshot. - the snapshot is taken just after we initialize the Mono runtime - cwraps initialized (they would be initialized again on subsequent start) - after loading all the DLLs into memory. From b92c82eaa97406751b8e5a21c45616b46d5685b7 Mon Sep 17 00:00:00 2001 From: Pavel Savara Date: Mon, 6 Mar 2023 18:42:37 +0100 Subject: [PATCH 08/45] Update src/mono/wasm/memory-snapshot.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Marek Fišera --- src/mono/wasm/memory-snapshot.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mono/wasm/memory-snapshot.md b/src/mono/wasm/memory-snapshot.md index f75a65260ec8b..13aa02541492b 100644 --- a/src/mono/wasm/memory-snapshot.md +++ b/src/mono/wasm/memory-snapshot.md @@ -13,7 +13,7 @@ Such subsequent start is significantly faster. - the mono-config has field `assetsHash` which is summary SHA256 of all the assets. - the configuration could be changed programmatically and se we calculate the hash at the runtime, just before taking snapshot. - the snapshot is taken just after we initialize the Mono runtime - - cwraps initialized (they would be initialized again on subsequent start) + - before cwraps are initialized (they would be initialized again on subsequent start) - after loading all the DLLs into memory. - after loading ICU and timezone data - after applying environment variables and other runtime options. From 7ccc8e6173c819b75fa2b86ea64329b698c2ccdf Mon Sep 17 00:00:00 2001 From: Pavel Savara Date: Mon, 6 Mar 2023 18:42:50 +0100 Subject: [PATCH 09/45] Update src/mono/wasm/memory-snapshot.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Marek Fišera --- src/mono/wasm/memory-snapshot.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mono/wasm/memory-snapshot.md b/src/mono/wasm/memory-snapshot.md index 13aa02541492b..666f94a5e975c 100644 --- a/src/mono/wasm/memory-snapshot.md +++ b/src/mono/wasm/memory-snapshot.md @@ -17,7 +17,7 @@ Such subsequent start is significantly faster. - after loading all the DLLs into memory. - after loading ICU and timezone data - after applying environment variables and other runtime options. - - before any worker threads init + - before any worker threads initialization - before any JavaScript interop initialization - before any Managed code initialization - therefore we do not expect to store any application state in the snapshot. From ec762acaf3872b15dd4c13a2581cfb670f6c81d7 Mon Sep 17 00:00:00 2001 From: Pavel Savara Date: Mon, 6 Mar 2023 18:43:02 +0100 Subject: [PATCH 10/45] Update src/mono/wasm/memory-snapshot.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Marek Fišera --- src/mono/wasm/memory-snapshot.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mono/wasm/memory-snapshot.md b/src/mono/wasm/memory-snapshot.md index 666f94a5e975c..095a70841104d 100644 --- a/src/mono/wasm/memory-snapshot.md +++ b/src/mono/wasm/memory-snapshot.md @@ -19,7 +19,7 @@ Such subsequent start is significantly faster. - after applying environment variables and other runtime options. - before any worker threads initialization - before any JavaScript interop initialization - - before any Managed code initialization + - before any Managed code executes - therefore we do not expect to store any application state in the snapshot. ### How to opt out From 91c021178b0abfd7bcc1d4da97be8ff60cf014b4 Mon Sep 17 00:00:00 2001 From: Pavel Savara Date: Mon, 6 Mar 2023 18:43:29 +0100 Subject: [PATCH 11/45] Update src/mono/wasm/memory-snapshot.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Marek Fišera --- src/mono/wasm/memory-snapshot.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mono/wasm/memory-snapshot.md b/src/mono/wasm/memory-snapshot.md index 095a70841104d..e9bc37ecc6fd1 100644 --- a/src/mono/wasm/memory-snapshot.md +++ b/src/mono/wasm/memory-snapshot.md @@ -23,5 +23,5 @@ Such subsequent start is significantly faster. - therefore we do not expect to store any application state in the snapshot. ### How to opt out - - you can use new API `withMemoryCache(false)` to opt out from the feature. +You can turn this feature of by calling `withMemoryCache(false)` on [dotnet API](https://github.com/dotnet/runtime/blob/main/src/mono/wasm/runtime/dotnet.d.ts). From 20d548bec838f77151696b82015562f8e79620eb Mon Sep 17 00:00:00 2001 From: Pavel Savara Date: Mon, 6 Mar 2023 18:43:47 +0100 Subject: [PATCH 12/45] Update src/mono/wasm/memory-snapshot.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Marek Fišera --- src/mono/wasm/memory-snapshot.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/mono/wasm/memory-snapshot.md b/src/mono/wasm/memory-snapshot.md index e9bc37ecc6fd1..3ba309923a7ff 100644 --- a/src/mono/wasm/memory-snapshot.md +++ b/src/mono/wasm/memory-snapshot.md @@ -2,9 +2,9 @@ We take snapshot of WASM memory after first cold start at run time​. We store it on the client side in the browser cache. -For subsequent start of the application with the same configuration and same assets, we could use the snapshot. -Instead of downloading all the assets and doing the runtime startup again. -Such subsequent start is significantly faster. +For subsequent runs with the same configuration and same assets, we use the snapshot +instead of downloading everything again and doing the runtime startup again. +These subsequent starts are significantly faster. ### Implementation details From 8e6865cf6ff5db7995c50fef7e139d19078ec192 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Mon, 6 Mar 2023 18:41:45 +0100 Subject: [PATCH 13/45] more linear memory usage and recovery from failure of snapshot use --- src/mono/wasm/runtime/assets.ts | 7 ++-- src/mono/wasm/runtime/startup.ts | 55 +++++++++++++++++++++----------- src/mono/wasm/runtime/storage.ts | 38 +++++++++++++++++----- src/mono/wasm/runtime/types.ts | 2 +- 4 files changed, 73 insertions(+), 29 deletions(-) diff --git a/src/mono/wasm/runtime/assets.ts b/src/mono/wasm/runtime/assets.ts index f184ec9398739..8608bd26150d8 100644 --- a/src/mono/wasm/runtime/assets.ts +++ b/src/mono/wasm/runtime/assets.ts @@ -8,7 +8,7 @@ import { mono_wasm_load_bytes_into_heap } from "./memory"; import { endMeasure, MeasuredBlock, startMeasure } from "./profiler"; import { createPromiseController, PromiseAndController } from "./promise-controller"; import { delay } from "./promise-utils"; -import { abort_startup, beforeOnRuntimeInitialized } from "./startup"; +import { abort_startup, beforeOnRuntimeInitialized, memorySnapshotIsResolved } from "./startup"; import { AssetBehaviours, AssetEntry, AssetEntryInternal, LoadingResource, mono_assert, ResourceRequest } from "./types"; import { InstantiateWasmSuccessCallback, VoidPtr } from "./types/emscripten"; @@ -59,7 +59,10 @@ export function resolve_asset_path(behavior: AssetBehaviours) { return asset; } export async function mono_download_assets(): Promise { - if (runtimeHelpers.memoryIsLoaded) { + // continue after we know if memory snapshot is available or not + await memorySnapshotIsResolved.promise; + + if (runtimeHelpers.useMemorySnapshot) { allAssetsInMemory.promise_control.resolve(); return; } diff --git a/src/mono/wasm/runtime/startup.ts b/src/mono/wasm/runtime/startup.ts index 6910054ee6fa0..4425b4b8fe34a 100644 --- a/src/mono/wasm/runtime/startup.ts +++ b/src/mono/wasm/runtime/startup.ts @@ -27,7 +27,7 @@ import { mono_wasm_init_diagnostics } from "./diagnostics"; import { preAllocatePThreadWorkerPool, instantiateWasmPThreadWorkerPool } from "./pthreads/browser"; import { export_linker } from "./exports-linker"; import { endMeasure, MeasuredBlock, startMeasure } from "./profiler"; -import { getMemory, storeMemory } from "./storage"; +import { getMemorySnapshot, storeMemorySnapshot, getMemorySnapshotSize } from "./storage"; // legacy import { init_legacy_exports } from "./net6-legacy/corebindings"; @@ -39,6 +39,7 @@ let configLoaded = false; export const dotnetReady = createPromiseController(); export const afterConfigLoaded = createPromiseController(); export const beforeInstantiateWasm = createPromiseController(); +export const memorySnapshotIsResolved = createPromiseController(); export const afterInstantiateWasm = createPromiseController(); export const beforePreInit = createPromiseController(); export const afterPreInit = createPromiseController(); @@ -108,7 +109,9 @@ function instantiateWasm( const mark = startMeasure(); if (userInstantiateWasm) { + // user wasm doesn't support memory snapshots beforeInstantiateWasm.promise_control.resolve(); + memorySnapshotIsResolved.promise_control.resolve(); const exports = userInstantiateWasm(imports, (instance: WebAssembly.Instance, module: WebAssembly.Module | undefined) => { endMeasure(mark, MeasuredBlock.instantiateWasm); afterInstantiateWasm.promise_control.resolve(); @@ -179,7 +182,6 @@ async function preInitWorkerAsync() { const mark = startMeasure(); try { if (runtimeHelpers.diagnosticTracing) console.debug("MONO_WASM: preInitWorker"); - beforePreInit.promise_control.resolve(); mono_wasm_pre_init_essential(true); await init_polyfills_async(); afterPreInit.promise_control.resolve(); @@ -286,6 +288,7 @@ export function abort_startup(reason: any, should_exit: boolean): void { if (runtimeHelpers.diagnosticTracing) console.trace("MONO_WASM: abort_startup"); dotnetReady.promise_control.reject(reason); beforeInstantiateWasm.promise_control.reject(reason); + memorySnapshotIsResolved.promise_control.reject(reason); afterInstantiateWasm.promise_control.reject(reason); beforePreInit.promise_control.reject(reason); afterPreInit.promise_control.reject(reason); @@ -334,7 +337,9 @@ async function mono_wasm_pre_init_full(): Promise { if (runtimeHelpers.diagnosticTracing) console.debug("MONO_WASM: mono_wasm_pre_init_full"); Module.addRunDependency("mono_wasm_pre_init_full"); + // continue after wasm is downloaded await beforeInstantiateWasm.promise; + await mono_download_assets(); Module.removeRunDependency("mono_wasm_pre_init_full"); @@ -348,8 +353,8 @@ async function mono_wasm_before_user_runtime_initialized(): Promise { mono_wasm_globalization_init(); if (!runtimeHelpers.mono_wasm_load_runtime_done) mono_wasm_load_runtime("unused", config.debugLevel); - if (config.cacheMemory && !runtimeHelpers.memoryIsLoaded) { - await storeMemory(Module.HEAP8.buffer); + if (config.cacheMemory && !runtimeHelpers.useMemorySnapshot) { + await storeMemorySnapshot(Module.HEAP8.buffer); } if (MonoWasmThreads) { preAllocatePThreadWorkerPool(MONO_PTHREAD_POOL_SIZE, config); @@ -460,19 +465,23 @@ async function instantiate_wasm_module( ): Promise { // this is called so early that even Module exports like addRunDependency don't exist yet try { - let memoryBytes: ArrayBuffer | undefined = undefined; + let memorySize: number | undefined = undefined; await mono_wasm_load_config(Module.configSrc); if (runtimeHelpers.diagnosticTracing) console.debug("MONO_WASM: instantiate_wasm_module"); const assetToLoad = resolve_asset_path("dotnetwasm"); + // FIXME: this would not apply re-try (on connection reset during download) for dotnet.wasm because we could not download the buffer before we pass it to instantiate_wasm_asset + await start_asset_download(assetToLoad); + beforeInstantiateWasm.promise_control.resolve(); if (config.cacheMemory && config.assetsHash) { - memoryBytes = await getMemory(); - runtimeHelpers.memoryIsLoaded = !!memoryBytes; + memorySize = await getMemorySnapshotSize(); + runtimeHelpers.useMemorySnapshot = !!memorySize; + } + if (!runtimeHelpers.useMemorySnapshot) { + // we should start downloading DLLs etc as they are not in the snapshot + memorySnapshotIsResolved.promise_control.resolve(); } - beforeInstantiateWasm.promise_control.resolve(); - // FIXME: this would not apply re-try (on connection reset during download) for dotnet.wasm because we could not download the buffer before we pass it to instantiate_wasm_asset - await start_asset_download(assetToLoad); await beforePreInit.promise; Module.addRunDependency("instantiate_wasm_module"); @@ -483,13 +492,23 @@ async function instantiate_wasm_module( assetToLoad.buffer = null as any; // GC if (runtimeHelpers.diagnosticTracing) console.debug("MONO_WASM: instantiate_wasm_module done"); - if (runtimeHelpers.memoryIsLoaded) { - const wasmMemory = anyModule.asm.memory; - // .grow() takes a delta compared to the previous size - wasmMemory.grow((memoryBytes!.byteLength - wasmMemory.buffer.byteLength + 65535) >>> 16); - runtimeHelpers.updateGlobalBufferAndViews(wasmMemory.buffer); - Module.HEAP8.set(new Int8Array(memoryBytes!), 0); - if (runtimeHelpers.diagnosticTracing) console.debug("MONO_WASM: Loaded memory from cache"); + if (runtimeHelpers.useMemorySnapshot) { + try { + const wasmMemory = anyModule.asm.memory; + // .grow() takes a delta compared to the previous size + wasmMemory.grow((memorySize! - wasmMemory.buffer.byteLength + 65535) >>> 16); + runtimeHelpers.updateGlobalBufferAndViews(wasmMemory.buffer); + + // get the bytes after we re-sized the memory, so that we don't have too much memory in use at the same time + const memoryBytes = await getMemorySnapshot(); + Module.HEAP8.set(new Int8Array(memoryBytes!), 0); + if (runtimeHelpers.diagnosticTracing) console.debug("MONO_WASM: Loaded memory from cache"); + } catch (err) { + console.warn("MONO_WASM: failed to load memory snapshot", err); + runtimeHelpers.useMemorySnapshot = false; + } + // now we know if the loading of memory succeeded or not, we can start loading the rest of the assets + memorySnapshotIsResolved.promise_control.resolve(); } afterInstantiateWasm.promise_control.resolve(); } catch (err) { @@ -531,7 +550,7 @@ export function mono_wasm_load_runtime(unused?: string, debugLevel?: number): vo runtimeHelpers.mono_wasm_load_runtime_done = true; try { const mark = startMeasure(); - if (!runtimeHelpers.memoryIsLoaded) { + if (!runtimeHelpers.useMemorySnapshot) { if (debugLevel == undefined) { debugLevel = 0; if (config && config.debugLevel) { diff --git a/src/mono/wasm/runtime/storage.ts b/src/mono/wasm/runtime/storage.ts index 501fbc0b0bef6..9228e1e6077ed 100644 --- a/src/mono/wasm/runtime/storage.ts +++ b/src/mono/wasm/runtime/storage.ts @@ -8,7 +8,7 @@ import { runtimeHelpers } from "./imports"; const memoryPrefix = "https://dotnet.generated.invalid/wasm-memory"; // adapted from Blazor's WebAssemblyResourceLoader.ts -async function open(): Promise { +async function openCache(): Promise { // caches will be undefined if we're running on an insecure origin (secure means https or localhost) if (typeof globalThis.caches === "undefined") { return null; @@ -43,14 +43,34 @@ async function open(): Promise { } } -export async function getMemory(): Promise { +export async function getMemorySnapshotSize(): Promise { try { const inputsHash = await getInputsHash(); if (!inputsHash) { return undefined; } const cacheKey = `${memoryPrefix}-${ProductVersion}-${GitHash}-${inputsHash}`; - const cache = await open(); + const cache = await openCache(); + if (!cache) { + return undefined; + } + const res = await cache.match(cacheKey); + const contentLength = res?.headers.get("content-length"); + return contentLength ? parseInt(contentLength) : undefined; + } catch (ex) { + console.warn("MONO_WASM: Failed find memory snapshot in the cache", ex); + return undefined; + } +} + +export async function getMemorySnapshot(): Promise { + try { + const inputsHash = await getInputsHash(); + if (!inputsHash) { + return undefined; + } + const cacheKey = `${memoryPrefix}-${ProductVersion}-${GitHash}-${inputsHash}`; + const cache = await openCache(); if (!cache) { return undefined; } @@ -60,18 +80,19 @@ export async function getMemory(): Promise { } return res.arrayBuffer(); } catch (ex) { + console.warn("MONO_WASM: Failed load memory snapshot from the cache", ex); return undefined; } } -export async function storeMemory(memory: ArrayBuffer) { +export async function storeMemorySnapshot(memory: ArrayBuffer) { try { const inputsHash = await getInputsHash(); if (!inputsHash) { return; } const cacheKey = `${memoryPrefix}-${ProductVersion}-${GitHash}-${inputsHash}`; - const cache = await open(); + const cache = await openCache(); if (!cache) { return; } @@ -85,15 +106,16 @@ export async function storeMemory(memory: ArrayBuffer) { await cache.put(cacheKey, responseToCache); - cleanupMemory(cacheKey); // no await + cleanupMemorySnapshots(cacheKey); // no await } catch (ex) { + console.warn("MONO_WASM: Failed to store memory snapshot in the cache", ex); return; } } -export async function cleanupMemory(protectKey: string) { +export async function cleanupMemorySnapshots(protectKey: string) { try { - const cache = await open(); + const cache = await openCache(); if (!cache) { return; } diff --git a/src/mono/wasm/runtime/types.ts b/src/mono/wasm/runtime/types.ts index 6c680248841db..f130bcd3b3a35 100644 --- a/src/mono/wasm/runtime/types.ts +++ b/src/mono/wasm/runtime/types.ts @@ -233,7 +233,7 @@ export type RuntimeHelpers = { locateFile: (path: string, prefix?: string) => string, javaScriptExports: JavaScriptExports, loadedFiles: string[], - memoryIsLoaded: boolean, + useMemorySnapshot: boolean, subtle: SubtleCrypto | null, preferredIcuAsset: string | null, timezone: string | null, From 7bf7881484da7b4b3149a72463cd4099851f9736 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Mon, 6 Mar 2023 18:46:08 +0100 Subject: [PATCH 14/45] feedback --- src/mono/wasm/memory-snapshot.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/mono/wasm/memory-snapshot.md b/src/mono/wasm/memory-snapshot.md index 3ba309923a7ff..3ce3691b4d80e 100644 --- a/src/mono/wasm/memory-snapshot.md +++ b/src/mono/wasm/memory-snapshot.md @@ -2,7 +2,7 @@ We take snapshot of WASM memory after first cold start at run time​. We store it on the client side in the browser cache. -For subsequent runs with the same configuration and same assets, we use the snapshot +For subsequent runs with the same configuration and same assets, we use the snapshot . instead of downloading everything again and doing the runtime startup again. These subsequent starts are significantly faster. @@ -11,15 +11,15 @@ These subsequent starts are significantly faster. - the consistency of inputs (configuration and assets) with the snapshot is done by calculating SHA256 of the inputs. - the DLLs and other downloaded assets each have SHA256 which is used to validate 'integrity' by the browser. - the mono-config has field `assetsHash` which is summary SHA256 of all the assets. - - the configuration could be changed programmatically and se we calculate the hash at the runtime, just before taking snapshot. -- the snapshot is taken just after we initialize the Mono runtime - - before cwraps are initialized (they would be initialized again on subsequent start) + - the configuration could be changed programmatically and so we calculate the hash at the runtime, just before taking snapshot. +- the snapshot is taken just after we initialize the Mono runtime. + - before cwraps are initialized (they would be initialized again on subsequent start). - after loading all the DLLs into memory. - - after loading ICU and timezone data + - after loading ICU and timezone data. - after applying environment variables and other runtime options. - - before any worker threads initialization - - before any JavaScript interop initialization - - before any Managed code executes + - before any worker threads initialization. + - before any JavaScript interop initialization. + - before any Managed code executes. - therefore we do not expect to store any application state in the snapshot. ### How to opt out From 5651f374b06633ae7d131175e8e7c84817573a61 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 8 Mar 2023 10:33:01 +0100 Subject: [PATCH 15/45] start runtime twice for unit tests, so that the actual tests is running on snapshots --- src/mono/wasm/test-main.js | 92 ++++++++++++++++++++++---------------- 1 file changed, 54 insertions(+), 38 deletions(-) diff --git a/src/mono/wasm/test-main.js b/src/mono/wasm/test-main.js index 979c8a2d90ae0..cc45bc1b32b33 100644 --- a/src/mono/wasm/test-main.js +++ b/src/mono/wasm/test-main.js @@ -66,8 +66,7 @@ async function getArgs() { let runArgsJson; // ToDo: runArgs should be read for all kinds of hosts, but // fetch is added to node>=18 and current Windows's emcc node<18 - if (is_browser) - { + if (is_browser) { const response = await globalThis.fetch('./runArgs.json'); if (response.ok) { runArgsJson = initRunArgs(await response.json()); @@ -243,53 +242,70 @@ const App = { }; globalThis.App = App; // Necessary as System.Runtime.InteropServices.JavaScript.Tests.MarshalTests (among others) call the App.call_test_method directly +function configureRuntime(dotnet, runArgs, INTERNAL) { + dotnet + .withVirtualWorkingDirectory(runArgs.workingDirectory) + .withEnvironmentVariables(runArgs.environmentVariables) + //.withDiagnosticTracing(runArgs.diagnosticTracing) + .withDiagnosticTracing(true) + .withExitOnUnhandledError() + .withExitCodeLogging() + .withElementOnExit(); + + if (is_node) { + dotnet + .withEnvironmentVariable("NodeJSPlatform", process.platform) + .withAsyncFlushOnExit(); + + const modulesToLoad = runArgs.environmentVariables["NPM_MODULES"]; + if (modulesToLoad) { + dotnet.withModuleConfig({ + onConfigLoaded: (config) => { + loadNodeModules(config, INTERNAL.require, modulesToLoad) + } + }) + } + } + if (is_browser) { + dotnet.withEnvironmentVariable("IsWebSocketSupported", "true"); + } + if (runArgs.runtimeArgs.length > 0) { + dotnet.withRuntimeOptions(runArgs.runtimeArgs); + } + if (runArgs.debugging) { + dotnet.withDebugging(-1); + dotnet.withWaitingForDebugger(-1); + } + if (runArgs.forwardConsole) { + dotnet.withConsoleForwarding(); + } +} + async function run() { try { + const runArgs = await getArgs(); + + if (is_browser) { + // this separate instance of the ES6 module, in which we just populate the caches + const { dotnet: dry_run, exit: dry_exit, INTERNAL: DRY_INTERNAL } = await loadDotnet('./dotnet.js?dry-run=true'); + mono_exit = dry_exit; + configureRuntime(dry_run, runArgs, DRY_INTERNAL); + await dry_run.create(); + } + + // this is subsequent run with the actual tests. It will use whatever was cached in the previous run. + // This way, we are testing that the cached version works. const { dotnet, exit, INTERNAL } = await loadDotnet('./dotnet.js'); mono_exit = exit; - const runArgs = await getArgs(); if (runArgs.applicationArguments.length == 0) { mono_exit(1, "Missing required --run argument"); return; } console.log("Application arguments: " + runArgs.applicationArguments.join(' ')); - dotnet - .withVirtualWorkingDirectory(runArgs.workingDirectory) - .withEnvironmentVariables(runArgs.environmentVariables) - .withDiagnosticTracing(runArgs.diagnosticTracing) - .withExitOnUnhandledError() - .withExitCodeLogging() - .withElementOnExit(); - - if (is_node) { - dotnet - .withEnvironmentVariable("NodeJSPlatform", process.platform) - .withAsyncFlushOnExit(); - - const modulesToLoad = runArgs.environmentVariables["NPM_MODULES"]; - if (modulesToLoad) { - dotnet.withModuleConfig({ - onConfigLoaded: (config) => { - loadNodeModules(config, INTERNAL.require, modulesToLoad) - } - }) - } - } - if (is_browser) { - dotnet.withEnvironmentVariable("IsWebSocketSupported", "true"); - } - if (runArgs.runtimeArgs.length > 0) { - dotnet.withRuntimeOptions(runArgs.runtimeArgs); - } - if (runArgs.debugging) { - dotnet.withDebugging(-1); - dotnet.withWaitingForDebugger(-1); - } - if (runArgs.forwardConsole) { - dotnet.withConsoleForwarding(); - } + configureRuntime(dotnet, runArgs, INTERNAL); + App.runtime = await dotnet.create(); App.runArgs = runArgs From 31b46ff1fc084604f14d5805f6d3c43a040ebb78 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 8 Mar 2023 12:46:46 +0100 Subject: [PATCH 16/45] wip --- src/mono/sample/wasm/browser-advanced/index.html | 1 - src/mono/sample/wasm/browser-bench/appstart-frame.html | 1 - 2 files changed, 2 deletions(-) diff --git a/src/mono/sample/wasm/browser-advanced/index.html b/src/mono/sample/wasm/browser-advanced/index.html index b4cefe75f4072..3e9ecf7daafb2 100644 --- a/src/mono/sample/wasm/browser-advanced/index.html +++ b/src/mono/sample/wasm/browser-advanced/index.html @@ -12,7 +12,6 @@ - diff --git a/src/mono/sample/wasm/browser-bench/appstart-frame.html b/src/mono/sample/wasm/browser-bench/appstart-frame.html index 1582a356568d8..594c527ba6eb2 100644 --- a/src/mono/sample/wasm/browser-bench/appstart-frame.html +++ b/src/mono/sample/wasm/browser-bench/appstart-frame.html @@ -12,7 +12,6 @@ - From 43a73e17a1e528d8fd039c90711dcb97f008a5bb Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 8 Mar 2023 15:45:10 +0100 Subject: [PATCH 17/45] continue loading non-snapshot assets --- .../sample/wasm/browser-advanced/index.html | 1 - .../wasm/browser-bench/appstart-frame.html | 1 - src/mono/wasm/runtime/assets.ts | 65 ++++++++++++++----- src/mono/wasm/runtime/startup.ts | 14 ++-- src/mono/wasm/runtime/types.ts | 3 +- 5 files changed, 60 insertions(+), 24 deletions(-) diff --git a/src/mono/sample/wasm/browser-advanced/index.html b/src/mono/sample/wasm/browser-advanced/index.html index 3e9ecf7daafb2..2b9801168077f 100644 --- a/src/mono/sample/wasm/browser-advanced/index.html +++ b/src/mono/sample/wasm/browser-advanced/index.html @@ -11,7 +11,6 @@ - diff --git a/src/mono/sample/wasm/browser-bench/appstart-frame.html b/src/mono/sample/wasm/browser-bench/appstart-frame.html index 594c527ba6eb2..1aec2376ab83a 100644 --- a/src/mono/sample/wasm/browser-bench/appstart-frame.html +++ b/src/mono/sample/wasm/browser-bench/appstart-frame.html @@ -11,7 +11,6 @@ - diff --git a/src/mono/wasm/runtime/assets.ts b/src/mono/wasm/runtime/assets.ts index 8608bd26150d8..e9784a881d61e 100644 --- a/src/mono/wasm/runtime/assets.ts +++ b/src/mono/wasm/runtime/assets.ts @@ -29,6 +29,7 @@ const skipDownloadsByAssetTypes: { [k: string]: boolean } = { "js-module-threads": true, + "dotnetwasm": true, }; // `response.arrayBuffer()` can't be called twice. Some usecases are calling it on response in the instantiation. @@ -38,6 +39,19 @@ const skipBufferByAssetTypes: { "dotnetwasm": true, }; +const skipInSnapshotByAssetTypes: { + [k: string]: boolean +} = { + "resource": true, + "assembly": true, + "pdb": true, + "heap": true, + "icu": true, + "js-module-threads": true, + "dotnetwasm": true, +}; + + // these assets are instantiated differently than the main flow const skipInstantiateByAssetTypes: { [k: string]: boolean @@ -59,20 +73,11 @@ export function resolve_asset_path(behavior: AssetBehaviours) { return asset; } export async function mono_download_assets(): Promise { - // continue after we know if memory snapshot is available or not - await memorySnapshotIsResolved.promise; - - if (runtimeHelpers.useMemorySnapshot) { - allAssetsInMemory.promise_control.resolve(); - return; - } - if (runtimeHelpers.diagnosticTracing) console.debug("MONO_WASM: mono_download_assets"); runtimeHelpers.maxParallelDownloads = runtimeHelpers.config.maxParallelDownloads || runtimeHelpers.maxParallelDownloads; runtimeHelpers.enableDownloadRetry = runtimeHelpers.config.enableDownloadRetry || runtimeHelpers.enableDownloadRetry; try { - const promises_of_assets: Promise[] = []; - // start fetching and instantiating all assets in parallel + // just validate all for (const a of runtimeHelpers.config.assets!) { const asset: AssetEntryInternal = a; mono_assert(typeof asset === "object", "asset must be object"); @@ -81,14 +86,44 @@ export async function mono_download_assets(): Promise { mono_assert(!asset.resolvedUrl || typeof asset.resolvedUrl === "string", "asset resolvedUrl could be string"); mono_assert(!asset.hash || typeof asset.hash === "string", "asset resolvedUrl could be string"); mono_assert(!asset.pendingDownload || typeof asset.pendingDownload === "object", "asset pendingDownload could be object"); - if (!skipInstantiateByAssetTypes[asset.behavior] && shouldLoadIcuAsset(asset)) { - expected_instantiated_assets_count++; + } + + + const promises_of_assets: Promise[] = []; + const nonSnapshotAssets: AssetEntryInternal[] = []; + // start fetching and instantiating all assets in parallel + for (const a of runtimeHelpers.config.assets!) { + const asset: AssetEntryInternal = a; + const skipInSnapshot = skipInSnapshotByAssetTypes[asset.behavior]; + if (!skipInSnapshot) { + if (!skipInstantiateByAssetTypes[asset.behavior] && shouldLoadIcuAsset(asset)) { + expected_instantiated_assets_count++; + } + if (!skipDownloadsByAssetTypes[asset.behavior] && shouldLoadIcuAsset(asset)) { + expected_downloaded_assets_count++; + promises_of_assets.push(start_asset_download(asset)); + } + } else { + nonSnapshotAssets.push(asset); } - if (!skipDownloadsByAssetTypes[asset.behavior] && shouldLoadIcuAsset(asset)) { - expected_downloaded_assets_count++; - promises_of_assets.push(start_asset_download(asset)); + } + + // continue after we know if memory snapshot is available or not + await memorySnapshotIsResolved.promise; + + // and load the rest of the assets if necessary + if (!runtimeHelpers.loadMemorySnapshot) { + for (const asset of nonSnapshotAssets) { + if (!skipInstantiateByAssetTypes[asset.behavior] && shouldLoadIcuAsset(asset)) { + expected_instantiated_assets_count++; + } + if (!skipDownloadsByAssetTypes[asset.behavior] && shouldLoadIcuAsset(asset)) { + expected_downloaded_assets_count++; + promises_of_assets.push(start_asset_download(asset)); + } } } + allDownloadsQueued.promise_control.resolve(); const promises_of_asset_instantiation: Promise[] = []; diff --git a/src/mono/wasm/runtime/startup.ts b/src/mono/wasm/runtime/startup.ts index cd101824be9eb..d377ebd7d6887 100644 --- a/src/mono/wasm/runtime/startup.ts +++ b/src/mono/wasm/runtime/startup.ts @@ -353,8 +353,9 @@ async function mono_wasm_before_user_runtime_initialized(): Promise { mono_wasm_globalization_init(); if (!runtimeHelpers.mono_wasm_load_runtime_done) mono_wasm_load_runtime("unused", config.debugLevel); - if (config.cacheMemory && !runtimeHelpers.useMemorySnapshot) { + if (config.cacheMemory && !runtimeHelpers.loadMemorySnapshot) { await storeMemorySnapshot(Module.HEAP8.buffer); + runtimeHelpers.storeMemorySnapshotPending = false; } if (MonoWasmThreads) { preAllocatePThreadWorkerPool(MONO_PTHREAD_POOL_SIZE, config); @@ -475,9 +476,10 @@ async function instantiate_wasm_module( if (config.cacheMemory && config.assetsHash) { memorySize = await getMemorySnapshotSize(); - runtimeHelpers.useMemorySnapshot = !!memorySize; + runtimeHelpers.loadMemorySnapshot = !!memorySize; + runtimeHelpers.storeMemorySnapshotPending = !runtimeHelpers.loadMemorySnapshot; } - if (!runtimeHelpers.useMemorySnapshot) { + if (!runtimeHelpers.loadMemorySnapshot) { // we should start downloading DLLs etc as they are not in the snapshot memorySnapshotIsResolved.promise_control.resolve(); } @@ -492,7 +494,7 @@ async function instantiate_wasm_module( assetToLoad.buffer = null as any; // GC if (runtimeHelpers.diagnosticTracing) console.debug("MONO_WASM: instantiate_wasm_module done"); - if (runtimeHelpers.useMemorySnapshot) { + if (runtimeHelpers.loadMemorySnapshot) { try { const wasmMemory = Module.asm.memory; // .grow() takes a delta compared to the previous size @@ -505,7 +507,7 @@ async function instantiate_wasm_module( if (runtimeHelpers.diagnosticTracing) console.debug("MONO_WASM: Loaded memory from cache"); } catch (err) { console.warn("MONO_WASM: failed to load memory snapshot", err); - runtimeHelpers.useMemorySnapshot = false; + runtimeHelpers.loadMemorySnapshot = false; } // now we know if the loading of memory succeeded or not, we can start loading the rest of the assets memorySnapshotIsResolved.promise_control.resolve(); @@ -550,7 +552,7 @@ export function mono_wasm_load_runtime(unused?: string, debugLevel?: number): vo runtimeHelpers.mono_wasm_load_runtime_done = true; try { const mark = startMeasure(); - if (!runtimeHelpers.useMemorySnapshot) { + if (!runtimeHelpers.loadMemorySnapshot) { if (debugLevel == undefined) { debugLevel = 0; if (config && config.debugLevel) { diff --git a/src/mono/wasm/runtime/types.ts b/src/mono/wasm/runtime/types.ts index 6a44f81280900..2f3dfaf60c67d 100644 --- a/src/mono/wasm/runtime/types.ts +++ b/src/mono/wasm/runtime/types.ts @@ -233,7 +233,8 @@ export type RuntimeHelpers = { locateFile: (path: string, prefix?: string) => string, javaScriptExports: JavaScriptExports, loadedFiles: string[], - useMemorySnapshot: boolean, + loadMemorySnapshot: boolean, + storeMemorySnapshotPending: boolean, subtle: SubtleCrypto | null, preferredIcuAsset: string | null, timezone: string | null, From f49354aa4a7c0e6520c98d6141449683acc4f1b6 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 8 Mar 2023 16:01:23 +0100 Subject: [PATCH 18/45] disable adding methods to __indirect_function_table before we take memory snapshot --- src/mono/wasm/runtime/jiterpreter-jit-call.ts | 7 ++++--- src/mono/wasm/runtime/jiterpreter-support.ts | 7 ++++--- src/mono/wasm/runtime/jiterpreter.ts | 4 +++- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/mono/wasm/runtime/jiterpreter-jit-call.ts b/src/mono/wasm/runtime/jiterpreter-jit-call.ts index 6dd43b0a7dec9..e78e9cdb34283 100644 --- a/src/mono/wasm/runtime/jiterpreter-jit-call.ts +++ b/src/mono/wasm/runtime/jiterpreter-jit-call.ts @@ -3,7 +3,7 @@ import { mono_assert, MonoType, MonoMethod } from "./types"; import { NativePointer, Int32Ptr, VoidPtr } from "./types/emscripten"; -import { Module } from "./imports"; +import { Module, runtimeHelpers } from "./imports"; import { getU8, getI32, getU32, setU32_unchecked } from "./memory"; @@ -269,8 +269,9 @@ export function mono_jiterp_do_jit_call_indirect ( } }; - let failed = !getIsWasmEhSupported(); - if (!failed) { + let failed = false; + const enabled = !runtimeHelpers.storeMemorySnapshotPending && getIsWasmEhSupported(); + if (enabled) { // Wasm EH is supported which means doJitCallModule was loaded and compiled. // Now that we have jit_call_cb, we can instantiate it. try { diff --git a/src/mono/wasm/runtime/jiterpreter-support.ts b/src/mono/wasm/runtime/jiterpreter-support.ts index 665757385ed80..1e00e38197331 100644 --- a/src/mono/wasm/runtime/jiterpreter-support.ts +++ b/src/mono/wasm/runtime/jiterpreter-support.ts @@ -3,7 +3,7 @@ import { mono_assert } from "./types"; import { NativePointer, ManagedPointer, VoidPtr } from "./types/emscripten"; -import { Module } from "./imports"; +import { Module, runtimeHelpers } from "./imports"; import { WasmOpcode } from "./jiterpreter-opcodes"; import cwraps from "./cwraps"; @@ -883,8 +883,9 @@ export function getWasmFunctionTable () { } export function addWasmFunctionPointer (f: Function) { - if (!f) - throw new Error("Attempting to set null function into table"); + mono_assert(f,"Attempting to set null function into table"); + mono_assert(!runtimeHelpers.storeMemorySnapshotPending, "Attempting to set function into table during creation of memory snapshot"); + const table = getWasmFunctionTable(); if (wasmFunctionIndicesFree <= 0) { wasmNextFunctionIndex = table.length; diff --git a/src/mono/wasm/runtime/jiterpreter.ts b/src/mono/wasm/runtime/jiterpreter.ts index 3bce90e22bd2d..75850f02c9417 100644 --- a/src/mono/wasm/runtime/jiterpreter.ts +++ b/src/mono/wasm/runtime/jiterpreter.ts @@ -3,7 +3,7 @@ import { mono_assert, MonoMethod } from "./types"; import { NativePointer } from "./types/emscripten"; -import { Module } from "./imports"; +import { Module, runtimeHelpers } from "./imports"; import { getU16, getU32 } from "./memory"; @@ -760,6 +760,8 @@ function generate_wasm ( // independently jitting traces will not stomp on each other and all threads // have a globally consistent view of which function pointer maps to each trace. rejected = false; + mono_assert(!runtimeHelpers.storeMemorySnapshotPending, "Attempting to set function into table during creation of memory snapshot"); + const idx = trapTraceErrors ? Module.addFunction( From 53781ab1e538fe1ad1c9f3eb7b4a7d245e1e8ad7 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 8 Mar 2023 18:22:41 +0100 Subject: [PATCH 19/45] rename startupMemoryCache --- src/mono/wasm/memory-snapshot.md | 2 +- src/mono/wasm/runtime/dotnet.d.ts | 8 ++++---- src/mono/wasm/runtime/run-outer.ts | 6 +++--- src/mono/wasm/runtime/startup.ts | 8 ++++---- src/mono/wasm/runtime/types.ts | 2 +- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/mono/wasm/memory-snapshot.md b/src/mono/wasm/memory-snapshot.md index 3ce3691b4d80e..51dd896b23691 100644 --- a/src/mono/wasm/memory-snapshot.md +++ b/src/mono/wasm/memory-snapshot.md @@ -23,5 +23,5 @@ These subsequent starts are significantly faster. - therefore we do not expect to store any application state in the snapshot. ### How to opt out -You can turn this feature of by calling `withMemoryCache(false)` on [dotnet API](https://github.com/dotnet/runtime/blob/main/src/mono/wasm/runtime/dotnet.d.ts). +You can turn this feature of by calling `withStartupMemoryCache (false)` on [dotnet API](https://github.com/dotnet/runtime/blob/main/src/mono/wasm/runtime/dotnet.d.ts). diff --git a/src/mono/wasm/runtime/dotnet.d.ts b/src/mono/wasm/runtime/dotnet.d.ts index 95f3fd5984ce0..91f9d52544701 100644 --- a/src/mono/wasm/runtime/dotnet.d.ts +++ b/src/mono/wasm/runtime/dotnet.d.ts @@ -18,7 +18,7 @@ interface DotnetHostBuilder { withDebugging(level: number): DotnetHostBuilder; withMainAssembly(mainAssemblyName: string): DotnetHostBuilder; withApplicationArgumentsFromQuery(): DotnetHostBuilder; - withMemoryCache(value: boolean): DotnetHostBuilder; + withStartupMemoryCache(value: boolean): DotnetHostBuilder; create(): Promise; run(): Promise; } @@ -132,7 +132,7 @@ type MonoConfig = { /** * If true, the snapshot of runtime's memory will be stored in the browser and used for faster startup next time. Default is true. */ - cacheMemory?: boolean; + startupMemoryCache?: boolean; /** * hash of assets */ @@ -179,8 +179,8 @@ interface AssetEntry extends ResourceRequest { } type AssetBehaviours = "resource" | "assembly" | "pdb" | "heap" | "icu" | "vfs" | "dotnetwasm" | "js-module-threads"; type GlobalizationMode = "icu" | // load ICU globalization data from any runtime assets with behavior "icu". -"invariant" | // operate in invariant globalization mode. -"auto"; + "invariant" | // operate in invariant globalization mode. + "auto"; type DotnetModuleConfig = { disableDotnet6Compatibility?: boolean; config?: MonoConfig; diff --git a/src/mono/wasm/runtime/run-outer.ts b/src/mono/wasm/runtime/run-outer.ts index dd4be9b379e16..c49317523403a 100644 --- a/src/mono/wasm/runtime/run-outer.ts +++ b/src/mono/wasm/runtime/run-outer.ts @@ -18,7 +18,7 @@ export interface DotnetHostBuilder { withDebugging(level: number): DotnetHostBuilder withMainAssembly(mainAssemblyName: string): DotnetHostBuilder withApplicationArgumentsFromQuery(): DotnetHostBuilder - withMemoryCache(value: boolean): DotnetHostBuilder + withStartupMemoryCache(value: boolean): DotnetHostBuilder create(): Promise run(): Promise } @@ -138,10 +138,10 @@ class HostBuilder implements DotnetHostBuilder { } } - withMemoryCache(value: boolean): DotnetHostBuilder { + withStartupMemoryCache(value: boolean): DotnetHostBuilder { try { const configInternal: MonoConfigInternal = { - cacheMemory: value + startupMemoryCache: value }; Object.assign(this.moduleConfig.config!, configInternal); return this; diff --git a/src/mono/wasm/runtime/startup.ts b/src/mono/wasm/runtime/startup.ts index d377ebd7d6887..3fb7f7a9f8edf 100644 --- a/src/mono/wasm/runtime/startup.ts +++ b/src/mono/wasm/runtime/startup.ts @@ -353,7 +353,7 @@ async function mono_wasm_before_user_runtime_initialized(): Promise { mono_wasm_globalization_init(); if (!runtimeHelpers.mono_wasm_load_runtime_done) mono_wasm_load_runtime("unused", config.debugLevel); - if (config.cacheMemory && !runtimeHelpers.loadMemorySnapshot) { + if (config.startupMemoryCache && !runtimeHelpers.loadMemorySnapshot) { await storeMemorySnapshot(Module.HEAP8.buffer); runtimeHelpers.storeMemorySnapshotPending = false; } @@ -474,7 +474,7 @@ async function instantiate_wasm_module( await start_asset_download(assetToLoad); beforeInstantiateWasm.promise_control.resolve(); - if (config.cacheMemory && config.assetsHash) { + if (config.startupMemoryCache && config.assetsHash) { memorySize = await getMemorySnapshotSize(); runtimeHelpers.loadMemorySnapshot = !!memorySize; runtimeHelpers.storeMemorySnapshotPending = !runtimeHelpers.loadMemorySnapshot; @@ -563,7 +563,7 @@ export function mono_wasm_load_runtime(unused?: string, debugLevel?: number): vo } endMeasure(mark, MeasuredBlock.loadRuntime); - if (!config.cacheMemory) bindings_init(); + if (!config.startupMemoryCache) bindings_init(); } catch (err: any) { _print_error("MONO_WASM: mono_wasm_load_runtime () failed", err); @@ -660,7 +660,7 @@ function normalizeConfig() { config.assets = config.assets || []; config.runtimeOptions = config.runtimeOptions || []; config.globalizationMode = config.globalizationMode || "auto"; - config.cacheMemory = config.cacheMemory == undefined ? true : !!config.cacheMemory; + config.startupMemoryCache = config.startupMemoryCache == undefined ? true : !!config.startupMemoryCache; if (config.debugLevel === undefined && BuildConfiguration === "Debug") { config.debugLevel = -1; } diff --git a/src/mono/wasm/runtime/types.ts b/src/mono/wasm/runtime/types.ts index 2f3dfaf60c67d..63309e516f0d4 100644 --- a/src/mono/wasm/runtime/types.ts +++ b/src/mono/wasm/runtime/types.ts @@ -122,7 +122,7 @@ export type MonoConfig = { /** * If true, the snapshot of runtime's memory will be stored in the browser and used for faster startup next time. Default is true. */ - cacheMemory?: boolean, + startupMemoryCache?: boolean, /** * hash of assets */ From 60b2c5a03379bad8bdc03090d697b15950445a37 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 8 Mar 2023 19:56:22 +0100 Subject: [PATCH 20/45] try to fix threads --- src/mono/wasm/runtime/dotnet.d.ts | 4 ++-- src/mono/wasm/runtime/startup.ts | 14 ++++++-------- src/mono/wasm/runtime/types/emscripten.ts | 3 ++- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/mono/wasm/runtime/dotnet.d.ts b/src/mono/wasm/runtime/dotnet.d.ts index 91f9d52544701..1059561aa6bf5 100644 --- a/src/mono/wasm/runtime/dotnet.d.ts +++ b/src/mono/wasm/runtime/dotnet.d.ts @@ -179,8 +179,8 @@ interface AssetEntry extends ResourceRequest { } type AssetBehaviours = "resource" | "assembly" | "pdb" | "heap" | "icu" | "vfs" | "dotnetwasm" | "js-module-threads"; type GlobalizationMode = "icu" | // load ICU globalization data from any runtime assets with behavior "icu". - "invariant" | // operate in invariant globalization mode. - "auto"; +"invariant" | // operate in invariant globalization mode. +"auto"; type DotnetModuleConfig = { disableDotnet6Compatibility?: boolean; config?: MonoConfig; diff --git a/src/mono/wasm/runtime/startup.ts b/src/mono/wasm/runtime/startup.ts index 3fb7f7a9f8edf..4606ddf66e6b6 100644 --- a/src/mono/wasm/runtime/startup.ts +++ b/src/mono/wasm/runtime/startup.ts @@ -214,9 +214,6 @@ async function preRunAsync(userPreRun: (() => void)[]) { if (runtimeHelpers.diagnosticTracing) console.debug("MONO_WASM: preRunAsync"); const mark = startMeasure(); try { - if (MonoWasmThreads) { - await instantiateWasmPThreadWorkerPool(); - } // all user Module.preRun callbacks userPreRun.map(fn => fn()); endMeasure(mark, MeasuredBlock.preRun); @@ -242,9 +239,6 @@ async function onRuntimeInitializedAsync(userOnRuntimeInitialized: () => void) { // load runtime await mono_wasm_before_user_runtime_initialized(); - if (config.runtimeOptions) { - mono_wasm_set_runtime_options(config.runtimeOptions); - } // call user code try { userOnRuntimeInitialized(); @@ -329,6 +323,9 @@ async function mono_wasm_pre_init_essential_async(): Promise { await init_polyfills_async(); await mono_wasm_load_config(Module.configSrc); + if (MonoWasmThreads) { + preAllocatePThreadWorkerPool(MONO_PTHREAD_POOL_SIZE, config); + } Module.removeRunDependency("mono_wasm_pre_init_essential_async"); } @@ -358,7 +355,7 @@ async function mono_wasm_before_user_runtime_initialized(): Promise { runtimeHelpers.storeMemorySnapshotPending = false; } if (MonoWasmThreads) { - preAllocatePThreadWorkerPool(MONO_PTHREAD_POOL_SIZE, config); + await instantiateWasmPThreadWorkerPool(); await mono_wasm_init_diagnostics(); } bindings_init(); @@ -496,7 +493,8 @@ async function instantiate_wasm_module( if (runtimeHelpers.loadMemorySnapshot) { try { - const wasmMemory = Module.asm.memory; + const wasmMemory = (Module.asm?.memory || Module.wasmMemory)!; + // .grow() takes a delta compared to the previous size wasmMemory.grow((memorySize! - wasmMemory.buffer.byteLength + 65535) >>> 16); runtimeHelpers.updateGlobalBufferAndViews(wasmMemory.buffer); diff --git a/src/mono/wasm/runtime/types/emscripten.ts b/src/mono/wasm/runtime/types/emscripten.ts index 67f9f17b0b3fe..a01423f9a0aaa 100644 --- a/src/mono/wasm/runtime/types/emscripten.ts +++ b/src/mono/wasm/runtime/types/emscripten.ts @@ -70,7 +70,8 @@ export declare interface EmscriptenModuleInternal { mainScriptUrlOrBlob?: string; wasmModule: WebAssembly.Instance | null; ready: Promise; - asm: { memory: WebAssembly.Memory }; + asm: { memory?: WebAssembly.Memory }; + wasmMemory?: WebAssembly.Memory; getWasmTableEntry(index: number): any; removeRunDependency(id: string): void; addRunDependency(id: string): void; From 977b46c8d2004cffa1a9a8139aa7ddd316ce855c Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 8 Mar 2023 19:58:31 +0100 Subject: [PATCH 21/45] whitespace --- src/mono/wasm/runtime/startup.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/mono/wasm/runtime/startup.ts b/src/mono/wasm/runtime/startup.ts index 4606ddf66e6b6..8d0f097c47d0b 100644 --- a/src/mono/wasm/runtime/startup.ts +++ b/src/mono/wasm/runtime/startup.ts @@ -323,6 +323,7 @@ async function mono_wasm_pre_init_essential_async(): Promise { await init_polyfills_async(); await mono_wasm_load_config(Module.configSrc); + if (MonoWasmThreads) { preAllocatePThreadWorkerPool(MONO_PTHREAD_POOL_SIZE, config); } From 933594b6bcba942b14b95e47277789faac7741c6 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Thu, 9 Mar 2023 10:49:07 +0100 Subject: [PATCH 22/45] fix --- src/mono/wasm/runtime/startup.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/mono/wasm/runtime/startup.ts b/src/mono/wasm/runtime/startup.ts index 8d0f097c47d0b..859bfb8f27308 100644 --- a/src/mono/wasm/runtime/startup.ts +++ b/src/mono/wasm/runtime/startup.ts @@ -350,7 +350,9 @@ async function mono_wasm_before_user_runtime_initialized(): Promise { await _apply_configuration_from_args(); mono_wasm_globalization_init(); - if (!runtimeHelpers.mono_wasm_load_runtime_done) mono_wasm_load_runtime("unused", config.debugLevel); + if (!config.startupMemoryCache || !runtimeHelpers.loadMemorySnapshot) { + if (!runtimeHelpers.mono_wasm_load_runtime_done) mono_wasm_load_runtime("unused", config.debugLevel); + } if (config.startupMemoryCache && !runtimeHelpers.loadMemorySnapshot) { await storeMemorySnapshot(Module.HEAP8.buffer); runtimeHelpers.storeMemorySnapshotPending = false; From c68c9e8c04a5de6241261b8438784a34586279d6 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Thu, 9 Mar 2023 11:36:34 +0100 Subject: [PATCH 23/45] fix storing memory --- src/mono/wasm/runtime/storage.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/mono/wasm/runtime/storage.ts b/src/mono/wasm/runtime/storage.ts index 9228e1e6077ed..bee634df7f7ed 100644 --- a/src/mono/wasm/runtime/storage.ts +++ b/src/mono/wasm/runtime/storage.ts @@ -3,6 +3,7 @@ import ProductVersion from "consts:productVersion"; import GitHash from "consts:gitHash"; +import MonoWasmThreads from "consts:monoWasmThreads"; import { runtimeHelpers } from "./imports"; const memoryPrefix = "https://dotnet.generated.invalid/wasm-memory"; @@ -96,8 +97,12 @@ export async function storeMemorySnapshot(memory: ArrayBuffer) { if (!cache) { return; } + const copy = MonoWasmThreads + // storing SHaredArrayBuffer in the cache is not working + ? (new Int8Array(memory)).slice(0) + : memory; - const responseToCache = new Response(memory, { + const responseToCache = new Response(copy, { headers: { "content-type": "wasm-memory", "content-length": memory.byteLength.toString(), From fa3e331768da56a1ccc1465d1c638c1be96d23d3 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Thu, 9 Mar 2023 12:23:07 +0100 Subject: [PATCH 24/45] assert --- src/mono/wasm/runtime/startup.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/mono/wasm/runtime/startup.ts b/src/mono/wasm/runtime/startup.ts index 859bfb8f27308..404db0e23751c 100644 --- a/src/mono/wasm/runtime/startup.ts +++ b/src/mono/wasm/runtime/startup.ts @@ -4,7 +4,7 @@ import BuildConfiguration from "consts:configuration"; import MonoWasmThreads from "consts:monoWasmThreads"; import WasmEnableLegacyJsInterop from "consts:WasmEnableLegacyJsInterop"; -import { CharPtrNull, DotnetModule, RuntimeAPI, MonoConfig, MonoConfigInternal, DotnetModuleInternal } from "./types"; +import { CharPtrNull, DotnetModule, RuntimeAPI, MonoConfig, MonoConfigInternal, DotnetModuleInternal, mono_assert } from "./types"; import { ENVIRONMENT_IS_NODE, ENVIRONMENT_IS_SHELL, INTERNAL, Module, runtimeHelpers } from "./imports"; import cwraps, { init_c_exports } from "./cwraps"; import { mono_wasm_raise_debug_event, mono_wasm_runtime_ready } from "./debug"; @@ -504,6 +504,7 @@ async function instantiate_wasm_module( // get the bytes after we re-sized the memory, so that we don't have too much memory in use at the same time const memoryBytes = await getMemorySnapshot(); + mono_assert(memoryBytes!.byteLength === memorySize!, "MONO_WASM: Loaded memory is not the expected size"); Module.HEAP8.set(new Int8Array(memoryBytes!), 0); if (runtimeHelpers.diagnosticTracing) console.debug("MONO_WASM: Loaded memory from cache"); } catch (err) { From c7f913b0548bc0abe8d439b294c7ad89bbbe2f8d Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Thu, 9 Mar 2023 14:01:04 +0100 Subject: [PATCH 25/45] - make snapshot hash more resilient - silent dry run in unit test --- src/mono/wasm/runtime/pthreads/worker/index.ts | 1 - src/mono/wasm/runtime/storage.ts | 15 ++++++++++++++- src/mono/wasm/test-main.js | 8 ++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/mono/wasm/runtime/pthreads/worker/index.ts b/src/mono/wasm/runtime/pthreads/worker/index.ts index ba411a70870f1..da4c780804c55 100644 --- a/src/mono/wasm/runtime/pthreads/worker/index.ts +++ b/src/mono/wasm/runtime/pthreads/worker/index.ts @@ -69,7 +69,6 @@ export function setupPreloadChannelToMainThread() { 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); workerPort.close(); mainPort.close(); diff --git a/src/mono/wasm/runtime/storage.ts b/src/mono/wasm/runtime/storage.ts index bee634df7f7ed..356d5e278b138 100644 --- a/src/mono/wasm/runtime/storage.ts +++ b/src/mono/wasm/runtime/storage.ts @@ -144,10 +144,23 @@ export async function getInputsHash(): Promise { // above already has env variables, runtime options, etc // above also already has config.assetsHash for this. It has all the asserts (DLLs, ICU, .wasms, etc). // So we could remove assets collectionfrom the hash. - inputs.assets = null; + delete inputs.assets; // some things are calculated at runtime, so we need to add them to the hash inputs.preferredIcuAsset = runtimeHelpers.preferredIcuAsset; inputs.timezone = runtimeHelpers.timezone; + // some things are not relevant for memory snapshot + delete inputs.forwardConsoleLogsToWS; + delete inputs.diagnosticTracing; + delete inputs.appendElementOnExit; + delete inputs.logExitCode; + delete inputs.pthreadPoolSize; + delete inputs.asyncFlushOnExit; + delete inputs.assemblyRootFolder; + delete inputs.remoteSources; + delete inputs.ignorePdbLoadErrors; + delete inputs.maxParallelDownloads; + delete inputs.enableDownloadRetry; + const inputsJson = JSON.stringify(inputs); const sha256Buffer = await runtimeHelpers.subtle.digest("SHA-256", new TextEncoder().encode(inputsJson)); const uint8ViewOfHash = new Uint8Array(sha256Buffer); diff --git a/src/mono/wasm/test-main.js b/src/mono/wasm/test-main.js index cc45bc1b32b33..162b562c6044b 100644 --- a/src/mono/wasm/test-main.js +++ b/src/mono/wasm/test-main.js @@ -290,6 +290,14 @@ async function run() { const { dotnet: dry_run, exit: dry_exit, INTERNAL: DRY_INTERNAL } = await loadDotnet('./dotnet.js?dry-run=true'); mono_exit = dry_exit; configureRuntime(dry_run, runArgs, DRY_INTERNAL); + // silent minimal startup + dry_run.withConfig({ + forwardConsoleLogsToWS: false, + diagnosticTracing: false, + appendElementOnExit: false, + logExitCode: false, + pthreadPoolSize: 0, + }); await dry_run.create(); } From 9cb1b726318dda4729adf41018073e6eabaac7ce Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Thu, 9 Mar 2023 14:22:27 +0100 Subject: [PATCH 26/45] fix --- src/mono/wasm/test-main.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/mono/wasm/test-main.js b/src/mono/wasm/test-main.js index 162b562c6044b..14d2d281c8678 100644 --- a/src/mono/wasm/test-main.js +++ b/src/mono/wasm/test-main.js @@ -291,14 +291,15 @@ async function run() { mono_exit = dry_exit; configureRuntime(dry_run, runArgs, DRY_INTERNAL); // silent minimal startup - dry_run.withConfig({ - forwardConsoleLogsToWS: false, - diagnosticTracing: false, - appendElementOnExit: false, - logExitCode: false, - pthreadPoolSize: 0, - }); - await dry_run.create(); + await dry_run.withModuleConfig({ + onConfigLoaded: (config) => { + config.forwardConsoleLogsToWS = false; + config.diagnosticTracing = false; + config.appendElementOnExit = false; + config.logExitCode = false; + config.pthreadPoolSize = 0; + } + }).create(); } // this is subsequent run with the actual tests. It will use whatever was cached in the previous run. From d1bbb9ca038d5cd1aca1665e45e148921614351f Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Thu, 9 Mar 2023 14:53:30 +0100 Subject: [PATCH 27/45] - renamed memorySnapshotSkippedOrDone - asset loading feedback - removed mono_wasm_load_icu_data from wasm imports --- src/mono/wasm/runtime/assets.ts | 57 ++++++++++----------- src/mono/wasm/runtime/es6/dotnet.es6.lib.js | 3 -- src/mono/wasm/runtime/exports-linker.ts | 4 -- src/mono/wasm/runtime/startup.ts | 10 ++-- 4 files changed, 31 insertions(+), 43 deletions(-) diff --git a/src/mono/wasm/runtime/assets.ts b/src/mono/wasm/runtime/assets.ts index e9784a881d61e..ccef70879bb40 100644 --- a/src/mono/wasm/runtime/assets.ts +++ b/src/mono/wasm/runtime/assets.ts @@ -8,7 +8,7 @@ import { mono_wasm_load_bytes_into_heap } from "./memory"; import { endMeasure, MeasuredBlock, startMeasure } from "./profiler"; import { createPromiseController, PromiseAndController } from "./promise-controller"; import { delay } from "./promise-utils"; -import { abort_startup, beforeOnRuntimeInitialized, memorySnapshotIsResolved } from "./startup"; +import { abort_startup, beforeOnRuntimeInitialized, memorySnapshotSkippedOrDone } from "./startup"; import { AssetBehaviours, AssetEntry, AssetEntryInternal, LoadingResource, mono_assert, ResourceRequest } from "./types"; import { InstantiateWasmSuccessCallback, VoidPtr } from "./types/emscripten"; @@ -19,7 +19,6 @@ let actual_instantiated_assets_count = 0; let expected_downloaded_assets_count = 0; let expected_instantiated_assets_count = 0; const loaded_files: { url: string, file: string }[] = []; -const loaded_assets: { [id: string]: [VoidPtr, number] } = Object.create(null); // in order to prevent net::ERR_INSUFFICIENT_RESOURCES if we start downloading too many files at same time let parallel_count = 0; let throttlingPromise: PromiseAndController | undefined; @@ -77,7 +76,10 @@ export async function mono_download_assets(): Promise { runtimeHelpers.maxParallelDownloads = runtimeHelpers.config.maxParallelDownloads || runtimeHelpers.maxParallelDownloads; runtimeHelpers.enableDownloadRetry = runtimeHelpers.config.enableDownloadRetry || runtimeHelpers.enableDownloadRetry; try { - // just validate all + const beforeSnapshotAssets: AssetEntryInternal[] = []; + const afterSnapshotAssets: AssetEntryInternal[] = []; + const promises_of_assets: Promise[] = []; + for (const a of runtimeHelpers.config.assets!) { const asset: AssetEntryInternal = a; mono_assert(typeof asset === "object", "asset must be object"); @@ -86,41 +88,35 @@ export async function mono_download_assets(): Promise { mono_assert(!asset.resolvedUrl || typeof asset.resolvedUrl === "string", "asset resolvedUrl could be string"); mono_assert(!asset.hash || typeof asset.hash === "string", "asset resolvedUrl could be string"); mono_assert(!asset.pendingDownload || typeof asset.pendingDownload === "object", "asset pendingDownload could be object"); + if (skipInSnapshotByAssetTypes[asset.behavior]) { + afterSnapshotAssets.push(asset); + } else { + beforeSnapshotAssets.push(asset); + } } - - const promises_of_assets: Promise[] = []; - const nonSnapshotAssets: AssetEntryInternal[] = []; - // start fetching and instantiating all assets in parallel - for (const a of runtimeHelpers.config.assets!) { - const asset: AssetEntryInternal = a; - const skipInSnapshot = skipInSnapshotByAssetTypes[asset.behavior]; - if (!skipInSnapshot) { - if (!skipInstantiateByAssetTypes[asset.behavior] && shouldLoadIcuAsset(asset)) { - expected_instantiated_assets_count++; - } - if (!skipDownloadsByAssetTypes[asset.behavior] && shouldLoadIcuAsset(asset)) { - expected_downloaded_assets_count++; - promises_of_assets.push(start_asset_download(asset)); - } - } else { - nonSnapshotAssets.push(asset); + const countAndStartDownload = (asset: AssetEntryInternal) => { + if (!skipInstantiateByAssetTypes[asset.behavior] && shouldLoadIcuAsset(asset)) { + expected_instantiated_assets_count++; + } + if (!skipDownloadsByAssetTypes[asset.behavior] && shouldLoadIcuAsset(asset)) { + expected_downloaded_assets_count++; + promises_of_assets.push(start_asset_download(asset)); } + }; + + // start fetching and assets in parallel, only assets which are not part of memory snapshot + for (const asset of beforeSnapshotAssets) { + countAndStartDownload(asset); } // continue after we know if memory snapshot is available or not - await memorySnapshotIsResolved.promise; + await memorySnapshotSkippedOrDone.promise; - // and load the rest of the assets if necessary + // start fetching and assets in parallel, only if memory snapshot is not available if (!runtimeHelpers.loadMemorySnapshot) { - for (const asset of nonSnapshotAssets) { - if (!skipInstantiateByAssetTypes[asset.behavior] && shouldLoadIcuAsset(asset)) { - expected_instantiated_assets_count++; - } - if (!skipDownloadsByAssetTypes[asset.behavior] && shouldLoadIcuAsset(asset)) { - expected_downloaded_assets_count++; - promises_of_assets.push(start_asset_download(asset)); - } + for (const asset of afterSnapshotAssets) { + countAndStartDownload(asset); } } @@ -415,7 +411,6 @@ function _instantiate_asset(asset: AssetEntry, url: string, bytes: Uint8Array) { case "heap": case "icu": offset = mono_wasm_load_bytes_into_heap(bytes); - loaded_assets[virtualName] = [offset, bytes.length]; break; case "vfs": { diff --git a/src/mono/wasm/runtime/es6/dotnet.es6.lib.js b/src/mono/wasm/runtime/es6/dotnet.es6.lib.js index 6c511fe49347f..04bc10482b447 100644 --- a/src/mono/wasm/runtime/es6/dotnet.es6.lib.js +++ b/src/mono/wasm/runtime/es6/dotnet.es6.lib.js @@ -97,9 +97,6 @@ let linked_functions = [ "mono_wasm_invoke_import", "mono_wasm_bind_cs_function", "mono_wasm_marshal_promise", - - // pal_icushim_static.c - "mono_wasm_load_icu_data", ]; if (monoWasmThreads) { diff --git a/src/mono/wasm/runtime/exports-linker.ts b/src/mono/wasm/runtime/exports-linker.ts index dbfa140f47c3b..f5f55de63f128 100644 --- a/src/mono/wasm/runtime/exports-linker.ts +++ b/src/mono/wasm/runtime/exports-linker.ts @@ -5,7 +5,6 @@ import MonoWasmThreads from "consts:monoWasmThreads"; import WasmEnableLegacyJsInterop from "consts:WasmEnableLegacyJsInterop"; import { mono_wasm_debugger_log, mono_wasm_add_dbg_command_received, mono_wasm_set_entrypoint_breakpoint, mono_wasm_fire_debugger_agent_message_with_data, mono_wasm_fire_debugger_agent_message_with_data_to_pause } from "./debug"; import { mono_wasm_release_cs_owned_object } from "./gc-handles"; -import { mono_wasm_load_icu_data } from "./icu"; import { mono_wasm_bind_cs_function } from "./invoke-cs"; import { mono_wasm_bind_js_function, mono_wasm_invoke_bound_function, mono_wasm_invoke_import } from "./invoke-js"; import { mono_interp_tier_prepare_jiterpreter } from "./jiterpreter"; @@ -95,9 +94,6 @@ export function export_linker(): any { mono_wasm_bind_cs_function, mono_wasm_marshal_promise, - // pal_icushim_static.c - mono_wasm_load_icu_data, - // threading exports, if threading is enabled ...mono_wasm_threads_exports, // legacy interop exports, if enabled diff --git a/src/mono/wasm/runtime/startup.ts b/src/mono/wasm/runtime/startup.ts index 404db0e23751c..491408f478b75 100644 --- a/src/mono/wasm/runtime/startup.ts +++ b/src/mono/wasm/runtime/startup.ts @@ -39,7 +39,7 @@ let configLoaded = false; export const dotnetReady = createPromiseController(); export const afterConfigLoaded = createPromiseController(); export const beforeInstantiateWasm = createPromiseController(); -export const memorySnapshotIsResolved = createPromiseController(); +export const memorySnapshotSkippedOrDone = createPromiseController(); export const afterInstantiateWasm = createPromiseController(); export const beforePreInit = createPromiseController(); export const afterPreInit = createPromiseController(); @@ -111,7 +111,7 @@ function instantiateWasm( if (userInstantiateWasm) { // user wasm doesn't support memory snapshots beforeInstantiateWasm.promise_control.resolve(); - memorySnapshotIsResolved.promise_control.resolve(); + memorySnapshotSkippedOrDone.promise_control.resolve(); const exports = userInstantiateWasm(imports, (instance: WebAssembly.Instance, module: WebAssembly.Module | undefined) => { endMeasure(mark, MeasuredBlock.instantiateWasm); afterInstantiateWasm.promise_control.resolve(); @@ -282,7 +282,7 @@ export function abort_startup(reason: any, should_exit: boolean): void { if (runtimeHelpers.diagnosticTracing) console.trace("MONO_WASM: abort_startup"); dotnetReady.promise_control.reject(reason); beforeInstantiateWasm.promise_control.reject(reason); - memorySnapshotIsResolved.promise_control.reject(reason); + memorySnapshotSkippedOrDone.promise_control.reject(reason); afterInstantiateWasm.promise_control.reject(reason); beforePreInit.promise_control.reject(reason); afterPreInit.promise_control.reject(reason); @@ -481,7 +481,7 @@ async function instantiate_wasm_module( } if (!runtimeHelpers.loadMemorySnapshot) { // we should start downloading DLLs etc as they are not in the snapshot - memorySnapshotIsResolved.promise_control.resolve(); + memorySnapshotSkippedOrDone.promise_control.resolve(); } await beforePreInit.promise; @@ -512,7 +512,7 @@ async function instantiate_wasm_module( runtimeHelpers.loadMemorySnapshot = false; } // now we know if the loading of memory succeeded or not, we can start loading the rest of the assets - memorySnapshotIsResolved.promise_control.resolve(); + memorySnapshotSkippedOrDone.promise_control.resolve(); } afterInstantiateWasm.promise_control.resolve(); } catch (err) { From ee4ba766241a7e1a4a99d25a32b58710bb8a268e Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Thu, 9 Mar 2023 15:55:23 +0100 Subject: [PATCH 28/45] - rename loadedMemorySnapshot - drop mono_wasm_load_runtime_done - reorder startup --- src/mono/wasm/runtime/assets.ts | 6 +- src/mono/wasm/runtime/imports.ts | 1 - src/mono/wasm/runtime/startup.ts | 127 +++++++++++++++---------------- src/mono/wasm/runtime/types.ts | 3 +- 4 files changed, 65 insertions(+), 72 deletions(-) diff --git a/src/mono/wasm/runtime/assets.ts b/src/mono/wasm/runtime/assets.ts index ccef70879bb40..27e074b63205c 100644 --- a/src/mono/wasm/runtime/assets.ts +++ b/src/mono/wasm/runtime/assets.ts @@ -114,7 +114,7 @@ export async function mono_download_assets(): Promise { await memorySnapshotSkippedOrDone.promise; // start fetching and assets in parallel, only if memory snapshot is not available - if (!runtimeHelpers.loadMemorySnapshot) { + if (!runtimeHelpers.loadedMemorySnapshot) { for (const asset of afterSnapshotAssets) { countAndStartDownload(asset); } @@ -135,8 +135,10 @@ export async function mono_download_assets(): Promise { asset.pendingDownload = null as any; // GC asset.buffer = null as any; // GC + // wait till after onRuntimeInitialized and after memory snapshot is loaded or skipped + await memorySnapshotSkippedOrDone.promise; await beforeOnRuntimeInitialized.promise; - // this is after onRuntimeInitialized + _instantiate_asset(asset, url, data); } } else { diff --git a/src/mono/wasm/runtime/imports.ts b/src/mono/wasm/runtime/imports.ts index fe2c256174705..e15015cc8b91e 100644 --- a/src/mono/wasm/runtime/imports.ts +++ b/src/mono/wasm/runtime/imports.ts @@ -51,7 +51,6 @@ export function set_emscripten_entrypoint( const initialRuntimeHelpers: Partial = { javaScriptExports: {} as any, - mono_wasm_load_runtime_done: false, mono_wasm_bindings_is_ready: false, maxParallelDownloads: 16, enableDownloadRetry: true, diff --git a/src/mono/wasm/runtime/startup.ts b/src/mono/wasm/runtime/startup.ts index 491408f478b75..88a1a7cacf93c 100644 --- a/src/mono/wasm/runtime/startup.ts +++ b/src/mono/wasm/runtime/startup.ts @@ -236,8 +236,23 @@ async function onRuntimeInitializedAsync(userOnRuntimeInitialized: () => void) { beforeOnRuntimeInitialized.promise_control.resolve(); try { await wait_for_all_assets(); - // load runtime - await mono_wasm_before_user_runtime_initialized(); + + // load runtime and apply environment settings (if necessary) + await mono_wasm_before_memory_snapshot(); + + if (MonoWasmThreads) { + await instantiateWasmPThreadWorkerPool(); + await mono_wasm_init_diagnostics(); + } + + bindings_init(); + if (!runtimeHelpers.mono_wasm_runtime_is_ready) mono_wasm_runtime_ready(); + if (!runtimeHelpers.mono_wasm_symbols_are_ready) readSymbolMapFile("dotnet.js.symbols"); + + setTimeout(() => { + // when there are free CPU cycles + string_decoder.init_fields(); + }); // call user code try { @@ -265,6 +280,11 @@ async function postRunAsync(userpostRun: (() => void)[]) { if (runtimeHelpers.diagnosticTracing) console.debug("MONO_WASM: postRunAsync"); try { const mark = startMeasure(); + + // create /usr/share folder which is SpecialFolder.CommonApplicationData + Module["FS_createPath"]("/", "usr", true, true); + Module["FS_createPath"]("/", "usr/share", true, true); + // all user Module.postRun callbacks userpostRun.map(fn => fn()); endMeasure(mark, MeasuredBlock.postRun); @@ -343,35 +363,37 @@ async function mono_wasm_pre_init_full(): Promise { Module.removeRunDependency("mono_wasm_pre_init_full"); } -async function mono_wasm_before_user_runtime_initialized(): Promise { - if (runtimeHelpers.diagnosticTracing) console.debug("MONO_WASM: mono_wasm_before_user_runtime_initialized"); +async function mono_wasm_before_memory_snapshot() { + if (runtimeHelpers.loadedMemorySnapshot) { + // all things below are loaded from the snapshot + return; + } - try { - await _apply_configuration_from_args(); - mono_wasm_globalization_init(); + for (const k in config.environmentVariables) { + const v = config.environmentVariables![k]; + if (typeof (v) === "string") + mono_wasm_setenv(k, v); + else + throw new Error(`Expected environment variable '${k}' to be a string but it was ${typeof v}: '${v}'`); + } - if (!config.startupMemoryCache || !runtimeHelpers.loadMemorySnapshot) { - if (!runtimeHelpers.mono_wasm_load_runtime_done) mono_wasm_load_runtime("unused", config.debugLevel); - } - if (config.startupMemoryCache && !runtimeHelpers.loadMemorySnapshot) { - await storeMemorySnapshot(Module.HEAP8.buffer); - runtimeHelpers.storeMemorySnapshotPending = false; - } - if (MonoWasmThreads) { - await instantiateWasmPThreadWorkerPool(); - await mono_wasm_init_diagnostics(); - } - bindings_init(); - if (!runtimeHelpers.mono_wasm_runtime_is_ready) mono_wasm_runtime_ready(); - if (!runtimeHelpers.mono_wasm_symbols_are_ready) readSymbolMapFile("dotnet.js.symbols"); + if (config.runtimeOptions) + mono_wasm_set_runtime_options(config.runtimeOptions); - setTimeout(() => { - // when there are free CPU cycles - string_decoder.init_fields(); - }); - } catch (err: any) { - _print_error("MONO_WASM: Error in mono_wasm_before_user_runtime_initialized", err); - throw err; + if (config.aotProfilerOptions) + mono_wasm_init_aot_profiler(config.aotProfilerOptions); + + if (config.browserProfilerOptions) + mono_wasm_init_browser_profiler(config.browserProfilerOptions); + + mono_wasm_globalization_init(); + + mono_wasm_load_runtime("unused", config.debugLevel); + + // we didn't have snapshot yet and the feature is enabled. Take snapshot now. + if (config.startupMemoryCache) { + await storeMemorySnapshot(Module.HEAP8.buffer); + runtimeHelpers.storeMemorySnapshotPending = false; } } @@ -476,10 +498,10 @@ async function instantiate_wasm_module( if (config.startupMemoryCache && config.assetsHash) { memorySize = await getMemorySnapshotSize(); - runtimeHelpers.loadMemorySnapshot = !!memorySize; - runtimeHelpers.storeMemorySnapshotPending = !runtimeHelpers.loadMemorySnapshot; + runtimeHelpers.loadedMemorySnapshot = !!memorySize; + runtimeHelpers.storeMemorySnapshotPending = !runtimeHelpers.loadedMemorySnapshot; } - if (!runtimeHelpers.loadMemorySnapshot) { + if (!runtimeHelpers.loadedMemorySnapshot) { // we should start downloading DLLs etc as they are not in the snapshot memorySnapshotSkippedOrDone.promise_control.resolve(); } @@ -494,7 +516,7 @@ async function instantiate_wasm_module( assetToLoad.buffer = null as any; // GC if (runtimeHelpers.diagnosticTracing) console.debug("MONO_WASM: instantiate_wasm_module done"); - if (runtimeHelpers.loadMemorySnapshot) { + if (runtimeHelpers.loadedMemorySnapshot) { try { const wasmMemory = (Module.asm?.memory || Module.wasmMemory)!; @@ -509,7 +531,7 @@ async function instantiate_wasm_module( if (runtimeHelpers.diagnosticTracing) console.debug("MONO_WASM: Loaded memory from cache"); } catch (err) { console.warn("MONO_WASM: failed to load memory snapshot", err); - runtimeHelpers.loadMemorySnapshot = false; + runtimeHelpers.loadedMemorySnapshot = false; } // now we know if the loading of memory succeeded or not, we can start loading the rest of the assets memorySnapshotSkippedOrDone.promise_control.resolve(); @@ -523,46 +545,17 @@ async function instantiate_wasm_module( Module.removeRunDependency("instantiate_wasm_module"); } -async function _apply_configuration_from_args() { - // create /usr/share folder which is SpecialFolder.CommonApplicationData - Module["FS_createPath"]("/", "usr", true, true); - Module["FS_createPath"]("/", "usr/share", true, true); - - for (const k in config.environmentVariables) { - const v = config.environmentVariables![k]; - if (typeof (v) === "string") - mono_wasm_setenv(k, v); - else - throw new Error(`Expected environment variable '${k}' to be a string but it was ${typeof v}: '${v}'`); - } - - if (config.runtimeOptions) - mono_wasm_set_runtime_options(config.runtimeOptions); - - if (config.aotProfilerOptions) - mono_wasm_init_aot_profiler(config.aotProfilerOptions); - - if (config.browserProfilerOptions) - mono_wasm_init_browser_profiler(config.browserProfilerOptions); -} - export function mono_wasm_load_runtime(unused?: string, debugLevel?: number): void { if (runtimeHelpers.diagnosticTracing) console.debug("MONO_WASM: mono_wasm_load_runtime"); - if (runtimeHelpers.mono_wasm_load_runtime_done) { - return; - } - runtimeHelpers.mono_wasm_load_runtime_done = true; try { const mark = startMeasure(); - if (!runtimeHelpers.loadMemorySnapshot) { - if (debugLevel == undefined) { - debugLevel = 0; - if (config && config.debugLevel) { - debugLevel = 0 + debugLevel; - } + if (debugLevel == undefined) { + debugLevel = 0; + if (config && config.debugLevel) { + debugLevel = 0 + debugLevel; } - cwraps.mono_wasm_load_runtime(unused || "unused", debugLevel); } + cwraps.mono_wasm_load_runtime(unused || "unused", debugLevel); endMeasure(mark, MeasuredBlock.loadRuntime); if (!config.startupMemoryCache) bindings_init(); diff --git a/src/mono/wasm/runtime/types.ts b/src/mono/wasm/runtime/types.ts index 63309e516f0d4..bc818b4fbfdb5 100644 --- a/src/mono/wasm/runtime/types.ts +++ b/src/mono/wasm/runtime/types.ts @@ -213,7 +213,6 @@ export type RuntimeHelpers = { runtime_interop_exports_class: MonoClass; _i52_error_scratch_buffer: Int32Ptr; - mono_wasm_load_runtime_done: boolean; mono_wasm_runtime_is_ready: boolean; mono_wasm_bindings_is_ready: boolean; mono_wasm_symbols_are_ready: boolean; @@ -233,7 +232,7 @@ export type RuntimeHelpers = { locateFile: (path: string, prefix?: string) => string, javaScriptExports: JavaScriptExports, loadedFiles: string[], - loadMemorySnapshot: boolean, + loadedMemorySnapshot: boolean, storeMemorySnapshotPending: boolean, subtle: SubtleCrypto | null, preferredIcuAsset: string | null, From 85ac81dd2ffbd473baf5effb4c89ef2e723fcf16 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Thu, 9 Mar 2023 17:12:28 +0100 Subject: [PATCH 29/45] fix --- src/mono/wasm/runtime/startup.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/mono/wasm/runtime/startup.ts b/src/mono/wasm/runtime/startup.ts index 88a1a7cacf93c..f21282648d4a2 100644 --- a/src/mono/wasm/runtime/startup.ts +++ b/src/mono/wasm/runtime/startup.ts @@ -182,6 +182,7 @@ async function preInitWorkerAsync() { const mark = startMeasure(); try { if (runtimeHelpers.diagnosticTracing) console.debug("MONO_WASM: preInitWorker"); + beforePreInit.promise_control.resolve(); mono_wasm_pre_init_essential(true); await init_polyfills_async(); afterPreInit.promise_control.resolve(); From 3168c422ac83ca1654bcf35bb816300958b1a855 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Fri, 10 Mar 2023 17:14:04 +0100 Subject: [PATCH 30/45] - internal config exitAfterSnapshot - do not emit default pthreadPoolSize to mono-config - prefer to propagate exit code from exception in set_exit_code_and_quit_now - allow silent errors in err.silent in _print_error - postpone mono_wasm_init_diagnostics when startupMemoryCache is enabled - restore memory bytes only after emscripten's emscripten_environ_constructor --- src/mono/wasm/build/WasmApp.targets | 2 +- src/mono/wasm/runtime/assets.ts | 1 - .../diagnostics/server_pthread/index.ts | 4 +- src/mono/wasm/runtime/run.ts | 4 +- src/mono/wasm/runtime/startup.ts | 54 +++++++++++-------- src/mono/wasm/runtime/storage.ts | 1 + src/mono/wasm/runtime/types.ts | 1 + src/mono/wasm/test-main.js | 48 +++++++++++------ src/tasks/WasmAppBuilder/WasmAppBuilder.cs | 2 +- 9 files changed, 73 insertions(+), 44 deletions(-) diff --git a/src/mono/wasm/build/WasmApp.targets b/src/mono/wasm/build/WasmApp.targets index 6cf8f68324760..fb19faaa9021f 100644 --- a/src/mono/wasm/build/WasmApp.targets +++ b/src/mono/wasm/build/WasmApp.targets @@ -362,7 +362,7 @@ <_WasmAppIncludeThreadsWorker Condition="'$(WasmEnableThreads)' == 'true' or '$(WasmEnablePerfTracing)' == 'true'">true - <_WasmPThreadPoolSize Condition="'$(_WasmPThreadPoolSize)' == '' and ('$(WasmEnableThreads)' == 'true' or '$(WasmEnablePerfTracing)' == 'true')">-1 + <_WasmPThreadPoolSize Condition="'$(_WasmPThreadPoolSize)' == ''">-1 diff --git a/src/mono/wasm/runtime/assets.ts b/src/mono/wasm/runtime/assets.ts index 27e074b63205c..895c4a7c06da3 100644 --- a/src/mono/wasm/runtime/assets.ts +++ b/src/mono/wasm/runtime/assets.ts @@ -138,7 +138,6 @@ export async function mono_download_assets(): Promise { // wait till after onRuntimeInitialized and after memory snapshot is loaded or skipped await memorySnapshotSkippedOrDone.promise; await beforeOnRuntimeInitialized.promise; - _instantiate_asset(asset, url, data); } } else { diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts index 01587e0bc8511..4f757d092970c 100644 --- a/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts +++ b/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts @@ -230,7 +230,7 @@ class DiagnosticServerImpl implements DiagnosticServer { } async stopEventPipe(ws: WebSocket | MockRemoteSocket, sessionID: EventPipeSessionIDImpl): Promise { - console.info("MONO_WASM: stopEventPipe", sessionID); + console.debug("MONO_WASM: stopEventPipe", sessionID); cwraps.mono_wasm_event_pipe_session_disable(sessionID); // we might send OK before the session is actually stopped since the websocket is async // but the client end should be robust to that. @@ -266,7 +266,7 @@ class DiagnosticServerImpl implements DiagnosticServer { resumeRuntime(): void { if (!this.runtimeResumed) { - console.info("MONO_WASM: resuming runtime startup"); + console.debug("MONO_WASM: resuming runtime startup"); cwraps.mono_wasm_diagnostic_server_post_resume_runtime(); this.runtimeResumed = true; } diff --git a/src/mono/wasm/runtime/run.ts b/src/mono/wasm/runtime/run.ts index 2e624bcf2f181..33db38a5dbb62 100644 --- a/src/mono/wasm/runtime/run.ts +++ b/src/mono/wasm/runtime/run.ts @@ -118,8 +118,10 @@ function set_exit_code_and_quit_now(exit_code: number, reason?: any): void { Module.printErr(JSON.stringify(reason)); } } - else { + else if (!reason) { reason = new runtimeHelpers.ExitStatus(exit_code); + } else { + exit_code = reason.status; } } logErrorOnExit(exit_code, reason); diff --git a/src/mono/wasm/runtime/startup.ts b/src/mono/wasm/runtime/startup.ts index 957c1a2ae6909..2e1851dcbfd98 100644 --- a/src/mono/wasm/runtime/startup.ts +++ b/src/mono/wasm/runtime/startup.ts @@ -38,7 +38,6 @@ let config: MonoConfigInternal = undefined as any; let configLoaded = false; export const dotnetReady = createPromiseController(); export const afterConfigLoaded = createPromiseController(); -export const beforeInstantiateWasm = createPromiseController(); export const memorySnapshotSkippedOrDone = createPromiseController(); export const afterInstantiateWasm = createPromiseController(); export const beforePreInit = createPromiseController(); @@ -110,7 +109,6 @@ function instantiateWasm( const mark = startMeasure(); if (userInstantiateWasm) { // user wasm instantiation doesn't support memory snapshots - beforeInstantiateWasm.promise_control.resolve(); memorySnapshotSkippedOrDone.promise_control.resolve(); const exports = userInstantiateWasm(imports, (instance: WebAssembly.Instance, module: WebAssembly.Module | undefined) => { endMeasure(mark, MeasuredBlock.instantiateWasm); @@ -238,13 +236,31 @@ async function onRuntimeInitializedAsync(userOnRuntimeInitialized: () => void) { try { await wait_for_all_assets(); + // Diagnostics early are not supported with memory snapshot. See below how we enable them later. + // Please disable startupMemoryCache in order to be able to diagnose or pause runtime startup. + if (MonoWasmThreads && !config.startupMemoryCache) { + await mono_wasm_init_diagnostics(); + } + // load runtime and apply environment settings (if necessary) await mono_wasm_before_memory_snapshot(); + if (config.exitAfterSnapshot) { + const reason = runtimeHelpers.ExitStatus + ? new runtimeHelpers.ExitStatus(0) + : new Error("Snapshot taken, exiting because exitAfterSnapshot was set."); + reason.silent = true; + + abort_startup(reason, false); + return; + } + if (MonoWasmThreads) { + if (config.startupMemoryCache) { + // we could enable diagnostics after the snapshot is taken + await mono_wasm_init_diagnostics(); + } await instantiateWasmPThreadWorkerPool(); - // FIXME - await mono_wasm_init_diagnostics(); } bindings_init(); @@ -303,7 +319,6 @@ async function postRunAsync(userpostRun: (() => void)[]) { export function abort_startup(reason: any, should_exit: boolean): void { if (runtimeHelpers.diagnosticTracing) console.trace("MONO_WASM: abort_startup"); dotnetReady.promise_control.reject(reason); - beforeInstantiateWasm.promise_control.reject(reason); memorySnapshotSkippedOrDone.promise_control.reject(reason); afterInstantiateWasm.promise_control.reject(reason); beforePreInit.promise_control.reject(reason); @@ -312,7 +327,7 @@ export function abort_startup(reason: any, should_exit: boolean): void { beforeOnRuntimeInitialized.promise_control.reject(reason); afterOnRuntimeInitialized.promise_control.reject(reason); afterPostRun.promise_control.reject(reason); - if (should_exit) { + if (should_exit && reason.silent !== true) { mono_exit(1, reason); } } @@ -357,9 +372,6 @@ async function mono_wasm_pre_init_full(): Promise { if (runtimeHelpers.diagnosticTracing) console.debug("MONO_WASM: mono_wasm_pre_init_full"); Module.addRunDependency("mono_wasm_pre_init_full"); - // continue after wasm is downloaded - await beforeInstantiateWasm.promise; - await mono_download_assets(); Module.removeRunDependency("mono_wasm_pre_init_full"); @@ -405,6 +417,9 @@ async function mono_wasm_after_user_runtime_initialized(): Promise { function _print_error(message: string, err: any): void { + if (err.silent) { + return; + } Module.printErr(`${message}: ${JSON.stringify(err)}`); if (err.stack) { Module.printErr("MONO_WASM: Stacktrace: \n"); @@ -462,7 +477,6 @@ async function instantiate_wasm_module( const assetToLoad = resolve_asset_path("dotnetwasm"); // FIXME: this would not apply re-try (on connection reset during download) for dotnet.wasm because we could not download the buffer before we pass it to instantiate_wasm_asset await start_asset_download(assetToLoad); - beforeInstantiateWasm.promise_control.resolve(); if (config.startupMemoryCache && config.assetsHash) { memorySize = await getMemorySnapshotSize(); @@ -492,14 +506,8 @@ async function instantiate_wasm_module( // .grow() takes a delta compared to the previous size wasmMemory.grow((memorySize! - wasmMemory.buffer.byteLength + 65535) >>> 16); runtimeHelpers.updateGlobalBufferAndViews(wasmMemory.buffer); - - // get the bytes after we re-sized the memory, so that we don't have too much memory in use at the same time - const memoryBytes = await getMemorySnapshot(); - mono_assert(memoryBytes!.byteLength === memorySize!, "MONO_WASM: Loaded memory is not the expected size"); - Module.HEAP8.set(new Int8Array(memoryBytes!), 0); - if (runtimeHelpers.diagnosticTracing) console.debug("MONO_WASM: Loaded memory from cache"); } catch (err) { - console.warn("MONO_WASM: failed to load memory snapshot", err); + console.warn("MONO_WASM: failed to resize memory for the snapshot", err); runtimeHelpers.loadedMemorySnapshot = false; } // now we know if the loading of memory succeeded or not, we can start loading the rest of the assets @@ -516,6 +524,13 @@ async function instantiate_wasm_module( async function mono_wasm_before_memory_snapshot() { if (runtimeHelpers.loadedMemorySnapshot) { + // get the bytes after we re-sized the memory, so that we don't have too much memory in use at the same time + const memoryBytes = await getMemorySnapshot(); + mono_assert(memoryBytes!.byteLength === Module.HEAP8.byteLength, "MONO_WASM: Loaded memory is not the expected size"); + Module.HEAP8.set(new Int8Array(memoryBytes!), 0); + if (runtimeHelpers.diagnosticTracing) console.info("MONO_WASM: Loaded WASM linear memory from browser cache"); + + // all things below are loaded from the snapshot return; } @@ -539,11 +554,6 @@ async function mono_wasm_before_memory_snapshot() { mono_wasm_globalization_init(); - // init diagnostics after environment variables are set - if (MonoWasmThreads) { - // FIXME - await mono_wasm_init_diagnostics(); - } mono_wasm_load_runtime("unused", config.debugLevel); diff --git a/src/mono/wasm/runtime/storage.ts b/src/mono/wasm/runtime/storage.ts index 356d5e278b138..b3c9d1c8e4029 100644 --- a/src/mono/wasm/runtime/storage.ts +++ b/src/mono/wasm/runtime/storage.ts @@ -160,6 +160,7 @@ export async function getInputsHash(): Promise { delete inputs.ignorePdbLoadErrors; delete inputs.maxParallelDownloads; delete inputs.enableDownloadRetry; + delete inputs.exitAfterSnapshot; const inputsJson = JSON.stringify(inputs); const sha256Buffer = await runtimeHelpers.subtle.digest("SHA-256", new TextEncoder().encode(inputsJson)); diff --git a/src/mono/wasm/runtime/types.ts b/src/mono/wasm/runtime/types.ts index bc818b4fbfdb5..8f7c5874425b8 100644 --- a/src/mono/wasm/runtime/types.ts +++ b/src/mono/wasm/runtime/types.ts @@ -138,6 +138,7 @@ export type MonoConfigInternal = MonoConfig & { logExitCode?: boolean forwardConsoleLogsToWS?: boolean, asyncFlushOnExit?: boolean + exitAfterSnapshot?: number, }; export type RunArguments = { diff --git a/src/mono/wasm/test-main.js b/src/mono/wasm/test-main.js index 14d2d281c8678..b58212a563a42 100644 --- a/src/mono/wasm/test-main.js +++ b/src/mono/wasm/test-main.js @@ -246,7 +246,7 @@ function configureRuntime(dotnet, runArgs, INTERNAL) { dotnet .withVirtualWorkingDirectory(runArgs.workingDirectory) .withEnvironmentVariables(runArgs.environmentVariables) - //.withDiagnosticTracing(runArgs.diagnosticTracing) + .withDiagnosticTracing(runArgs.diagnosticTracing) .withDiagnosticTracing(true) .withExitOnUnhandledError() .withExitCodeLogging() @@ -281,25 +281,42 @@ function configureRuntime(dotnet, runArgs, INTERNAL) { } } +async function dry_run(runArgs) { + try { + console.log("Silently starting separate runtime instance as another ES6 module to populate caches..."); + // this separate instance of the ES6 module, in which we just populate the caches + const { dotnet, exit, INTERNAL } = await loadDotnet('./dotnet.js?dry_run=true'); + mono_exit = exit; + configureRuntime(dotnet, runArgs, INTERNAL); + // silent minimal startup + await dotnet.withConfig({ + forwardConsoleLogsToWS: false, + diagnosticTracing: false, + appendElementOnExit: false, + logExitCode: false, + pthreadPoolSize: 0, + exitAfterSnapshot: true + }).create(); + } catch (err) { + if (err && err.status !== 0) { + return false; + } + } + console.log("Separate runtime instance finished loading."); + return true; +} + async function run() { try { const runArgs = await getArgs(); + console.log("Application arguments: " + runArgs.applicationArguments.join(' ')); if (is_browser) { - // this separate instance of the ES6 module, in which we just populate the caches - const { dotnet: dry_run, exit: dry_exit, INTERNAL: DRY_INTERNAL } = await loadDotnet('./dotnet.js?dry-run=true'); - mono_exit = dry_exit; - configureRuntime(dry_run, runArgs, DRY_INTERNAL); - // silent minimal startup - await dry_run.withModuleConfig({ - onConfigLoaded: (config) => { - config.forwardConsoleLogsToWS = false; - config.diagnosticTracing = false; - config.appendElementOnExit = false; - config.logExitCode = false; - config.pthreadPoolSize = 0; - } - }).create(); + const dryOk = await dry_run(runArgs); + if (!dryOk) { + mono_exit(1, "Failed during dry run"); + return; + } } // this is subsequent run with the actual tests. It will use whatever was cached in the previous run. @@ -311,7 +328,6 @@ async function run() { mono_exit(1, "Missing required --run argument"); return; } - console.log("Application arguments: " + runArgs.applicationArguments.join(' ')); configureRuntime(dotnet, runArgs, INTERNAL); diff --git a/src/tasks/WasmAppBuilder/WasmAppBuilder.cs b/src/tasks/WasmAppBuilder/WasmAppBuilder.cs index 40a74d7f57975..c74b5816a26bf 100644 --- a/src/tasks/WasmAppBuilder/WasmAppBuilder.cs +++ b/src/tasks/WasmAppBuilder/WasmAppBuilder.cs @@ -334,7 +334,7 @@ protected override bool ExecuteInternal() { throw new LogAsErrorException($"PThreadPoolSize must be -1, 0 or positive, but got {PThreadPoolSize}"); } - else + else if (PThreadPoolSize > -1) { config.Extra["pthreadPoolSize"] = PThreadPoolSize; } From 4a25447e5279f12c2d4268ab1e841384b2c031af Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Fri, 10 Mar 2023 17:24:14 +0100 Subject: [PATCH 31/45] measure --- src/mono/wasm/runtime/profiler.ts | 1 + src/mono/wasm/runtime/startup.ts | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/mono/wasm/runtime/profiler.ts b/src/mono/wasm/runtime/profiler.ts index 1948fc85a19b5..489cd4ff86856 100644 --- a/src/mono/wasm/runtime/profiler.ts +++ b/src/mono/wasm/runtime/profiler.ts @@ -41,6 +41,7 @@ export const enum MeasuredBlock { preRunWorker = "mono.preRunWorker", onRuntimeInitialized = "mono.onRuntimeInitialized", postRun = "mono.postRun", + memorySnapshot = "mono.memorySnapshot", loadRuntime = "mono.loadRuntime", bindingsInit = "mono.bindingsInit", bindJsFunction = "mono.bindJsFunction:", diff --git a/src/mono/wasm/runtime/startup.ts b/src/mono/wasm/runtime/startup.ts index 2e1851dcbfd98..f84b5c8e0e8c0 100644 --- a/src/mono/wasm/runtime/startup.ts +++ b/src/mono/wasm/runtime/startup.ts @@ -523,6 +523,7 @@ async function instantiate_wasm_module( } async function mono_wasm_before_memory_snapshot() { + const mark = startMeasure(); if (runtimeHelpers.loadedMemorySnapshot) { // get the bytes after we re-sized the memory, so that we don't have too much memory in use at the same time const memoryBytes = await getMemorySnapshot(); @@ -554,7 +555,6 @@ async function mono_wasm_before_memory_snapshot() { mono_wasm_globalization_init(); - mono_wasm_load_runtime("unused", config.debugLevel); // we didn't have snapshot yet and the feature is enabled. Take snapshot now. @@ -562,6 +562,8 @@ async function mono_wasm_before_memory_snapshot() { await storeMemorySnapshot(Module.HEAP8.buffer); runtimeHelpers.storeMemorySnapshotPending = false; } + + endMeasure(mark, MeasuredBlock.memorySnapshot); } export function mono_wasm_load_runtime(unused?: string, debugLevel?: number): void { From 17617864c1c2c99da7116de5d00b86cd9105ba86 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Sat, 11 Mar 2023 18:11:29 +0100 Subject: [PATCH 32/45] fix --- src/mono/wasm/runtime/startup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mono/wasm/runtime/startup.ts b/src/mono/wasm/runtime/startup.ts index f0afc956f66ef..a7acb514c393f 100644 --- a/src/mono/wasm/runtime/startup.ts +++ b/src/mono/wasm/runtime/startup.ts @@ -505,7 +505,7 @@ async function instantiate_wasm_module( // .grow() takes a delta compared to the previous size wasmMemory.grow((memorySize! - wasmMemory.buffer.byteLength + 65535) >>> 16); - runtimeHelpers.updateGlobalBufferAndViews(wasmMemory.buffer); + runtimeHelpers.updateMemoryViews(); } catch (err) { console.warn("MONO_WASM: failed to resize memory for the snapshot", err); runtimeHelpers.loadedMemorySnapshot = false; From 94e931a7cc55c12d65b881e6518b51cdef8807b0 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Mon, 13 Mar 2023 09:32:27 +0100 Subject: [PATCH 33/45] fix --- src/mono/wasm/runtime/dotnet.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/mono/wasm/runtime/dotnet.d.ts b/src/mono/wasm/runtime/dotnet.d.ts index 9d1ff8b63f7c2..d80c3a4fb72c5 100644 --- a/src/mono/wasm/runtime/dotnet.d.ts +++ b/src/mono/wasm/runtime/dotnet.d.ts @@ -39,6 +39,7 @@ declare interface EmscriptenModule { HEAP8: Int8Array; HEAP16: Int16Array; HEAP32: Int32Array; + HEAP64: BigInt64Array; HEAPU8: Uint8Array; HEAPU16: Uint16Array; HEAPU32: Uint32Array; From 2a7508e409fec8ae0d287c37b660b944e294bbbc Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Mon, 13 Mar 2023 11:28:37 +0100 Subject: [PATCH 34/45] optimize timezone detection --- src/mono/wasm/runtime/icu.ts | 59 +++++++++++++++----------------- src/mono/wasm/runtime/startup.ts | 23 +++++-------- src/mono/wasm/runtime/storage.ts | 3 +- src/mono/wasm/runtime/types.ts | 2 +- 4 files changed, 40 insertions(+), 47 deletions(-) diff --git a/src/mono/wasm/runtime/icu.ts b/src/mono/wasm/runtime/icu.ts index f362ec82861da..5175453f34167 100644 --- a/src/mono/wasm/runtime/icu.ts +++ b/src/mono/wasm/runtime/icu.ts @@ -5,53 +5,50 @@ import cwraps from "./cwraps"; import { Module, runtimeHelpers } from "./imports"; import { VoidPtr } from "./types/emscripten"; -let num_icu_assets_loaded_successfully = 0; - // @offset must be the address of an ICU data archive in the native heap. // returns true on success. export function mono_wasm_load_icu_data(offset: VoidPtr): boolean { - const ok = (cwraps.mono_wasm_load_icu_data(offset)) === 1; - if (ok) - num_icu_assets_loaded_successfully++; - return ok; + return (cwraps.mono_wasm_load_icu_data(offset)) === 1; } -// Performs setup for globalization. -// @globalizationMode is one of "icu", "invariant", or "auto". -// "auto" will use "icu" if any ICU data archives have been loaded, -// otherwise "invariant". -export function mono_wasm_globalization_init(): void { - const config = runtimeHelpers.config; - let invariantMode = false; - if (!config.globalizationMode) - config.globalizationMode = "auto"; - if (config.globalizationMode === "invariant") - invariantMode = true; +export function init_globalization() { + runtimeHelpers.invariantMode = runtimeHelpers.config.globalizationMode === "invariant"; + runtimeHelpers.preferredIcuAsset = get_preferred_icu_asset(); - if (!invariantMode) { - if (num_icu_assets_loaded_successfully > 0) { - if (runtimeHelpers.diagnosticTracing) { - console.debug("MONO_WASM: ICU data archive(s) loaded, disabling invariant mode"); - } - } else if (config.globalizationMode !== "icu") { - if (runtimeHelpers.diagnosticTracing) { - console.debug("MONO_WASM: ICU data archive(s) not loaded, using invariant globalization mode"); - } - invariantMode = true; + if (!runtimeHelpers.invariantMode) { + if (runtimeHelpers.preferredIcuAsset) { + if (runtimeHelpers.diagnosticTracing) console.debug("MONO_WASM: ICU data archive(s) available, disabling invariant mode"); + } else if (runtimeHelpers.config.globalizationMode !== "icu") { + if (runtimeHelpers.diagnosticTracing) console.debug("MONO_WASM: ICU data archive(s) not available, using invariant globalization mode"); + runtimeHelpers.invariantMode = true; + runtimeHelpers.preferredIcuAsset = null; } else { - const msg = "invariant globalization mode is inactive and no ICU data archives were loaded"; + const msg = "invariant globalization mode is inactive and no ICU data archives are available"; Module.err(`MONO_WASM: ERROR: ${msg}`); throw new Error(msg); } } - if (invariantMode) { - cwraps.mono_wasm_setenv("DOTNET_SYSTEM_GLOBALIZATION_INVARIANT", "1"); + const invariantEnv = "DOTNET_SYSTEM_GLOBALIZATION_INVARIANT"; + const env_variables = runtimeHelpers.config.environmentVariables!; + if (env_variables[invariantEnv] === undefined && runtimeHelpers.invariantMode) { + env_variables[invariantEnv] = "1"; + } + if (env_variables["TZ"] === undefined) { + try { + // this call is relatively expensive, so we call it during download of other assets + const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || null; + if (timezone) { + env_variables!["TZ"] = timezone; + } + } catch { + console.info("MONO_WASM: failed to detect timezone, will fallback to UTC"); + } } } export function get_preferred_icu_asset(): string | null { - if (!runtimeHelpers.config.assets) + if (!runtimeHelpers.config.assets || runtimeHelpers.invariantMode) return null; // By setting user can define what ICU source file they want to load. diff --git a/src/mono/wasm/runtime/startup.ts b/src/mono/wasm/runtime/startup.ts index a7acb514c393f..099d6310f3054 100644 --- a/src/mono/wasm/runtime/startup.ts +++ b/src/mono/wasm/runtime/startup.ts @@ -8,7 +8,6 @@ import { CharPtrNull, DotnetModule, RuntimeAPI, MonoConfig, MonoConfigInternal, import { ENVIRONMENT_IS_NODE, ENVIRONMENT_IS_SHELL, INTERNAL, Module, runtimeHelpers } from "./imports"; import cwraps, { init_c_exports } from "./cwraps"; import { mono_wasm_raise_debug_event, mono_wasm_runtime_ready } from "./debug"; -import { get_preferred_icu_asset, mono_wasm_globalization_init } from "./icu"; import { toBase64StringImpl } from "./base64"; import { mono_wasm_init_aot_profiler, mono_wasm_init_browser_profiler } from "./profiler"; import { mono_on_abort, mono_exit } from "./run"; @@ -33,6 +32,7 @@ import { getMemorySnapshot, storeMemorySnapshot, getMemorySnapshotSize } from ". import { init_legacy_exports } from "./net6-legacy/corebindings"; import { cwraps_binding_api, cwraps_mono_api } from "./net6-legacy/exports-legacy"; import { BINDING, MONO } from "./net6-legacy/imports"; +import { init_globalization } from "./icu"; let config: MonoConfigInternal = undefined as any; let configLoaded = false; @@ -108,6 +108,7 @@ function instantiateWasm( const mark = startMeasure(); if (userInstantiateWasm) { + init_globalization(); // user wasm instantiation doesn't support memory snapshots memorySnapshotSkippedOrDone.promise_control.resolve(); const exports = userInstantiateWasm(imports, (instance: WebAssembly.Instance, module: WebAssembly.Module | undefined) => { @@ -158,9 +159,10 @@ function preInit(userPreInit: (() => void)[]) { // It will block emscripten `userOnRuntimeInitialized` by pending addRunDependency("mono_pre_init") (async () => { try { + // - init the rest of the polyfills + // - download Module.config from configSrc await mono_wasm_pre_init_essential_async(); - // - download Module.config from configSrc // - start download assets like DLLs await mono_wasm_pre_init_full(); @@ -476,7 +478,10 @@ async function instantiate_wasm_module( if (runtimeHelpers.diagnosticTracing) console.debug("MONO_WASM: instantiate_wasm_module"); const assetToLoad = resolve_asset_path("dotnetwasm"); // FIXME: this would not apply re-try (on connection reset during download) for dotnet.wasm because we could not download the buffer before we pass it to instantiate_wasm_asset - await start_asset_download(assetToLoad); + const wasmDownloadPromise = start_asset_download(assetToLoad); + + // this is right time as we have free CPU time to do this + init_globalization(); if (config.startupMemoryCache && config.assetsHash) { memorySize = await getMemorySnapshotSize(); @@ -488,6 +493,7 @@ async function instantiate_wasm_module( memorySnapshotSkippedOrDone.promise_control.resolve(); } + await wasmDownloadPromise; await beforePreInit.promise; Module.addRunDependency("instantiate_wasm_module"); @@ -553,8 +559,6 @@ async function mono_wasm_before_memory_snapshot() { if (config.browserProfilerOptions) mono_wasm_init_browser_profiler(config.browserProfilerOptions); - mono_wasm_globalization_init(); - mono_wasm_load_runtime("unused", config.debugLevel); // we didn't have snapshot yet and the feature is enabled. Take snapshot now. @@ -686,16 +690,7 @@ function normalizeConfig() { runtimeHelpers.enablePerfMeasure = !!config.browserProfilerOptions && globalThis.performance && typeof globalThis.performance.measure === "function"; - runtimeHelpers.preferredIcuAsset = get_preferred_icu_asset(); - if (runtimeHelpers.timezone === undefined && config.environmentVariables["TZ"] === undefined) { - try { - runtimeHelpers.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || null; - if (runtimeHelpers.timezone) config.environmentVariables["TZ"] = runtimeHelpers.timezone; - } catch { - console.info("MONO_WASM: failed to detect timezone, will fallback to UTC"); - } - } runtimeHelpers.waitForDebugger = config.waitForDebugger; } diff --git a/src/mono/wasm/runtime/storage.ts b/src/mono/wasm/runtime/storage.ts index b3c9d1c8e4029..a81e6843acaf0 100644 --- a/src/mono/wasm/runtime/storage.ts +++ b/src/mono/wasm/runtime/storage.ts @@ -147,7 +147,8 @@ export async function getInputsHash(): Promise { delete inputs.assets; // some things are calculated at runtime, so we need to add them to the hash inputs.preferredIcuAsset = runtimeHelpers.preferredIcuAsset; - inputs.timezone = runtimeHelpers.timezone; + // timezone is part of env variables, so it is already in the hash + // some things are not relevant for memory snapshot delete inputs.forwardConsoleLogsToWS; delete inputs.diagnosticTracing; diff --git a/src/mono/wasm/runtime/types.ts b/src/mono/wasm/runtime/types.ts index 53fd6db1671fb..d6e8443d27976 100644 --- a/src/mono/wasm/runtime/types.ts +++ b/src/mono/wasm/runtime/types.ts @@ -237,7 +237,7 @@ export type RuntimeHelpers = { storeMemorySnapshotPending: boolean, subtle: SubtleCrypto | null, preferredIcuAsset: string | null, - timezone: string | null, + invariantMode: boolean, updateMemoryViews: () => void } From 9759ad849c2a7a531784e9bb44afea819e802546 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Mon, 13 Mar 2023 12:23:59 +0100 Subject: [PATCH 35/45] feedback --- src/mono/wasm/runtime/assets.ts | 16 ++++++++-------- src/mono/wasm/runtime/run.ts | 2 +- src/mono/wasm/runtime/startup.ts | 11 ++++++++--- src/mono/wasm/runtime/storage.ts | 23 ++++++++++++----------- src/mono/wasm/test-main.js | 2 ++ 5 files changed, 31 insertions(+), 23 deletions(-) diff --git a/src/mono/wasm/runtime/assets.ts b/src/mono/wasm/runtime/assets.ts index 5ab9322ae695e..bbdadd2430e24 100644 --- a/src/mono/wasm/runtime/assets.ts +++ b/src/mono/wasm/runtime/assets.ts @@ -38,7 +38,7 @@ const skipBufferByAssetTypes: { "dotnetwasm": true, }; -const skipInSnapshotByAssetTypes: { +const containedInSnapshotByAssetTypes: { [k: string]: boolean } = { "resource": true, @@ -76,8 +76,8 @@ export async function mono_download_assets(): Promise { runtimeHelpers.maxParallelDownloads = runtimeHelpers.config.maxParallelDownloads || runtimeHelpers.maxParallelDownloads; runtimeHelpers.enableDownloadRetry = runtimeHelpers.config.enableDownloadRetry || runtimeHelpers.enableDownloadRetry; try { - const beforeSnapshotAssets: AssetEntryInternal[] = []; - const afterSnapshotAssets: AssetEntryInternal[] = []; + const alwaysLoadedAssets: AssetEntryInternal[] = []; + const containedInSnapshotAssets: AssetEntryInternal[] = []; const promises_of_assets: Promise[] = []; for (const a of runtimeHelpers.config.assets!) { @@ -88,10 +88,10 @@ export async function mono_download_assets(): Promise { mono_assert(!asset.resolvedUrl || typeof asset.resolvedUrl === "string", "asset resolvedUrl could be string"); mono_assert(!asset.hash || typeof asset.hash === "string", "asset resolvedUrl could be string"); mono_assert(!asset.pendingDownload || typeof asset.pendingDownload === "object", "asset pendingDownload could be object"); - if (skipInSnapshotByAssetTypes[asset.behavior]) { - afterSnapshotAssets.push(asset); + if (containedInSnapshotByAssetTypes[asset.behavior]) { + containedInSnapshotAssets.push(asset); } else { - beforeSnapshotAssets.push(asset); + alwaysLoadedAssets.push(asset); } } @@ -106,7 +106,7 @@ export async function mono_download_assets(): Promise { }; // start fetching and assets in parallel, only assets which are not part of memory snapshot - for (const asset of beforeSnapshotAssets) { + for (const asset of alwaysLoadedAssets) { countAndStartDownload(asset); } @@ -115,7 +115,7 @@ export async function mono_download_assets(): Promise { // start fetching and assets in parallel, only if memory snapshot is not available if (!runtimeHelpers.loadedMemorySnapshot) { - for (const asset of afterSnapshotAssets) { + for (const asset of containedInSnapshotAssets) { countAndStartDownload(asset); } } diff --git a/src/mono/wasm/runtime/run.ts b/src/mono/wasm/runtime/run.ts index a8ea1e8d4c926..27d1a65fd6fb6 100644 --- a/src/mono/wasm/runtime/run.ts +++ b/src/mono/wasm/runtime/run.ts @@ -120,7 +120,7 @@ function set_exit_code_and_quit_now(exit_code: number, reason?: any): void { } else if (!reason) { reason = new runtimeHelpers.ExitStatus(exit_code); - } else { + } else if (typeof reason.status === "number") { exit_code = reason.status; } } diff --git a/src/mono/wasm/runtime/startup.ts b/src/mono/wasm/runtime/startup.ts index 099d6310f3054..b9df8662a5d76 100644 --- a/src/mono/wasm/runtime/startup.ts +++ b/src/mono/wasm/runtime/startup.ts @@ -329,7 +329,7 @@ export function abort_startup(reason: any, should_exit: boolean): void { beforeOnRuntimeInitialized.promise_control.reject(reason); afterOnRuntimeInitialized.promise_control.reject(reason); afterPostRun.promise_control.reject(reason); - if (should_exit && reason.silent !== true) { + if (should_exit && (typeof reason !== "object" || reason.silent !== true)) { mono_exit(1, reason); } } @@ -678,7 +678,7 @@ function normalizeConfig() { config.assets = config.assets || []; config.runtimeOptions = config.runtimeOptions || []; config.globalizationMode = config.globalizationMode || "auto"; - config.startupMemoryCache = config.startupMemoryCache == undefined ? true : !!config.startupMemoryCache; + if (config.debugLevel === undefined && BuildConfiguration === "Debug") { config.debugLevel = -1; } @@ -686,12 +686,17 @@ function normalizeConfig() { config.diagnosticTracing = true; } runtimeHelpers.diagnosticTracing = !!config.diagnosticTracing; + runtimeHelpers.waitForDebugger = config.waitForDebugger; + config.startupMemoryCache = config.startupMemoryCache == undefined ? true : !!config.startupMemoryCache; + if (config.startupMemoryCache && runtimeHelpers.waitForDebugger) { + if (runtimeHelpers.diagnosticTracing) console.info("MONO_WASM: Disabling startupMemoryCache because waitForDebugger is set"); + config.startupMemoryCache = false; + } runtimeHelpers.enablePerfMeasure = !!config.browserProfilerOptions && globalThis.performance && typeof globalThis.performance.measure === "function"; - runtimeHelpers.waitForDebugger = config.waitForDebugger; } export function mono_wasm_asm_loaded(assembly_name: CharPtr, assembly_ptr: number, assembly_len: number, pdb_ptr: number, pdb_len: number): void { diff --git a/src/mono/wasm/runtime/storage.ts b/src/mono/wasm/runtime/storage.ts index a81e6843acaf0..1daf50008008e 100644 --- a/src/mono/wasm/runtime/storage.ts +++ b/src/mono/wasm/runtime/storage.ts @@ -46,11 +46,10 @@ async function openCache(): Promise { export async function getMemorySnapshotSize(): Promise { try { - const inputsHash = await getInputsHash(); - if (!inputsHash) { + const cacheKey = await getCacheKey(); + if (!cacheKey) { return undefined; } - const cacheKey = `${memoryPrefix}-${ProductVersion}-${GitHash}-${inputsHash}`; const cache = await openCache(); if (!cache) { return undefined; @@ -66,11 +65,10 @@ export async function getMemorySnapshotSize(): Promise { export async function getMemorySnapshot(): Promise { try { - const inputsHash = await getInputsHash(); - if (!inputsHash) { + const cacheKey = await getCacheKey(); + if (!cacheKey) { return undefined; } - const cacheKey = `${memoryPrefix}-${ProductVersion}-${GitHash}-${inputsHash}`; const cache = await openCache(); if (!cache) { return undefined; @@ -88,11 +86,10 @@ export async function getMemorySnapshot(): Promise { export async function storeMemorySnapshot(memory: ArrayBuffer) { try { - const inputsHash = await getInputsHash(); - if (!inputsHash) { + const cacheKey = await getCacheKey(); + if (!cacheKey) { return; } - const cacheKey = `${memoryPrefix}-${ProductVersion}-${GitHash}-${inputsHash}`; const cache = await openCache(); if (!cache) { return; @@ -136,7 +133,7 @@ export async function cleanupMemorySnapshots(protectKey: string) { } // calculate hash of things which affect the memory snapshot -export async function getInputsHash(): Promise { +async function getCacheKey(): Promise { if (!runtimeHelpers.subtle) { return null; } @@ -163,9 +160,13 @@ export async function getInputsHash(): Promise { delete inputs.enableDownloadRetry; delete inputs.exitAfterSnapshot; + inputs.GitHash = GitHash; + inputs.ProductVersion = ProductVersion; + const inputsJson = JSON.stringify(inputs); const sha256Buffer = await runtimeHelpers.subtle.digest("SHA-256", new TextEncoder().encode(inputsJson)); const uint8ViewOfHash = new Uint8Array(sha256Buffer); const hashAsString = Array.from(uint8ViewOfHash).map((b) => b.toString(16).padStart(2, "0")).join(""); - return hashAsString; + + return `${memoryPrefix}-${hashAsString}`; } diff --git a/src/mono/wasm/test-main.js b/src/mono/wasm/test-main.js index b58212a563a42..73437d7a5351d 100644 --- a/src/mono/wasm/test-main.js +++ b/src/mono/wasm/test-main.js @@ -295,6 +295,8 @@ async function dry_run(runArgs) { appendElementOnExit: false, logExitCode: false, pthreadPoolSize: 0, + // this just means to not continue startup after the snapshot is taken. + // If there was previously a matching snapshot, it will be used. exitAfterSnapshot: true }).create(); } catch (err) { From 1f4e4c61368e6cec9ea2fc7ed08eac7e2d1e3983 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Mon, 13 Mar 2023 13:40:00 +0100 Subject: [PATCH 36/45] fix --- src/mono/wasm/runtime/startup.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mono/wasm/runtime/startup.ts b/src/mono/wasm/runtime/startup.ts index b9df8662a5d76..ad57732dc4eec 100644 --- a/src/mono/wasm/runtime/startup.ts +++ b/src/mono/wasm/runtime/startup.ts @@ -419,11 +419,11 @@ async function mono_wasm_after_user_runtime_initialized(): Promise { function _print_error(message: string, err: any): void { - if (err.silent) { + if (typeof err === "object" && err.silent) { return; } Module.err(`${message}: ${JSON.stringify(err)}`); - if (err.stack) { + if (typeof err === "object" && err.stack) { Module.err("MONO_WASM: Stacktrace: \n"); Module.err(err.stack); } From e2a2cd4d2b51b0f8707a2284c206f7aedd38e693 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Mon, 13 Mar 2023 15:28:08 +0100 Subject: [PATCH 37/45] - enable snapshot in blazor - @kg feedback about mono_jiterp_do_jit_call_indirect --- src/mono/mono/mini/interp/jiterpreter.c | 3 +++ src/mono/wasm/runtime/jiterpreter-jit-call.ts | 6 +++--- .../wasm/runtime/{storage.ts => snapshot.ts} | 17 +++++++++++++++-- src/mono/wasm/runtime/startup.ts | 12 ++++++++---- 4 files changed, 29 insertions(+), 9 deletions(-) rename src/mono/wasm/runtime/{storage.ts => snapshot.ts} (92%) diff --git a/src/mono/mono/mini/interp/jiterpreter.c b/src/mono/mono/mini/interp/jiterpreter.c index cff40a400384b..2f867757c96a9 100644 --- a/src/mono/mono/mini/interp/jiterpreter.c +++ b/src/mono/mono/mini/interp/jiterpreter.c @@ -1075,6 +1075,9 @@ mono_jiterp_update_jit_call_dispatcher (WasmDoJitCall dispatcher) // blocked the use of Module.addFunction if (!dispatcher) dispatcher = (WasmDoJitCall)mono_llvm_cpp_catch_exception; + else if (((int)(void*)dispatcher)==-1) + dispatcher = mono_jiterp_do_jit_call_indirect; + jiterpreter_do_jit_call = dispatcher; } diff --git a/src/mono/wasm/runtime/jiterpreter-jit-call.ts b/src/mono/wasm/runtime/jiterpreter-jit-call.ts index 4e7d0e278c133..e84fc620da4ff 100644 --- a/src/mono/wasm/runtime/jiterpreter-jit-call.ts +++ b/src/mono/wasm/runtime/jiterpreter-jit-call.ts @@ -256,6 +256,7 @@ function getIsWasmEhSupported () : boolean { export function mono_jiterp_do_jit_call_indirect ( jit_call_cb: number, cb_data: VoidPtr, thrown: Int32Ptr ) : void { + mono_assert(!runtimeHelpers.storeMemorySnapshotPending, "Attempting to set function into table during creation of memory snapshot"); const table = getWasmFunctionTable(); const jitCallCb = table.get(jit_call_cb); @@ -269,9 +270,8 @@ export function mono_jiterp_do_jit_call_indirect ( } }; - let failed = false; - const enabled = !runtimeHelpers.storeMemorySnapshotPending && getIsWasmEhSupported(); - if (enabled) { + let failed = !getIsWasmEhSupported(); + if (!failed) { // Wasm EH is supported which means doJitCallModule was loaded and compiled. // Now that we have jit_call_cb, we can instantiate it. try { diff --git a/src/mono/wasm/runtime/storage.ts b/src/mono/wasm/runtime/snapshot.ts similarity index 92% rename from src/mono/wasm/runtime/storage.ts rename to src/mono/wasm/runtime/snapshot.ts index 1daf50008008e..7321291564e4b 100644 --- a/src/mono/wasm/runtime/storage.ts +++ b/src/mono/wasm/runtime/snapshot.ts @@ -139,8 +139,21 @@ async function getCacheKey(): Promise { } const inputs = Object.assign({}, runtimeHelpers.config) as any; // above already has env variables, runtime options, etc - // above also already has config.assetsHash for this. It has all the asserts (DLLs, ICU, .wasms, etc). - // So we could remove assets collectionfrom the hash. + + if (!inputs.assetsHash) { + // this is fallback for blazor which does not have assetsHash yet + inputs.assetsHash = []; + for (const asset of inputs.assets) { + if (!asset.hash) { + // if we don't have hash, we can't use the cache + return null; + } + inputs.assetsHash.push(asset.hash); + } + } + // otherwise config.assetsHash already has hashes for all the assets (DLLs, ICU, .wasms, etc). + + // Now we remove assets collection from the hash. delete inputs.assets; // some things are calculated at runtime, so we need to add them to the hash inputs.preferredIcuAsset = runtimeHelpers.preferredIcuAsset; diff --git a/src/mono/wasm/runtime/startup.ts b/src/mono/wasm/runtime/startup.ts index ad57732dc4eec..970a8408f3eea 100644 --- a/src/mono/wasm/runtime/startup.ts +++ b/src/mono/wasm/runtime/startup.ts @@ -26,7 +26,7 @@ import { mono_wasm_init_diagnostics } from "./diagnostics"; import { preAllocatePThreadWorkerPool, instantiateWasmPThreadWorkerPool } from "./pthreads/browser"; import { export_linker } from "./exports-linker"; import { endMeasure, MeasuredBlock, startMeasure } from "./profiler"; -import { getMemorySnapshot, storeMemorySnapshot, getMemorySnapshotSize } from "./storage"; +import { getMemorySnapshot, storeMemorySnapshot, getMemorySnapshotSize } from "./snapshot"; // legacy import { init_legacy_exports } from "./net6-legacy/corebindings"; @@ -483,7 +483,7 @@ async function instantiate_wasm_module( // this is right time as we have free CPU time to do this init_globalization(); - if (config.startupMemoryCache && config.assetsHash) { + if (config.startupMemoryCache) { memorySize = await getMemorySnapshotSize(); runtimeHelpers.loadedMemorySnapshot = !!memorySize; runtimeHelpers.storeMemorySnapshotPending = !runtimeHelpers.loadedMemorySnapshot; @@ -537,7 +537,6 @@ async function mono_wasm_before_memory_snapshot() { Module.HEAP8.set(new Int8Array(memoryBytes!), 0); if (runtimeHelpers.diagnosticTracing) console.info("MONO_WASM: Loaded WASM linear memory from browser cache"); - // all things below are loaded from the snapshot return; } @@ -549,7 +548,10 @@ async function mono_wasm_before_memory_snapshot() { else throw new Error(`Expected environment variable '${k}' to be a string but it was ${typeof v}: '${v}'`); } - + if (config.startupMemoryCache) { + // disable the trampoline for now, we will re-enable it after we stored the snapshot + cwraps.mono_jiterp_update_jit_call_dispatcher(0); + } if (config.runtimeOptions) mono_wasm_set_runtime_options(config.runtimeOptions); @@ -563,6 +565,8 @@ async function mono_wasm_before_memory_snapshot() { // we didn't have snapshot yet and the feature is enabled. Take snapshot now. if (config.startupMemoryCache) { + // this would install the mono_jiterp_do_jit_call_indirect + cwraps.mono_jiterp_update_jit_call_dispatcher(-1); await storeMemorySnapshot(Module.HEAP8.buffer); runtimeHelpers.storeMemorySnapshotPending = false; } From eaaeaa59e0f3b1ba6df871a6a4ed16c2754c6e03 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Mon, 13 Mar 2023 15:30:26 +0100 Subject: [PATCH 38/45] store cache key --- src/mono/wasm/runtime/snapshot.ts | 7 +++++-- src/mono/wasm/runtime/types.ts | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/mono/wasm/runtime/snapshot.ts b/src/mono/wasm/runtime/snapshot.ts index 7321291564e4b..eb96dec46c561 100644 --- a/src/mono/wasm/runtime/snapshot.ts +++ b/src/mono/wasm/runtime/snapshot.ts @@ -134,6 +134,9 @@ export async function cleanupMemorySnapshots(protectKey: string) { // calculate hash of things which affect the memory snapshot async function getCacheKey(): Promise { + if (runtimeHelpers.memorySnapshotCacheKey) { + return runtimeHelpers.memorySnapshotCacheKey; + } if (!runtimeHelpers.subtle) { return null; } @@ -180,6 +183,6 @@ async function getCacheKey(): Promise { const sha256Buffer = await runtimeHelpers.subtle.digest("SHA-256", new TextEncoder().encode(inputsJson)); const uint8ViewOfHash = new Uint8Array(sha256Buffer); const hashAsString = Array.from(uint8ViewOfHash).map((b) => b.toString(16).padStart(2, "0")).join(""); - - return `${memoryPrefix}-${hashAsString}`; + runtimeHelpers.memorySnapshotCacheKey = `${memoryPrefix}-${hashAsString}`; + return runtimeHelpers.memorySnapshotCacheKey; } diff --git a/src/mono/wasm/runtime/types.ts b/src/mono/wasm/runtime/types.ts index d6e8443d27976..cd8b63dbe9ff0 100644 --- a/src/mono/wasm/runtime/types.ts +++ b/src/mono/wasm/runtime/types.ts @@ -235,6 +235,7 @@ export type RuntimeHelpers = { loadedFiles: string[], loadedMemorySnapshot: boolean, storeMemorySnapshotPending: boolean, + memorySnapshotCacheKey: string, subtle: SubtleCrypto | null, preferredIcuAsset: string | null, invariantMode: boolean, From bc86ffe23c88905477c8ab05b98931e6b5291740 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Mon, 13 Mar 2023 15:38:36 +0100 Subject: [PATCH 39/45] feedback and cleanup --- src/mono/sample/wasm/browser-advanced/index.html | 3 +++ .../sample/wasm/browser-bench/appstart-frame.html | 3 +++ src/mono/wasm/runtime/assets.ts | 11 ++++++++--- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/mono/sample/wasm/browser-advanced/index.html b/src/mono/sample/wasm/browser-advanced/index.html index 2b9801168077f..0532ef8fcef32 100644 --- a/src/mono/sample/wasm/browser-advanced/index.html +++ b/src/mono/sample/wasm/browser-advanced/index.html @@ -11,6 +11,9 @@ + + + diff --git a/src/mono/sample/wasm/browser-bench/appstart-frame.html b/src/mono/sample/wasm/browser-bench/appstart-frame.html index 1aec2376ab83a..c218f7f0b3a05 100644 --- a/src/mono/sample/wasm/browser-bench/appstart-frame.html +++ b/src/mono/sample/wasm/browser-bench/appstart-frame.html @@ -11,6 +11,9 @@ + + + diff --git a/src/mono/wasm/runtime/assets.ts b/src/mono/wasm/runtime/assets.ts index bbdadd2430e24..5adc53034e8f5 100644 --- a/src/mono/wasm/runtime/assets.ts +++ b/src/mono/wasm/runtime/assets.ts @@ -113,10 +113,15 @@ export async function mono_download_assets(): Promise { // continue after we know if memory snapshot is available or not await memorySnapshotSkippedOrDone.promise; - // start fetching and assets in parallel, only if memory snapshot is not available - if (!runtimeHelpers.loadedMemorySnapshot) { - for (const asset of containedInSnapshotAssets) { + // start fetching and assets in parallel, only if memory snapshot is not available. + for (const asset of containedInSnapshotAssets) { + if (!runtimeHelpers.loadedMemorySnapshot) { countAndStartDownload(asset); + } else { + // Otherwise cleanup in case we were given pending download. It would be even better if we could abort the download. + asset.pendingDownloadInternal = null as any; // GC + asset.pendingDownload = null as any; // GC + asset.buffer = null as any; // GC } } From 59a7a568337ae539a262360ed18f4566e03f7f6d Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Mon, 13 Mar 2023 16:21:56 +0100 Subject: [PATCH 40/45] fix debugging --- src/mono/wasm/runtime/assets.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/mono/wasm/runtime/assets.ts b/src/mono/wasm/runtime/assets.ts index 5adc53034e8f5..827f28e7733ae 100644 --- a/src/mono/wasm/runtime/assets.ts +++ b/src/mono/wasm/runtime/assets.ts @@ -122,6 +122,13 @@ export async function mono_download_assets(): Promise { asset.pendingDownloadInternal = null as any; // GC asset.pendingDownload = null as any; // GC asset.buffer = null as any; // GC + if (asset.behavior == "resource" || asset.behavior == "assembly" || asset.behavior == "pdb") { + const url = resolve_path(asset, ""); + const virtualName: string = typeof (asset.virtualPath) === "string" + ? asset.virtualPath + : asset.name; + loaded_files.push({ url: url, file: virtualName }); + } } } From 6d9f7eebbb39ed1be509f2b5356bc37ad4041ce1 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Tue, 14 Mar 2023 01:52:42 +0100 Subject: [PATCH 41/45] fix --- src/mono/sample/wasm/browser-bench/appstart-frame.html | 2 +- src/mono/wasm/runtime/assets.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/mono/sample/wasm/browser-bench/appstart-frame.html b/src/mono/sample/wasm/browser-bench/appstart-frame.html index c218f7f0b3a05..b54590f423895 100644 --- a/src/mono/sample/wasm/browser-bench/appstart-frame.html +++ b/src/mono/sample/wasm/browser-bench/appstart-frame.html @@ -13,7 +13,7 @@ - + diff --git a/src/mono/wasm/runtime/assets.ts b/src/mono/wasm/runtime/assets.ts index 827f28e7733ae..7b28c1e7f011b 100644 --- a/src/mono/wasm/runtime/assets.ts +++ b/src/mono/wasm/runtime/assets.ts @@ -122,6 +122,7 @@ export async function mono_download_assets(): Promise { asset.pendingDownloadInternal = null as any; // GC asset.pendingDownload = null as any; // GC asset.buffer = null as any; // GC + // tell the debugger it is loaded if (asset.behavior == "resource" || asset.behavior == "assembly" || asset.behavior == "pdb") { const url = resolve_path(asset, ""); const virtualName: string = typeof (asset.virtualPath) === "string" From f617787363666c4fe651b692aed159bba976fa7a Mon Sep 17 00:00:00 2001 From: Pavel Savara Date: Tue, 14 Mar 2023 09:24:06 +0100 Subject: [PATCH 42/45] Update src/mono/wasm/memory-snapshot.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Marek Fišera --- src/mono/wasm/memory-snapshot.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mono/wasm/memory-snapshot.md b/src/mono/wasm/memory-snapshot.md index 51dd896b23691..d61b580781248 100644 --- a/src/mono/wasm/memory-snapshot.md +++ b/src/mono/wasm/memory-snapshot.md @@ -1,4 +1,4 @@ -# Memory snapshot of the Mono runtime # +# Memory snapshot of the Mono runtime We take snapshot of WASM memory after first cold start at run time​. We store it on the client side in the browser cache. From cfc5acf1dbbc816b52789677207061589d4ea700 Mon Sep 17 00:00:00 2001 From: Pavel Savara Date: Tue, 14 Mar 2023 09:24:16 +0100 Subject: [PATCH 43/45] Update src/mono/wasm/memory-snapshot.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Marek Fišera --- src/mono/wasm/memory-snapshot.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mono/wasm/memory-snapshot.md b/src/mono/wasm/memory-snapshot.md index d61b580781248..d9d392555cb10 100644 --- a/src/mono/wasm/memory-snapshot.md +++ b/src/mono/wasm/memory-snapshot.md @@ -2,7 +2,7 @@ We take snapshot of WASM memory after first cold start at run time​. We store it on the client side in the browser cache. -For subsequent runs with the same configuration and same assets, we use the snapshot . +For subsequent runs with the same configuration and same assets, we use the snapshot instead of downloading everything again and doing the runtime startup again. These subsequent starts are significantly faster. From c78ef575d5914b26d06a16062a53f19c810ccf61 Mon Sep 17 00:00:00 2001 From: Pavel Savara Date: Tue, 14 Mar 2023 09:24:27 +0100 Subject: [PATCH 44/45] Update src/mono/wasm/runtime/assets.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Marek Fišera --- src/mono/wasm/runtime/assets.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mono/wasm/runtime/assets.ts b/src/mono/wasm/runtime/assets.ts index 7b28c1e7f011b..4b60e0521bcb3 100644 --- a/src/mono/wasm/runtime/assets.ts +++ b/src/mono/wasm/runtime/assets.ts @@ -105,7 +105,7 @@ export async function mono_download_assets(): Promise { } }; - // start fetching and assets in parallel, only assets which are not part of memory snapshot + // start fetching assets in parallel, only assets which are not part of memory snapshot for (const asset of alwaysLoadedAssets) { countAndStartDownload(asset); } From 7309e43835b7a639799251cb3f57f0b89fdaaefa Mon Sep 17 00:00:00 2001 From: Pavel Savara Date: Tue, 14 Mar 2023 09:24:38 +0100 Subject: [PATCH 45/45] Update src/mono/wasm/runtime/assets.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Marek Fišera --- src/mono/wasm/runtime/assets.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mono/wasm/runtime/assets.ts b/src/mono/wasm/runtime/assets.ts index 4b60e0521bcb3..2852e1f4d463b 100644 --- a/src/mono/wasm/runtime/assets.ts +++ b/src/mono/wasm/runtime/assets.ts @@ -113,7 +113,7 @@ export async function mono_download_assets(): Promise { // continue after we know if memory snapshot is available or not await memorySnapshotSkippedOrDone.promise; - // start fetching and assets in parallel, only if memory snapshot is not available. + // start fetching assets in parallel, only if memory snapshot is not available. for (const asset of containedInSnapshotAssets) { if (!runtimeHelpers.loadedMemorySnapshot) { countAndStartDownload(asset);