From d9c9cbad978e973c6d815563ab3cf951c8b1bf8b Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 27 Jul 2022 13:11:13 +0200 Subject: [PATCH 1/2] wip --- src/mono/wasm/runtime/polyfills.ts | 2 + src/mono/wasm/runtime/startup.ts | 162 ++++++++++++++++------------- 2 files changed, 93 insertions(+), 71 deletions(-) diff --git a/src/mono/wasm/runtime/polyfills.ts b/src/mono/wasm/runtime/polyfills.ts index abb90829d7cb4..46bb4ef1c0ff5 100644 --- a/src/mono/wasm/runtime/polyfills.ts +++ b/src/mono/wasm/runtime/polyfills.ts @@ -250,6 +250,8 @@ export async function fetch_like(url: string, init?: RequestInit): Promise{ ok: false, url, + status: 500, + statusText: "ERR28: " + e, arrayBuffer: () => { throw e; }, json: () => { throw e; } }; diff --git a/src/mono/wasm/runtime/startup.ts b/src/mono/wasm/runtime/startup.ts index 137c12a87947a..b7694328e3664 100644 --- a/src/mono/wasm/runtime/startup.ts +++ b/src/mono/wasm/runtime/startup.ts @@ -21,9 +21,10 @@ import { mono_wasm_new_root } from "./roots"; import { init_crypto } from "./crypto-worker"; import { init_polyfills_async } from "./polyfills"; import * as pthreads_worker from "./pthreads/worker"; -import { createPromiseController } from "./promise-controller"; +import { createPromiseController, PromiseAndController } from "./promise-controller"; import { string_decoder } from "./strings"; import { mono_wasm_init_diagnostics } from "./diagnostics/index"; +import { delay } from "./promise-utils"; let all_assets_loaded_in_memory: Promise | null = null; const loaded_files: { url?: string, file: string }[] = []; @@ -33,17 +34,15 @@ let downloded_assets_count = 0; const max_parallel_downloads = 100; // in order to prevent net::ERR_INSUFFICIENT_RESOURCES if we start downloading too many files at same time let parallel_count = 0; -let throttling_promise: Promise | undefined = undefined; -let throttling_promise_resolve: Function | undefined = undefined; let config: MonoConfig = undefined as any; -const afterInstantiateWasm = createPromiseController(); -const beforePreInit = createPromiseController(); -const afterPreInit = createPromiseController(); -const afterPreRun = createPromiseController(); -const beforeOnRuntimeInitialized = createPromiseController(); -const afterOnRuntimeInitialized = createPromiseController(); -const afterPostRun = createPromiseController(); +const afterInstantiateWasm = createPromiseController(); +const beforePreInit = createPromiseController(); +const afterPreInit = createPromiseController(); +const afterPreRun = createPromiseController(); +const beforeOnRuntimeInitialized = createPromiseController(); +const afterOnRuntimeInitialized = createPromiseController(); +const afterPostRun = createPromiseController(); // we are making emscripten startup async friendly // emscripten is executing the events without awaiting it and so we need to block progress via PromiseControllers above @@ -108,7 +107,7 @@ function instantiateWasm( if (userInstantiateWasm) { const exports = userInstantiateWasm(imports, (instance: WebAssembly.Instance, module: WebAssembly.Module) => { - afterInstantiateWasm.promise_control.resolve(null); + afterInstantiateWasm.promise_control.resolve(); successCallback(instance, module); }); return exports; @@ -125,7 +124,7 @@ function preInit(isCustomStartup: boolean, userPreInit: (() => void)[]) { try { mono_wasm_pre_init_essential(); if (runtimeHelpers.diagnostic_tracing) console.debug("MONO_WASM: preInit"); - beforePreInit.promise_control.resolve(null); + beforePreInit.promise_control.resolve(); // all user Module.preInit callbacks userPreInit.forEach(fn => fn()); } catch (err) { @@ -149,7 +148,7 @@ function preInit(isCustomStartup: boolean, userPreInit: (() => void)[]) { throw err; } // signal next stage - afterPreInit.promise_control.resolve(null); + afterPreInit.promise_control.resolve(); Module.removeRunDependency("mono_pre_init"); })(); } @@ -169,7 +168,7 @@ async function preRunAsync(userPreRun: (() => void)[]) { throw err; } // signal next stage - afterPreRun.promise_control.resolve(null); + afterPreRun.promise_control.resolve(); Module.removeRunDependency("mono_pre_run_async"); } @@ -178,7 +177,7 @@ async function onRuntimeInitializedAsync(isCustomStartup: boolean, userOnRuntime await afterPreRun.promise; if (runtimeHelpers.diagnostic_tracing) console.debug("MONO_WASM: onRuntimeInitialized"); // signal this stage, this will allow pending assets to allocate memory - beforeOnRuntimeInitialized.promise_control.resolve(null); + beforeOnRuntimeInitialized.promise_control.resolve(); try { if (!isCustomStartup) { // wait for all assets in memory @@ -207,7 +206,7 @@ async function onRuntimeInitializedAsync(isCustomStartup: boolean, userOnRuntime throw err; } // signal next stage - afterOnRuntimeInitialized.promise_control.resolve(null); + afterOnRuntimeInitialized.promise_control.resolve(); } async function postRunAsync(userpostRun: (() => void)[]) { @@ -223,7 +222,7 @@ async function postRunAsync(userpostRun: (() => void)[]) { throw err; } // signal next stage - afterPostRun.promise_control.resolve(null); + afterPostRun.promise_control.resolve(); } // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types @@ -421,7 +420,7 @@ async function _instantiate_wasm_module(): Promise { ++instantiated_assets_count; wasm_success_callback!(compiledInstance, compiledModule); if (runtimeHelpers.diagnostic_tracing) console.debug("MONO_WASM: instantiateWasm done"); - afterInstantiateWasm.promise_control.resolve(null); + afterInstantiateWasm.promise_control.resolve(); wasm_success_callback = null; wasm_module_imports = null; } catch (err) { @@ -659,9 +658,7 @@ function downloadResource(request: ResourceRequest): LoadingResource { name: request.name, url: request.resolvedUrl!, response }; } - -async function start_asset_download(asset: AssetEntry): Promise { - // we don't addRunDependency to allow download in parallel with onRuntimeInitialized event! +async function start_asset_download_sources(asset: AssetEntry): Promise { if (asset.buffer) { ++downloded_assets_count; const buffer = asset.buffer; @@ -683,23 +680,8 @@ async function start_asset_download(asset: AssetEntry): Promise { - throttling_promise_resolve = resolve; - }); - } - const sourcesList = asset.load_remote && config.remote_sources ? config.remote_sources : [""]; - - let error = undefined; - let result: AssetEntry | undefined = undefined; + let response: Response | undefined = undefined; for (let sourcePrefix of sourcesList) { sourcePrefix = sourcePrefix.trim(); // HACK: Special-case because MSBuild doesn't allow "" as an attribute @@ -739,63 +721,101 @@ async function start_asset_download(asset: AssetEntry): Promise | undefined; +async function start_asset_download_throttle(asset: AssetEntry): Promise { + // we don't addRunDependency to allow download in parallel with onRuntimeInitialized event! + try { + ++parallel_count; + if (parallel_count == max_parallel_downloads) { + if (runtimeHelpers.diagnostic_tracing) + console.debug("MONO_WASM: Throttling further parallel downloads"); + throttling = createPromiseController(); + } + while (throttling) { + await throttling.promise; + } + return await start_asset_download_sources(asset); } - - if (error) { + catch (response: any) { const isOkToFail = asset.is_optional || (asset.name.match(/\.pdb$/) && config.ignore_pdb_load_errors); - if (!isOkToFail) - throw error; + if (!isOkToFail) { + const err: any = new Error(`MONO_WASM: download '${response.url}' for ${asset.name} failed ${response.status} ${response.statusText}`); + err.status = response.status; + throw err; + } } + finally { + --parallel_count; + if (throttling && parallel_count == ((max_parallel_downloads / 2) | 0)) { + if (runtimeHelpers.diagnostic_tracing) + console.debug("MONO_WASM: Resuming more parallel downloads"); + const old_throttling = throttling; + throttling = undefined; + old_throttling.promise_control.resolve(); + } + } +} - return result; +async function start_asset_download(asset: AssetEntry): Promise { + try { + return await start_asset_download_throttle(asset); + } catch (err: any) { + if (err && err.status == 404) { + throw err; + } + // second attempt only after all first attempts are queued + await allDownloadsQueued.promise; + try { + return await start_asset_download_throttle(asset); + } catch (err) { + // third attempt after small delay + await delay(100); + return await start_asset_download_throttle(asset); + } + } } +const allDownloadsQueued = createPromiseController(); async function mono_download_assets(): Promise { if (runtimeHelpers.diagnostic_tracing) console.debug("MONO_WASM: mono_download_assets"); try { - const asset_promises: Promise[] = []; - + const download_promises: Promise[] = []; // start fetching and instantiating all assets in parallel for (const asset of config.assets || []) { if (asset.behavior != "dotnetwasm") { - const downloadedAsset = await start_asset_download(asset); - if (downloadedAsset) { - asset_promises.push((async () => { - const url = downloadedAsset.pending!.url; - const response = await downloadedAsset.pending!.response; - downloadedAsset.pending = undefined; //GC - const buffer = await response.arrayBuffer(); - await beforeOnRuntimeInitialized.promise; - // this is after onRuntimeInitialized - _instantiate_asset(downloadedAsset, url, new Uint8Array(buffer)); - })()); - } + download_promises.push(start_asset_download(asset)); + } + } + allDownloadsQueued.promise_control.resolve(); + + const asset_promises: Promise[] = []; + for (const downloadPromise of download_promises) { + const downloadedAsset = await downloadPromise; + if (downloadedAsset) { + asset_promises.push((async () => { + const url = downloadedAsset.pending!.url; + const response = await downloadedAsset.pending!.response; + downloadedAsset.pending = undefined; //GC + const buffer = await response.arrayBuffer(); + await beforeOnRuntimeInitialized.promise; + // this is after onRuntimeInitialized + _instantiate_asset(downloadedAsset, url, new Uint8Array(buffer)); + })()); } } From 5d6db87862c60468704940b8d61cdfbfbad2e69e Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Thu, 28 Jul 2022 17:17:08 +0200 Subject: [PATCH 2/2] configurable max_parallel_downloads --- src/mono/wasm/runtime/dotnet.d.ts | 1 + src/mono/wasm/runtime/imports.ts | 1 + src/mono/wasm/runtime/startup.ts | 12 ++++++------ src/mono/wasm/runtime/types.ts | 2 ++ 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/mono/wasm/runtime/dotnet.d.ts b/src/mono/wasm/runtime/dotnet.d.ts index 5e5c5a5db5094..b5a55fdef4b56 100644 --- a/src/mono/wasm/runtime/dotnet.d.ts +++ b/src/mono/wasm/runtime/dotnet.d.ts @@ -155,6 +155,7 @@ declare type MonoConfig = { globalization_mode?: GlobalizationMode; diagnostic_tracing?: boolean; remote_sources?: string[]; + max_parallel_downloads?: number; environment_variables?: { [i: string]: string; }; diff --git a/src/mono/wasm/runtime/imports.ts b/src/mono/wasm/runtime/imports.ts index 5bec05a72a10f..c41e11cc620cb 100644 --- a/src/mono/wasm/runtime/imports.ts +++ b/src/mono/wasm/runtime/imports.ts @@ -53,6 +53,7 @@ export const runtimeHelpers: RuntimeHelpers = { classname: "Runtime", mono_wasm_load_runtime_done: false, mono_wasm_bindings_is_ready: false, + max_parallel_downloads: 16, get mono_wasm_runtime_is_ready() { return runtime_is_ready; }, diff --git a/src/mono/wasm/runtime/startup.ts b/src/mono/wasm/runtime/startup.ts index b7694328e3664..b22f3e79b65f3 100644 --- a/src/mono/wasm/runtime/startup.ts +++ b/src/mono/wasm/runtime/startup.ts @@ -31,7 +31,6 @@ const loaded_files: { url?: string, file: string }[] = []; const loaded_assets: { [id: string]: [VoidPtr, number] } = Object.create(null); let instantiated_assets_count = 0; let downloded_assets_count = 0; -const max_parallel_downloads = 100; // in order to prevent net::ERR_INSUFFICIENT_RESOURCES if we start downloading too many files at same time let parallel_count = 0; let config: MonoConfig = undefined as any; @@ -739,16 +738,16 @@ async function start_asset_download_sources(asset: AssetEntry): Promise | undefined; async function start_asset_download_throttle(asset: AssetEntry): Promise { // we don't addRunDependency to allow download in parallel with onRuntimeInitialized event! + while (throttling) { + await throttling.promise; + } try { ++parallel_count; - if (parallel_count == max_parallel_downloads) { + if (parallel_count == runtimeHelpers.max_parallel_downloads) { if (runtimeHelpers.diagnostic_tracing) console.debug("MONO_WASM: Throttling further parallel downloads"); throttling = createPromiseController(); } - while (throttling) { - await throttling.promise; - } return await start_asset_download_sources(asset); } catch (response: any) { @@ -761,7 +760,7 @@ async function start_asset_download_throttle(asset: AssetEntry): Promise(); async function mono_download_assets(): Promise { if (runtimeHelpers.diagnostic_tracing) console.debug("MONO_WASM: mono_download_assets"); + runtimeHelpers.max_parallel_downloads = runtimeHelpers.config.max_parallel_downloads || runtimeHelpers.max_parallel_downloads; try { const download_promises: Promise[] = []; // start fetching and instantiating all assets in parallel diff --git a/src/mono/wasm/runtime/types.ts b/src/mono/wasm/runtime/types.ts index b909569af544d..a022758f77367 100644 --- a/src/mono/wasm/runtime/types.ts +++ b/src/mono/wasm/runtime/types.ts @@ -80,6 +80,7 @@ export type MonoConfig = { globalization_mode?: GlobalizationMode, // configures the runtime's globalization mode diagnostic_tracing?: boolean // enables diagnostic log messages during startup remote_sources?: string[], // additional search locations for assets. Sources will be checked in sequential order until the asset is found. The string "./" indicates to load from the application directory (as with the files in assembly_list), and a fully-qualified URL like "https://example.com/" indicates that asset loads can be attempted from a remote server. Sources must end with a "/". + max_parallel_downloads?: number, // we are throttling parallel downloads in order to avoid net::ERR_INSUFFICIENT_RESOURCES on chrome environment_variables?: { [i: string]: string; }, // dictionary-style Object containing environment variables @@ -151,6 +152,7 @@ export type RuntimeHelpers = { mono_wasm_bindings_is_ready: boolean; loaded_files: string[]; + max_parallel_downloads: number; config: MonoConfig; diagnostic_tracing: boolean; enable_debugging: number;