Skip to content

Commit acadc53

Browse files
authored
[browser][coreCLR] asset loading improvements (#122886)
1 parent d967910 commit acadc53

File tree

20 files changed

+315
-152
lines changed

20 files changed

+315
-152
lines changed

src/native/corehost/browserhost/host/host.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ import { _ems_ } from "../../../libs/Common/JavaScript/ems-ambient";
66

77
const loadedAssemblies: Map<string, { ptr: number, length: number }> = new Map();
88

9+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
10+
export function registerPdbBytes(bytes: Uint8Array, asset: { name: string, virtualPath: string }) {
11+
// WASM-TODO: https://github.com/dotnet/runtime/issues/122921
12+
}
13+
914
export function registerDllBytes(bytes: Uint8Array, asset: { name: string, virtualPath: string }) {
1015
const sp = _ems_.Module.stackSave();
1116
try {

src/native/corehost/browserhost/host/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { _ems_ } from "../../../libs/Common/JavaScript/ems-ambient";
77

88
import GitHash from "consts:gitHash";
99

10-
import { runMain, runMainAndExit, registerDllBytes, installVfsFile, loadIcuData, initializeCoreCLR } from "./host";
10+
import { runMain, runMainAndExit, registerDllBytes, installVfsFile, loadIcuData, initializeCoreCLR, registerPdbBytes } from "./host";
1111

1212
export function dotnetInitializeModule(internals: InternalExchange): void {
1313
if (!Array.isArray(internals)) throw new Error("Expected internals to be an array");
@@ -27,6 +27,7 @@ export function dotnetInitializeModule(internals: InternalExchange): void {
2727
installVfsFile,
2828
loadIcuData,
2929
initializeCoreCLR,
30+
registerPdbBytes,
3031
});
3132
_ems_.dotnetUpdateInternals(internals, _ems_.dotnetUpdateInternalsSubscriber);
3233
function browserHostExportsToTable(map: BrowserHostExports): BrowserHostExportsTable {
@@ -36,6 +37,7 @@ export function dotnetInitializeModule(internals: InternalExchange): void {
3637
map.installVfsFile,
3738
map.loadIcuData,
3839
map.initializeCoreCLR,
40+
map.registerPdbBytes,
3941
];
4042
}
4143
}

src/native/corehost/browserhost/loader/assets.ts

Lines changed: 198 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
import type { JsModuleExports, JsAsset, AssemblyAsset, WasmAsset, IcuAsset, EmscriptenModuleInternal, InstantiateWasmSuccessCallback, WebAssemblyBootResourceType, AssetEntryInternal, LoadBootResourceCallback } from "./types";
4+
import type { JsModuleExports, JsAsset, AssemblyAsset, WasmAsset, IcuAsset, EmscriptenModuleInternal, InstantiateWasmSuccessCallback, WebAssemblyBootResourceType, AssetEntryInternal, PromiseCompletionSource, LoadBootResourceCallback } from "./types";
55

6-
import { dotnetAssert, dotnetGetInternals, dotnetBrowserHostExports, dotnetUpdateInternals, dotnetLogger } from "./cross-module";
7-
import { ENVIRONMENT_IS_WEB } from "./per-module";
8-
import { createPromiseCompletionSource } from "./promise-completion-source";
6+
import { dotnetAssert, dotnetLogger, dotnetInternals, dotnetBrowserHostExports, dotnetUpdateInternals, Module } from "./cross-module";
7+
import { ENVIRONMENT_IS_WEB, ENVIRONMENT_IS_SHELL, ENVIRONMENT_IS_NODE } from "./per-module";
8+
import { createPromiseCompletionSource, delay } from "./promise-completion-source";
99
import { locateFile, makeURLAbsoluteWithApplicationBase } from "./bootstrap";
1010
import { fetchLike } from "./polyfills";
1111
import { loaderConfig } from "./config";
1212

13+
let throttlingPCS: PromiseCompletionSource<void> | undefined;
14+
let currentParallelDownloads = 0;
15+
let downloadedAssetsCount = 0;
16+
let totalAssetsToDownload = 0;
1317
let loadBootResourceCallback: LoadBootResourceCallback | undefined = undefined;
1418

1519
export function setLoadBootResourceCallback(callback: LoadBootResourceCallback | undefined): void {
@@ -18,28 +22,39 @@ export function setLoadBootResourceCallback(callback: LoadBootResourceCallback |
1822

1923
export let wasmBinaryPromise: Promise<Response> | undefined = undefined;
2024
export const nativeModulePromiseController = createPromiseCompletionSource<EmscriptenModuleInternal>(() => {
21-
dotnetUpdateInternals(dotnetGetInternals());
25+
dotnetUpdateInternals(dotnetInternals);
2226
});
2327

24-
export async function loadJSModule(asset: JsAsset): Promise<JsModuleExports> {
25-
const assetInternal = asset as AssetEntryInternal;
26-
if (assetInternal.name && !asset.resolvedUrl) {
27-
asset.resolvedUrl = locateFile(assetInternal.name, true);
28-
}
29-
assetInternal.behavior = "js-module-dotnet";
30-
if (typeof loadBootResourceCallback === "function") {
31-
const type = runtimeToBlazorAssetTypeMap[assetInternal.behavior];
32-
dotnetAssert.check(type, `Unsupported asset behavior: ${assetInternal.behavior}`);
33-
const customLoadResult = loadBootResourceCallback(type, assetInternal.name, asset.resolvedUrl!, assetInternal.integrity!, assetInternal.behavior);
34-
dotnetAssert.check(typeof customLoadResult === "string", "loadBootResourceCallback for JS modules must return string URL");
35-
asset.resolvedUrl = makeURLAbsoluteWithApplicationBase(customLoadResult);
36-
}
28+
export async function loadDotnetModule(asset: JsAsset): Promise<JsModuleExports> {
29+
return loadJSModule(asset);
30+
}
3731

38-
if (!asset.resolvedUrl) throw new Error("Invalid config, resources is not set");
39-
return await import(/* webpackIgnore: true */ asset.resolvedUrl);
32+
export async function loadJSModule(asset: JsAsset): Promise<any> {
33+
let mod: JsModuleExports = asset.moduleExports;
34+
totalAssetsToDownload++;
35+
if (!mod) {
36+
const assetInternal = asset as AssetEntryInternal;
37+
if (assetInternal.name && !asset.resolvedUrl) {
38+
asset.resolvedUrl = locateFile(assetInternal.name, true);
39+
}
40+
assetInternal.behavior = "js-module-dotnet";
41+
if (typeof loadBootResourceCallback === "function") {
42+
const type = runtimeToBlazorAssetTypeMap[assetInternal.behavior];
43+
dotnetAssert.check(type, `Unsupported asset behavior: ${assetInternal.behavior}`);
44+
const customLoadResult = loadBootResourceCallback(type, assetInternal.name, asset.resolvedUrl!, assetInternal.integrity!, assetInternal.behavior);
45+
dotnetAssert.check(typeof customLoadResult === "string", "loadBootResourceCallback for JS modules must return string URL");
46+
asset.resolvedUrl = makeURLAbsoluteWithApplicationBase(customLoadResult);
47+
}
48+
49+
if (!asset.resolvedUrl) throw new Error("Invalid config, resources is not set");
50+
mod = await import(/* webpackIgnore: true */ asset.resolvedUrl);
51+
}
52+
onDownloadedAsset();
53+
return mod;
4054
}
4155

4256
export function fetchWasm(asset: WasmAsset): Promise<Response> {
57+
totalAssetsToDownload++;
4358
const assetInternal = asset as AssetEntryInternal;
4459
if (assetInternal.name && !asset.resolvedUrl) {
4560
asset.resolvedUrl = locateFile(assetInternal.name);
@@ -56,10 +71,12 @@ export async function instantiateWasm(imports: WebAssembly.Imports, successCallb
5671
const data = await res.arrayBuffer();
5772
const module = await WebAssembly.compile(data);
5873
const instance = await WebAssembly.instantiate(module, imports);
74+
onDownloadedAsset();
5975
successCallback(instance, module);
6076
} else {
6177
const instantiated = await WebAssembly.instantiateStreaming(wasmBinaryPromise, imports);
6278
await checkResponseOk();
79+
onDownloadedAsset();
6380
successCallback(instantiated.instance, instantiated.module);
6481
}
6582

@@ -78,17 +95,22 @@ export async function instantiateWasm(imports: WebAssembly.Imports, successCallb
7895
}
7996

8097
export async function fetchIcu(asset: IcuAsset): Promise<void> {
98+
totalAssetsToDownload++;
8199
const assetInternal = asset as AssetEntryInternal;
82100
if (assetInternal.name && !asset.resolvedUrl) {
83101
asset.resolvedUrl = locateFile(assetInternal.name);
84102
}
85103
assetInternal.behavior = "icu";
86104
const bytes = await fetchBytes(assetInternal);
87105
await nativeModulePromiseController.promise;
88-
dotnetBrowserHostExports.loadIcuData(bytes);
106+
onDownloadedAsset();
107+
if (bytes) {
108+
dotnetBrowserHostExports.loadIcuData(bytes);
109+
}
89110
}
90111

91112
export async function fetchDll(asset: AssemblyAsset): Promise<void> {
113+
totalAssetsToDownload++;
92114
const assetInternal = asset as AssetEntryInternal;
93115
if (assetInternal.name && !asset.resolvedUrl) {
94116
asset.resolvedUrl = locateFile(assetInternal.name);
@@ -97,38 +119,176 @@ export async function fetchDll(asset: AssemblyAsset): Promise<void> {
97119
const bytes = await fetchBytes(assetInternal);
98120
await nativeModulePromiseController.promise;
99121

100-
dotnetBrowserHostExports.registerDllBytes(bytes, asset);
122+
onDownloadedAsset();
123+
if (bytes) {
124+
dotnetBrowserHostExports.registerDllBytes(bytes, asset);
125+
}
126+
}
127+
128+
export async function fetchPdb(asset: AssemblyAsset): Promise<void> {
129+
totalAssetsToDownload++;
130+
const assetInternal = asset as AssetEntryInternal;
131+
if (assetInternal.name && !asset.resolvedUrl) {
132+
asset.resolvedUrl = locateFile(assetInternal.name);
133+
}
134+
assetInternal.behavior = "pdb";
135+
assetInternal.isOptional = assetInternal.isOptional || loaderConfig.ignorePdbLoadErrors;
136+
const bytes = await fetchBytes(assetInternal);
137+
await nativeModulePromiseController.promise;
138+
139+
onDownloadedAsset();
140+
if (bytes) {
141+
dotnetBrowserHostExports.registerPdbBytes(bytes, asset);
142+
}
101143
}
102144

103145
export async function fetchVfs(asset: AssemblyAsset): Promise<void> {
146+
totalAssetsToDownload++;
104147
const assetInternal = asset as AssetEntryInternal;
105148
if (assetInternal.name && !asset.resolvedUrl) {
106149
asset.resolvedUrl = locateFile(assetInternal.name);
107150
}
108151
assetInternal.behavior = "vfs";
109152
const bytes = await fetchBytes(assetInternal);
110153
await nativeModulePromiseController.promise;
111-
112-
dotnetBrowserHostExports.installVfsFile(bytes, asset);
154+
onDownloadedAsset();
155+
if (bytes) {
156+
dotnetBrowserHostExports.installVfsFile(bytes, asset);
157+
}
113158
}
114159

115-
async function fetchBytes(asset: AssetEntryInternal): Promise<Uint8Array> {
160+
async function fetchBytes(asset: AssetEntryInternal): Promise<Uint8Array | null> {
116161
dotnetAssert.check(asset && asset.resolvedUrl, "Bad asset.resolvedUrl");
117162
const response = await loadResource(asset);
118163
if (!response.ok) {
164+
if (asset.isOptional) {
165+
dotnetLogger.warn(`Optional resource '${asset.name}' failed to load from '${asset.resolvedUrl}'. HTTP status: ${response.status} ${response.statusText}`);
166+
return null;
167+
}
119168
throw new Error(`Failed to load resource '${asset.name}' from '${asset.resolvedUrl}'. HTTP status: ${response.status} ${response.statusText}`);
120169
}
121-
const buffer = await response.arrayBuffer();
170+
const buffer = await (asset.buffer || response.arrayBuffer());
122171
return new Uint8Array(buffer);
123172
}
124173

125-
async function loadResource(asset: AssetEntryInternal): Promise<Response> {
174+
function loadResource(asset: AssetEntryInternal): Promise<Response> {
175+
if ("dotnetwasm" === asset.behavior) {
176+
// `response.arrayBuffer()` can't be called twice.
177+
return loadResourceFetch(asset);
178+
}
179+
if (ENVIRONMENT_IS_SHELL || ENVIRONMENT_IS_NODE || asset.resolvedUrl && asset.resolvedUrl.indexOf("file://") !== -1) {
180+
// no need to retry or throttle local file access
181+
return loadResourceFetch(asset);
182+
}
183+
if (!loaderConfig.enableDownloadRetry) {
184+
// only throttle, no retry
185+
return loadResourceThrottle(asset);
186+
}
187+
// retry and throttle
188+
return loadResourceRetry(asset);
189+
}
190+
191+
const noRetryStatusCodes = new Set<number>([400, 401, 403, 404, 405, 406, 409, 410, 411, 413, 414, 415, 422, 426, 501, 505,]);
192+
async function loadResourceRetry(asset: AssetEntryInternal): Promise<Response> {
193+
let response: Response;
194+
response = await loadResourceAttempt();
195+
if (response.ok || asset.isOptional || noRetryStatusCodes.has(response.status)) {
196+
return response;
197+
}
198+
if (response.status === 429) {
199+
// Too Many Requests
200+
await delay(100);
201+
}
202+
response = await loadResourceAttempt();
203+
if (response.ok || noRetryStatusCodes.has(response.status)) {
204+
return response;
205+
}
206+
await delay(100); // wait 100ms before the last retry
207+
response = await loadResourceAttempt();
208+
if (response.ok) {
209+
return response;
210+
}
211+
throw new Error(`Failed to load resource '${asset.name}' from '${asset.resolvedUrl}' after multiple attempts. Last HTTP status: ${response.status} ${response.statusText}`);
212+
213+
async function loadResourceAttempt(): Promise<Response> {
214+
let response: Response;
215+
try {
216+
response = await loadResourceThrottle(asset);
217+
if (!response) {
218+
response = {
219+
ok: false,
220+
status: -1,
221+
statusText: "No response",
222+
} as any;
223+
}
224+
} catch (err: any) {
225+
response = {
226+
ok: false,
227+
status: -1,
228+
statusText: err.message || "Exception during fetch",
229+
} as any;
230+
}
231+
return response;
232+
}
233+
}
234+
235+
// in order to prevent net::ERR_INSUFFICIENT_RESOURCES if we start downloading too many files at same time on a device with low resources
236+
async function loadResourceThrottle(asset: AssetEntryInternal): Promise<Response> {
237+
while (throttlingPCS) {
238+
await throttlingPCS.promise;
239+
}
240+
try {
241+
++currentParallelDownloads;
242+
if (currentParallelDownloads === loaderConfig.maxParallelDownloads) {
243+
dotnetLogger.debug("Throttling further parallel downloads");
244+
throttlingPCS = createPromiseCompletionSource<void>();
245+
}
246+
const responsePromise = loadResourceFetch(asset);
247+
const response = await responsePromise;
248+
dotnetAssert.check(response, "Bad response in loadResourceThrottle");
249+
250+
asset.buffer = await response.arrayBuffer();
251+
return response;
252+
} finally {
253+
--currentParallelDownloads;
254+
if (throttlingPCS && currentParallelDownloads == loaderConfig.maxParallelDownloads! - 1) {
255+
dotnetLogger.debug("Resuming more parallel downloads");
256+
const oldThrottlingPCS = throttlingPCS;
257+
throttlingPCS = undefined;
258+
oldThrottlingPCS.resolve();
259+
}
260+
}
261+
}
262+
263+
async function loadResourceFetch(asset: AssetEntryInternal): Promise<Response> {
264+
if (asset.buffer) {
265+
return <Response><any>{
266+
ok: true,
267+
headers: {
268+
length: 0,
269+
get: () => null
270+
},
271+
url: asset.resolvedUrl,
272+
arrayBuffer: () => Promise.resolve(asset.buffer!),
273+
json: () => {
274+
throw new Error("NotImplementedException");
275+
},
276+
text: () => {
277+
throw new Error("NotImplementedException");
278+
}
279+
};
280+
}
281+
if (asset.pendingDownload) {
282+
return asset.pendingDownload.response;
283+
}
126284
if (typeof loadBootResourceCallback === "function") {
127285
const type = runtimeToBlazorAssetTypeMap[asset.behavior];
128286
dotnetAssert.check(type, `Unsupported asset behavior: ${asset.behavior}`);
129287
const customLoadResult = loadBootResourceCallback(type, asset.name, asset.resolvedUrl!, asset.integrity!, asset.behavior);
130288
if (typeof customLoadResult === "string") {
131289
asset.resolvedUrl = makeURLAbsoluteWithApplicationBase(customLoadResult);
290+
} else if (typeof customLoadResult === "object") {
291+
return customLoadResult as any;
132292
}
133293
}
134294
dotnetAssert.check(asset.resolvedUrl, "Bad asset.resolvedUrl");
@@ -157,6 +317,17 @@ async function loadResource(asset: AssetEntryInternal): Promise<Response> {
157317
return fetchLike(asset.resolvedUrl!, fetchOptions);
158318
}
159319

320+
function onDownloadedAsset() {
321+
++downloadedAssetsCount;
322+
if (Module.onDownloadResourceProgress) {
323+
Module.onDownloadResourceProgress(downloadedAssetsCount, totalAssetsToDownload);
324+
}
325+
}
326+
327+
export function verifyAllAssetsDownloaded(): void {
328+
dotnetAssert.check(downloadedAssetsCount === totalAssetsToDownload, `Not all assets were downloaded. Downloaded ${downloadedAssetsCount} out of ${totalAssetsToDownload}`);
329+
}
330+
160331
const runtimeToBlazorAssetTypeMap: { [key: string]: WebAssemblyBootResourceType | undefined } = {
161332
"resource": "assembly",
162333
"assembly": "assembly",

src/native/corehost/browserhost/loader/config.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export function validateLoaderConfig(): void {
2020

2121

2222
export function mergeLoaderConfig(source: Partial<LoaderConfigInternal>): void {
23-
normalizeConfig(loaderConfig);
23+
defaultConfig(loaderConfig);
2424
normalizeConfig(source);
2525
mergeConfigs(loaderConfig, source);
2626
}
@@ -70,19 +70,23 @@ function mergeResources(target: Assets, source: Assets): Assets {
7070
return Object.assign(target, source);
7171
}
7272

73-
74-
function normalizeConfig(target: LoaderConfigInternal) {
75-
if (!target.resources) target.resources = {} as any;
76-
normalizeResources(target.resources!);
77-
if (!target.environmentVariables) target.environmentVariables = {};
78-
if (!target.runtimeOptions) target.runtimeOptions = [];
73+
function defaultConfig(target: LoaderConfigInternal) {
7974
if (target.appendElementOnExit === undefined) target.appendElementOnExit = false;
8075
if (target.logExitCode === undefined) target.logExitCode = false;
8176
if (target.exitOnUnhandledError === undefined) target.exitOnUnhandledError = false;
8277
if (target.loadAllSatelliteResources === undefined) target.loadAllSatelliteResources = false;
8378
if (target.debugLevel === undefined) target.debugLevel = 0;
8479
if (target.diagnosticTracing === undefined) target.diagnosticTracing = false;
8580
if (target.virtualWorkingDirectory === undefined) target.virtualWorkingDirectory = "/";
81+
if (target.maxParallelDownloads === undefined) target.maxParallelDownloads = 16;
82+
normalizeConfig(target);
83+
}
84+
85+
function normalizeConfig(target: LoaderConfigInternal) {
86+
if (!target.resources) target.resources = {} as any;
87+
normalizeResources(target.resources!);
88+
if (!target.environmentVariables) target.environmentVariables = {};
89+
if (!target.runtimeOptions) target.runtimeOptions = [];
8690
}
8791

8892
function normalizeResources(target: Assets) {

0 commit comments

Comments
 (0)