From 5d3e70a47d137c159f161a2b9fd44866cebfb467 Mon Sep 17 00:00:00 2001 From: Pavel Savara Date: Mon, 15 Nov 2021 23:22:48 +0100 Subject: [PATCH] [wasm] parallel asset loading (#61610) --- src/mono/sample/wasm/Directory.Build.targets | 2 +- src/mono/wasm/runtime/startup.ts | 201 +++++++------------ src/mono/wasm/runtime/types.ts | 4 +- 3 files changed, 82 insertions(+), 125 deletions(-) diff --git a/src/mono/sample/wasm/Directory.Build.targets b/src/mono/sample/wasm/Directory.Build.targets index 3ddadf53edc593..f0b90a539a9c56 100644 --- a/src/mono/sample/wasm/Directory.Build.targets +++ b/src/mono/sample/wasm/Directory.Build.targets @@ -20,6 +20,6 @@ - + diff --git a/src/mono/wasm/runtime/startup.ts b/src/mono/wasm/runtime/startup.ts index f398adee92d963..df61a27399d861 100644 --- a/src/mono/wasm/runtime/startup.ts +++ b/src/mono/wasm/runtime/startup.ts @@ -2,7 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. import { INTERNAL, Module, MONO, runtimeHelpers } from "./modules"; -import { AssetEntry, CharPtr, CharPtrNull, EmscriptenModuleMono, GlobalizationMode, MonoConfig, TypedArray, VoidPtr, wasm_type_symbol } from "./types"; +import { AllAssetEntryTypes, AssetEntry, CharPtr, CharPtrNull, EmscriptenModuleMono, GlobalizationMode, MonoConfig, TypedArray, VoidPtr, wasm_type_symbol } from "./types"; import cwraps from "./cwraps"; import { mono_wasm_raise_debug_event, mono_wasm_runtime_ready } from "./debug"; import { mono_wasm_globalization_init, mono_wasm_load_icu_data } from "./icu"; @@ -56,7 +56,7 @@ export function mono_wasm_set_runtime_options(options: string[]): void { cwraps.mono_wasm_parse_runtime_options(options.length, argv); } -function _handle_loaded_asset(ctx: MonoInitContext, asset: AssetEntry, url: string, blob: ArrayBuffer) { +function _handle_fetched_asset(ctx: MonoInitContext, asset: AssetEntry, url: string, blob: ArrayBuffer) { const bytes = new Uint8Array(blob); if (ctx.tracing) console.log(`MONO_WASM: Loaded:${asset.name} as ${asset.behavior} size ${bytes.length} from ${url}`); @@ -129,16 +129,6 @@ function _handle_loaded_asset(ctx: MonoInitContext, asset: AssetEntry, url: stri } } -// Initializes the runtime and loads assemblies, debug information, and other files. -export function mono_load_runtime_and_bcl_args(args: MonoConfig): void { - try { - return _load_assets_and_runtime(args); - } catch (exc: any) { - console.error("MONO_WASM: Error in mono_load_runtime_and_bcl_args:", exc); - throw exc; - } -} - function _apply_configuration_from_args(args: MonoConfig) { for (const k in (args.environment_variables || {})) mono_wasm_setenv(k, args.environment_variables![k]); @@ -153,7 +143,7 @@ function _apply_configuration_from_args(args: MonoConfig) { mono_wasm_init_coverage_profiler(args.coverage_profiler_options); } -function _get_fetch_file_cb_from_args(args: MonoConfig): (asset: string) => Promise { +function _get_fetch_implementation(args: MonoConfig): (asset: string) => Promise { if (typeof (args.fetch_file_cb) === "function") return args.fetch_file_cb; @@ -307,116 +297,50 @@ export function bindings_lazy_init(): void { _create_primitive_converters(); } -function _load_assets_and_runtime(args: MonoConfig) { - if (args.enable_debugging) - args.debug_level = args.enable_debugging; - - const ctx: MonoInitContext = { - tracing: args.diagnostic_tracing || false, - pending_count: args.assets.length, - loaded_assets: Object.create(null), - // dlls and pdbs, used by blazor and the debugger - loaded_files: [], - createPath: Module.FS_createPath, - createDataFile: Module.FS_createDataFile - }; - - if (ctx.tracing) - console.log("MONO_WASM: mono_wasm_load_runtime_with_args", JSON.stringify(args)); - - _apply_configuration_from_args(args); - const fetch_file_cb = _get_fetch_file_cb_from_args(args); - - const onPendingRequestComplete = function () { - --ctx.pending_count; - - if (ctx.pending_count === 0) { - try { - _finalize_startup(args, ctx); - } catch (exc: any) { - console.error("MONO_WASM: Unhandled exception in _finalize_startup", exc); - console.error(exc.stack); - throw exc; - } - } - }; +// Initializes the runtime and loads assemblies, debug information, and other files. +export async function mono_load_runtime_and_bcl_args(args: MonoConfig): Promise { + try { + if (args.enable_debugging) + args.debug_level = args.enable_debugging; + + const ctx: MonoInitContext = { + tracing: args.diagnostic_tracing || false, + pending_count: args.assets.length, + loaded_assets: Object.create(null), + // dlls and pdbs, used by blazor and the debugger + loaded_files: [], + createPath: Module.FS_createPath, + createDataFile: Module.FS_createDataFile + }; - const processFetchResponseBuffer = function (asset: AssetEntry, url: string, buffer: ArrayBuffer) { - try { - _handle_loaded_asset(ctx, asset, url, buffer); - } catch (exc) { - console.error(`MONO_WASM: Unhandled exception in processFetchResponseBuffer ${url} ${exc}`); - throw exc; - } finally { - onPendingRequestComplete(); - } - }; + _apply_configuration_from_args(args); - args.assets.forEach(function (asset: AssetEntry) { - let sourceIndex = 0; - const sourcesList = asset.load_remote ? args.remote_sources! : [""]; + const local_fetch = _get_fetch_implementation(args); - const handleFetchResponse = function (response: Response) { - if (!response.ok) { - try { - attemptNextSource(); - return; - } catch (exc) { - console.error(`MONO_WASM: Unhandled exception in handleFetchResponse attemptNextSource for asset ${asset.name} ${exc}`); - throw exc; - } - } + const load_asset = async (asset: AllAssetEntryTypes): Promise => { + //TODO we could do module.addRunDependency(asset.name) and delay emscripten run() after all assets are loaded - try { - const bufferPromise = response.arrayBuffer(); - bufferPromise.then((data) => processFetchResponseBuffer(asset, response.url, data)); - } catch (exc) { - console.error(`MONO_WASM: Unhandled exception in handleFetchResponse for asset ${asset.name} ${exc}`); - attemptNextSource(); - } - }; + const sourcesList = asset.load_remote ? args.remote_sources! : [""]; + let error = undefined; + for (let sourcePrefix of sourcesList) { + // HACK: Special-case because MSBuild doesn't allow "" as an attribute + if (sourcePrefix === "./") + sourcePrefix = ""; - const attemptNextSource = function () { - if (sourceIndex >= sourcesList.length) { - const msg = `MONO_WASM: Failed to load ${asset.name}`; - try { - const isOk = asset.is_optional || - (asset.name.match(/\.pdb$/) && args.ignore_pdb_load_errors); - - if (isOk) - console.debug(msg); - else { - console.error(msg); - throw new Error(msg); + let attemptUrl; + if (sourcePrefix.trim() === "") { + if (asset.behavior === "assembly") + attemptUrl = locateFile(args.assembly_root + "/" + asset.name); + else if (asset.behavior === "resource") { + const path = asset.culture !== "" ? `${asset.culture}/${asset.name}` : asset.name; + attemptUrl = locateFile(args.assembly_root + "/" + path); } - } finally { - onPendingRequestComplete(); - } - } - - let sourcePrefix = sourcesList[sourceIndex]; - sourceIndex++; - - // HACK: Special-case because MSBuild doesn't allow "" as an attribute - if (sourcePrefix === "./") - sourcePrefix = ""; - - let attemptUrl; - if (sourcePrefix.trim() === "") { - if (asset.behavior === "assembly") - attemptUrl = locateFile(args.assembly_root + "/" + asset.name); - else if (asset.behavior === "resource") { - const path = asset.culture !== "" ? `${asset.culture}/${asset.name}` : asset.name; - attemptUrl = locateFile(args.assembly_root + "/" + path); + else + attemptUrl = asset.name; + } else { + attemptUrl = sourcePrefix + asset.name; } - else - attemptUrl = asset.name; - } else { - attemptUrl = sourcePrefix + asset.name; - } - - try { if (asset.name === attemptUrl) { if (ctx.tracing) console.log(`MONO_WASM: Attempting to fetch '${attemptUrl}'`); @@ -424,19 +348,50 @@ function _load_assets_and_runtime(args: MonoConfig) { if (ctx.tracing) console.log(`MONO_WASM: Attempting to fetch '${attemptUrl}' for ${asset.name}`); } - const fetch_promise = fetch_file_cb(attemptUrl); - fetch_promise.then(handleFetchResponse); - } catch (exc) { - console.error(`MONO_WASM: Error fetching ${attemptUrl} ${exc}`); - attemptNextSource(); + try { + const response = await local_fetch(attemptUrl); + if (!response.ok) { + error = new Error(`MONO_WASM: Fetch '${attemptUrl}' for ${asset.name} failed ${response.status} ${response.statusText}`); + continue;// next source + } + + const buffer = await response.arrayBuffer(); + _handle_fetched_asset(ctx, asset, attemptUrl, buffer); + --ctx.pending_count; + error = undefined; + } + catch (err) { + error = new Error(`MONO_WASM: Fetch '${attemptUrl}' for ${asset.name} failed ${err}`); + continue; //next source + } + + if (!error) { + //TODO Module.removeRunDependency(configFilePath); + break; // this source worked, stop searching + } + } + if (error) { + const isOkToFail = asset.is_optional || (asset.name.match(/\.pdb$/) && args.ignore_pdb_load_errors); + if (!isOkToFail) + throw error; } }; + const fetch_promises: Promise[] = []; + // start fetching all assets in parallel + for (const asset of args.assets) { + fetch_promises.push(load_asset(asset)); + } - attemptNextSource(); - }); + await Promise.all(fetch_promises); + + _finalize_startup(args, ctx); + } catch (exc: any) { + console.error("MONO_WASM: Error in mono_load_runtime_and_bcl_args:", exc); + throw exc; + } } -// used from ASP.NET +// used from Blazor export function mono_wasm_load_data_archive(data: TypedArray, prefix: string): boolean { if (data.length < 8) return false; diff --git a/src/mono/wasm/runtime/types.ts b/src/mono/wasm/runtime/types.ts index 93d31c55dc6c2d..c2db47aae5cc4a 100644 --- a/src/mono/wasm/runtime/types.ts +++ b/src/mono/wasm/runtime/types.ts @@ -75,7 +75,7 @@ export function coerceNull(ptr: T | nu export type MonoConfig = { isError: false, assembly_root: string, // the subfolder containing managed assemblies and pdbs - assets: (AssetEntry | AssemblyEntry | SatelliteAssemblyEntry | VfsEntry | IcuData)[], // a list of assets to load along with the runtime. each asset is a dictionary-style Object with the following properties: + assets: AllAssetEntryTypes[], // a list of assets to load along with the runtime. each asset is a dictionary-style Object with the following properties: debug_level?: number, // Either this or the next one needs to be set enable_debugging?: number, // Either this or the previous one needs to be set fetch_file_cb?: Request, // a function (string) invoked to fetch a given file. If no callback is provided a default implementation appropriate for the current environment will be selected (readFileSync in node, fetch elsewhere). If no default implementation is available this call will fail. @@ -97,6 +97,8 @@ export type MonoConfigError = { error: any } +export type AllAssetEntryTypes = AssetEntry | AssemblyEntry | SatelliteAssemblyEntry | VfsEntry | IcuData; + // Types of assets that can be in the mono-config.js/mono-config.json file (taken from /src/tasks/WasmAppBuilder/WasmAppBuilder.cs) export type AssetEntry = { name: string, // the name of the asset, including extension.