Skip to content
5 changes: 5 additions & 0 deletions src/native/corehost/browserhost/host/host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ import { _ems_ } from "../../../libs/Common/JavaScript/ems-ambient";

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

// eslint-disable-next-line @typescript-eslint/no-unused-vars
export function registerPdbBytes(bytes: Uint8Array, asset: { name: string, virtualPath: string }) {
// WASM-TODO: https://github.com/dotnet/runtime/issues/122921
}

export function registerDllBytes(bytes: Uint8Array, asset: { name: string, virtualPath: string }) {
const sp = _ems_.Module.stackSave();
try {
Expand Down
4 changes: 3 additions & 1 deletion src/native/corehost/browserhost/host/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { _ems_ } from "../../../libs/Common/JavaScript/ems-ambient";

import GitHash from "consts:gitHash";

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

export function dotnetInitializeModule(internals: InternalExchange): void {
if (!Array.isArray(internals)) throw new Error("Expected internals to be an array");
Expand All @@ -27,6 +27,7 @@ export function dotnetInitializeModule(internals: InternalExchange): void {
installVfsFile,
loadIcuData,
initializeCoreCLR,
registerPdbBytes,
});
_ems_.dotnetUpdateInternals(internals, _ems_.dotnetUpdateInternalsSubscriber);
function browserHostExportsToTable(map: BrowserHostExports): BrowserHostExportsTable {
Expand All @@ -36,6 +37,7 @@ export function dotnetInitializeModule(internals: InternalExchange): void {
map.installVfsFile,
map.loadIcuData,
map.initializeCoreCLR,
map.registerPdbBytes,
];
}
}
Expand Down
225 changes: 198 additions & 27 deletions src/native/corehost/browserhost/loader/assets.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

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

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

let throttlingPCS: PromiseCompletionSource<void> | undefined;
let currentParallelDownloads = 0;
let downloadedAssetsCount = 0;
let totalAssetsToDownload = 0;
let loadBootResourceCallback: LoadBootResourceCallback | undefined = undefined;

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

export let wasmBinaryPromise: Promise<Response> | undefined = undefined;
export const nativeModulePromiseController = createPromiseCompletionSource<EmscriptenModuleInternal>(() => {
dotnetUpdateInternals(dotnetGetInternals());
dotnetUpdateInternals(dotnetInternals);
});

export async function loadJSModule(asset: JsAsset): Promise<JsModuleExports> {
const assetInternal = asset as AssetEntryInternal;
if (assetInternal.name && !asset.resolvedUrl) {
asset.resolvedUrl = locateFile(assetInternal.name, true);
}
assetInternal.behavior = "js-module-dotnet";
if (typeof loadBootResourceCallback === "function") {
const type = runtimeToBlazorAssetTypeMap[assetInternal.behavior];
dotnetAssert.check(type, `Unsupported asset behavior: ${assetInternal.behavior}`);
const customLoadResult = loadBootResourceCallback(type, assetInternal.name, asset.resolvedUrl!, assetInternal.integrity!, assetInternal.behavior);
dotnetAssert.check(typeof customLoadResult === "string", "loadBootResourceCallback for JS modules must return string URL");
asset.resolvedUrl = makeURLAbsoluteWithApplicationBase(customLoadResult);
}
export async function loadDotnetModule(asset: JsAsset): Promise<JsModuleExports> {
return loadJSModule(asset);
}

if (!asset.resolvedUrl) throw new Error("Invalid config, resources is not set");
return await import(/* webpackIgnore: true */ asset.resolvedUrl);
export async function loadJSModule(asset: JsAsset): Promise<any> {
let mod: JsModuleExports = asset.moduleExports;
totalAssetsToDownload++;
if (!mod) {
const assetInternal = asset as AssetEntryInternal;
if (assetInternal.name && !asset.resolvedUrl) {
asset.resolvedUrl = locateFile(assetInternal.name, true);
}
assetInternal.behavior = "js-module-dotnet";
if (typeof loadBootResourceCallback === "function") {
const type = runtimeToBlazorAssetTypeMap[assetInternal.behavior];
dotnetAssert.check(type, `Unsupported asset behavior: ${assetInternal.behavior}`);
const customLoadResult = loadBootResourceCallback(type, assetInternal.name, asset.resolvedUrl!, assetInternal.integrity!, assetInternal.behavior);
dotnetAssert.check(typeof customLoadResult === "string", "loadBootResourceCallback for JS modules must return string URL");
asset.resolvedUrl = makeURLAbsoluteWithApplicationBase(customLoadResult);
}

if (!asset.resolvedUrl) throw new Error("Invalid config, resources is not set");
mod = await import(/* webpackIgnore: true */ asset.resolvedUrl);
}
onDownloadedAsset();
return mod;
}

export function fetchWasm(asset: WasmAsset): Promise<Response> {
totalAssetsToDownload++;
const assetInternal = asset as AssetEntryInternal;
if (assetInternal.name && !asset.resolvedUrl) {
asset.resolvedUrl = locateFile(assetInternal.name);
Expand All @@ -56,10 +71,12 @@ export async function instantiateWasm(imports: WebAssembly.Imports, successCallb
const data = await res.arrayBuffer();
const module = await WebAssembly.compile(data);
const instance = await WebAssembly.instantiate(module, imports);
onDownloadedAsset();
successCallback(instance, module);
} else {
const instantiated = await WebAssembly.instantiateStreaming(wasmBinaryPromise, imports);
await checkResponseOk();
onDownloadedAsset();
successCallback(instantiated.instance, instantiated.module);
}

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

export async function fetchIcu(asset: IcuAsset): Promise<void> {
totalAssetsToDownload++;
const assetInternal = asset as AssetEntryInternal;
if (assetInternal.name && !asset.resolvedUrl) {
asset.resolvedUrl = locateFile(assetInternal.name);
}
assetInternal.behavior = "icu";
const bytes = await fetchBytes(assetInternal);
await nativeModulePromiseController.promise;
dotnetBrowserHostExports.loadIcuData(bytes);
onDownloadedAsset();
if (bytes) {
dotnetBrowserHostExports.loadIcuData(bytes);
}
}

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

dotnetBrowserHostExports.registerDllBytes(bytes, asset);
onDownloadedAsset();
if (bytes) {
dotnetBrowserHostExports.registerDllBytes(bytes, asset);
}
}

export async function fetchPdb(asset: AssemblyAsset): Promise<void> {
totalAssetsToDownload++;
const assetInternal = asset as AssetEntryInternal;
if (assetInternal.name && !asset.resolvedUrl) {
asset.resolvedUrl = locateFile(assetInternal.name);
}
assetInternal.behavior = "pdb";
assetInternal.isOptional = assetInternal.isOptional || loaderConfig.ignorePdbLoadErrors;
const bytes = await fetchBytes(assetInternal);
await nativeModulePromiseController.promise;

onDownloadedAsset();
if (bytes) {
dotnetBrowserHostExports.registerPdbBytes(bytes, asset);
}
}

export async function fetchVfs(asset: AssemblyAsset): Promise<void> {
totalAssetsToDownload++;
const assetInternal = asset as AssetEntryInternal;
if (assetInternal.name && !asset.resolvedUrl) {
asset.resolvedUrl = locateFile(assetInternal.name);
}
assetInternal.behavior = "vfs";
const bytes = await fetchBytes(assetInternal);
await nativeModulePromiseController.promise;

dotnetBrowserHostExports.installVfsFile(bytes, asset);
onDownloadedAsset();
if (bytes) {
dotnetBrowserHostExports.installVfsFile(bytes, asset);
}
}

async function fetchBytes(asset: AssetEntryInternal): Promise<Uint8Array> {
async function fetchBytes(asset: AssetEntryInternal): Promise<Uint8Array | null> {
dotnetAssert.check(asset && asset.resolvedUrl, "Bad asset.resolvedUrl");
const response = await loadResource(asset);
if (!response.ok) {
if (asset.isOptional) {
dotnetLogger.warn(`Optional resource '${asset.name}' failed to load from '${asset.resolvedUrl}'. HTTP status: ${response.status} ${response.statusText}`);
return null;
}
throw new Error(`Failed to load resource '${asset.name}' from '${asset.resolvedUrl}'. HTTP status: ${response.status} ${response.statusText}`);
}
const buffer = await response.arrayBuffer();
const buffer = await (asset.buffer || response.arrayBuffer());
return new Uint8Array(buffer);
}

async function loadResource(asset: AssetEntryInternal): Promise<Response> {
function loadResource(asset: AssetEntryInternal): Promise<Response> {
if ("dotnetwasm" === asset.behavior) {
// `response.arrayBuffer()` can't be called twice.
return loadResourceFetch(asset);
}
if (ENVIRONMENT_IS_SHELL || ENVIRONMENT_IS_NODE || asset.resolvedUrl && asset.resolvedUrl.indexOf("file://") !== -1) {
// no need to retry or throttle local file access
return loadResourceFetch(asset);
}
if (!loaderConfig.enableDownloadRetry) {
// only throttle, no retry
return loadResourceThrottle(asset);
}
// retry and throttle
return loadResourceRetry(asset);
}

const noRetryStatusCodes = new Set<number>([400, 401, 403, 404, 405, 406, 409, 410, 411, 413, 414, 415, 422, 426, 501, 505,]);
async function loadResourceRetry(asset: AssetEntryInternal): Promise<Response> {
let response: Response;
response = await loadResourceAttempt();
if (response.ok || asset.isOptional || noRetryStatusCodes.has(response.status)) {
return response;
}
if (response.status === 429) {
// Too Many Requests
await delay(100);
}
response = await loadResourceAttempt();
if (response.ok || noRetryStatusCodes.has(response.status)) {
return response;
}
await delay(100); // wait 100ms before the last retry
response = await loadResourceAttempt();
if (response.ok) {
return response;
}
throw new Error(`Failed to load resource '${asset.name}' from '${asset.resolvedUrl}' after multiple attempts. Last HTTP status: ${response.status} ${response.statusText}`);

async function loadResourceAttempt(): Promise<Response> {
let response: Response;
try {
response = await loadResourceThrottle(asset);
if (!response) {
response = {
ok: false,
status: -1,
statusText: "No response",
} as any;
}
} catch (err: any) {
response = {
ok: false,
status: -1,
statusText: err.message || "Exception during fetch",
} as any;
}
return response;
}
}

// in order to prevent net::ERR_INSUFFICIENT_RESOURCES if we start downloading too many files at same time on a device with low resources
async function loadResourceThrottle(asset: AssetEntryInternal): Promise<Response> {
while (throttlingPCS) {
await throttlingPCS.promise;
}
try {
++currentParallelDownloads;
if (currentParallelDownloads === loaderConfig.maxParallelDownloads) {
dotnetLogger.debug("Throttling further parallel downloads");
throttlingPCS = createPromiseCompletionSource<void>();
}
const responsePromise = loadResourceFetch(asset);
const response = await responsePromise;
dotnetAssert.check(response, "Bad response in loadResourceThrottle");

asset.buffer = await response.arrayBuffer();
return response;
} finally {
--currentParallelDownloads;
if (throttlingPCS && currentParallelDownloads == loaderConfig.maxParallelDownloads! - 1) {
dotnetLogger.debug("Resuming more parallel downloads");
const oldThrottlingPCS = throttlingPCS;
throttlingPCS = undefined;
oldThrottlingPCS.resolve();
}
}
}

async function loadResourceFetch(asset: AssetEntryInternal): Promise<Response> {
if (asset.buffer) {
return <Response><any>{
ok: true,
headers: {
length: 0,
get: () => null
},
url: asset.resolvedUrl,
arrayBuffer: () => Promise.resolve(asset.buffer!),
json: () => {
throw new Error("NotImplementedException");
},
text: () => {
throw new Error("NotImplementedException");
}
};
}
if (asset.pendingDownload) {
return asset.pendingDownload.response;
}
if (typeof loadBootResourceCallback === "function") {
const type = runtimeToBlazorAssetTypeMap[asset.behavior];
dotnetAssert.check(type, `Unsupported asset behavior: ${asset.behavior}`);
const customLoadResult = loadBootResourceCallback(type, asset.name, asset.resolvedUrl!, asset.integrity!, asset.behavior);
if (typeof customLoadResult === "string") {
asset.resolvedUrl = makeURLAbsoluteWithApplicationBase(customLoadResult);
} else if (typeof customLoadResult === "object") {
return customLoadResult as any;
}
}
dotnetAssert.check(asset.resolvedUrl, "Bad asset.resolvedUrl");
Expand Down Expand Up @@ -157,6 +317,17 @@ async function loadResource(asset: AssetEntryInternal): Promise<Response> {
return fetchLike(asset.resolvedUrl!, fetchOptions);
}

function onDownloadedAsset() {
++downloadedAssetsCount;
if (Module.onDownloadResourceProgress) {
Module.onDownloadResourceProgress(downloadedAssetsCount, totalAssetsToDownload);
}
}

export function verifyAllAssetsDownloaded(): void {
dotnetAssert.check(downloadedAssetsCount === totalAssetsToDownload, `Not all assets were downloaded. Downloaded ${downloadedAssetsCount} out of ${totalAssetsToDownload}`);
}

const runtimeToBlazorAssetTypeMap: { [key: string]: WebAssemblyBootResourceType | undefined } = {
"resource": "assembly",
"assembly": "assembly",
Expand Down
18 changes: 11 additions & 7 deletions src/native/corehost/browserhost/loader/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export function validateLoaderConfig(): void {


export function mergeLoaderConfig(source: Partial<LoaderConfigInternal>): void {
normalizeConfig(loaderConfig);
defaultConfig(loaderConfig);
normalizeConfig(source);
mergeConfigs(loaderConfig, source);
}
Expand Down Expand Up @@ -70,19 +70,23 @@ function mergeResources(target: Assets, source: Assets): Assets {
return Object.assign(target, source);
}


function normalizeConfig(target: LoaderConfigInternal) {
if (!target.resources) target.resources = {} as any;
normalizeResources(target.resources!);
if (!target.environmentVariables) target.environmentVariables = {};
if (!target.runtimeOptions) target.runtimeOptions = [];
function defaultConfig(target: LoaderConfigInternal) {
if (target.appendElementOnExit === undefined) target.appendElementOnExit = false;
if (target.logExitCode === undefined) target.logExitCode = false;
if (target.exitOnUnhandledError === undefined) target.exitOnUnhandledError = false;
if (target.loadAllSatelliteResources === undefined) target.loadAllSatelliteResources = false;
if (target.debugLevel === undefined) target.debugLevel = 0;
if (target.diagnosticTracing === undefined) target.diagnosticTracing = false;
if (target.virtualWorkingDirectory === undefined) target.virtualWorkingDirectory = "/";
if (target.maxParallelDownloads === undefined) target.maxParallelDownloads = 16;
normalizeConfig(target);
}

function normalizeConfig(target: LoaderConfigInternal) {
if (!target.resources) target.resources = {} as any;
normalizeResources(target.resources!);
if (!target.environmentVariables) target.environmentVariables = {};
if (!target.runtimeOptions) target.runtimeOptions = [];
}

function normalizeResources(target: Assets) {
Expand Down
Loading
Loading