Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[wasm] re-try downloading assets #72933

Merged
merged 4 commits into from
Aug 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/mono/wasm/runtime/dotnet.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ declare type MonoConfig = {
globalization_mode?: GlobalizationMode;
diagnostic_tracing?: boolean;
remote_sources?: string[];
max_parallel_downloads?: number;
environment_variables?: {
[i: string]: string;
};
Expand Down
1 change: 1 addition & 0 deletions src/mono/wasm/runtime/imports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export const runtimeHelpers: RuntimeHelpers = <any>{
javaScriptExports: {},
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;
},
Expand Down
2 changes: 2 additions & 0 deletions src/mono/wasm/runtime/polyfills.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,8 @@ export async function fetch_like(url: string, init?: RequestInit): Promise<Respo
return <Response><any>{
ok: false,
url,
status: 500,
statusText: "ERR28: " + e,
arrayBuffer: () => { throw e; },
json: () => { throw e; }
};
Expand Down
164 changes: 92 additions & 72 deletions src/mono/wasm/runtime/startup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,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";
import { init_managed_exports } from "./managed-exports";
import { init_legacy_exports } from "./net6-legacy/corebindings";
import { mono_wasm_load_bytes_into_heap } from "./memory";
Expand All @@ -32,20 +33,17 @@ 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 throttling_promise: Promise<void> | 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<void>();
const beforePreInit = createPromiseController<void>();
const afterPreInit = createPromiseController<void>();
const afterPreRun = createPromiseController<void>();
const beforeOnRuntimeInitialized = createPromiseController<void>();
const afterOnRuntimeInitialized = createPromiseController<void>();
const afterPostRun = createPromiseController<void>();

// 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
Expand Down Expand Up @@ -110,7 +108,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;
Expand All @@ -127,7 +125,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) {
Expand All @@ -151,7 +149,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");
})();
}
Expand All @@ -171,7 +169,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");
}

Expand All @@ -180,7 +178,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
Expand Down Expand Up @@ -209,7 +207,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)[]) {
Expand All @@ -225,7 +223,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
Expand Down Expand Up @@ -427,7 +425,7 @@ async function _instantiate_wasm_module(): Promise<void> {
++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) {
Expand Down Expand Up @@ -627,9 +625,7 @@ function downloadResource(request: ResourceRequest): LoadingResource {
name: request.name, url: request.resolvedUrl!, response
};
}

async function start_asset_download(asset: AssetEntry): Promise<AssetEntry | undefined> {
// we don't addRunDependency to allow download in parallel with onRuntimeInitialized event!
async function start_asset_download_sources(asset: AssetEntry): Promise<AssetEntry> {
if (asset.buffer) {
++downloded_assets_count;
const buffer = asset.buffer;
Expand All @@ -651,23 +647,8 @@ async function start_asset_download(asset: AssetEntry): Promise<AssetEntry | und
return asset;
}

while (throttling_promise) {
await throttling_promise;
}
++parallel_count;
if (parallel_count == max_parallel_downloads) {
if (runtimeHelpers.diagnostic_tracing)
console.debug("MONO_WASM: Throttling further parallel downloads");

throttling_promise = new Promise((resolve) => {
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
Expand Down Expand Up @@ -707,63 +688,102 @@ async function start_asset_download(asset: AssetEntry): Promise<AssetEntry | und
hash: asset.hash,
behavior: asset.behavior
});
const response = await loadingResource.response;
response = await loadingResource.response;
if (!response.ok) {
error = new Error(`MONO_WASM: download '${attemptUrl}' for ${asset.name} failed ${response.status} ${response.statusText}`);
continue;// next source
}
asset.pending = loadingResource;
result = asset;
++downloded_assets_count;
error = undefined;
return asset;
}
catch (err) {
error = new Error(`MONO_WASM: download '${attemptUrl}' for ${asset.name} failed ${err}`);
continue; //next source
}

if (!error) {
break; // this source worked, stop searching
}
}
throw response;
}

--parallel_count;
if (throttling_promise && parallel_count == ((max_parallel_downloads / 2) | 0)) {
if (runtimeHelpers.diagnostic_tracing)
console.debug("MONO_WASM: Resuming more parallel downloads");
throttling_promise_resolve!();
throttling_promise = undefined;
let throttling: PromiseAndController<void> | undefined;
async function start_asset_download_throttle(asset: AssetEntry): Promise<AssetEntry | undefined> {
// we don't addRunDependency to allow download in parallel with onRuntimeInitialized event!
while (throttling) {
await throttling.promise;
}

if (error) {
try {
++parallel_count;
if (parallel_count == runtimeHelpers.max_parallel_downloads) {
if (runtimeHelpers.diagnostic_tracing)
console.debug("MONO_WASM: Throttling further parallel downloads");
throttling = createPromiseController<void>();
}
return await start_asset_download_sources(asset);
}
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 == runtimeHelpers.max_parallel_downloads - 1) {
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<AssetEntry | undefined> {
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<void>();
async function mono_download_assets(): Promise<void> {
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 asset_promises: Promise<void>[] = [];

const download_promises: Promise<AssetEntry | undefined>[] = [];
// 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<void>[] = [];
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));
})());
}
}

Expand Down
2 changes: 2 additions & 0 deletions src/mono/wasm/runtime/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,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
Expand Down Expand Up @@ -154,6 +155,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;
Expand Down