From 8656c10dabcf9bb1f0c07ce2571530b443e7dcd4 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Mon, 5 Jan 2026 19:22:56 +0100 Subject: [PATCH 01/10] - throttling and re-try - fixed loadBootResourceCallback for non-JS assets - implemented onRuntimeConfigLoaded and onRuntimeReady initializer modules - implemented Module.onDownloadResourceProgress - removed support for remoteSources - simplifies emscripten API - fetchPdb and seed of registerPdbBytes - support for asset.isOptional - support for asset.buffer - support for asset.pendingDownload --- src/native/corehost/browserhost/host/host.ts | 5 + src/native/corehost/browserhost/host/index.ts | 4 +- .../corehost/browserhost/loader/assets.ts | 220 +++++++++++++++--- .../corehost/browserhost/loader/config.ts | 1 + .../corehost/browserhost/loader/dotnet.d.ts | 17 -- src/native/corehost/browserhost/loader/icu.ts | 24 +- .../corehost/browserhost/loader/polyfills.ts | 4 +- .../loader/promise-completion-source.ts | 3 + src/native/corehost/browserhost/loader/run.ts | 61 +++-- .../Common/JavaScript/cross-module/index.ts | 1 + .../Common/JavaScript/types/emscripten.ts | 9 - .../libs/Common/JavaScript/types/exchange.ts | 4 +- .../libs/Common/JavaScript/types/internal.ts | 24 +- .../Common/JavaScript/types/public-api.ts | 4 - 14 files changed, 267 insertions(+), 114 deletions(-) diff --git a/src/native/corehost/browserhost/host/host.ts b/src/native/corehost/browserhost/host/host.ts index 32c4cfd2447c97..9561faaf5ada90 100644 --- a/src/native/corehost/browserhost/host/host.ts +++ b/src/native/corehost/browserhost/host/host.ts @@ -6,6 +6,11 @@ import { } from "./cross-linked"; // ensure ambient symbols are declared const loadedAssemblies: Map = new Map(); +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export function registerPdbBytes(bytes: Uint8Array, asset: { name: string, virtualPath: string }) { + throw new Error("Not implemented"); +} + export function registerDllBytes(bytes: Uint8Array, asset: { name: string, virtualPath: string }) { const sp = Module.stackSave(); try { diff --git a/src/native/corehost/browserhost/host/index.ts b/src/native/corehost/browserhost/host/index.ts index 3eadb4cb94c3c6..780394e57f993c 100644 --- a/src/native/corehost/browserhost/host/index.ts +++ b/src/native/corehost/browserhost/host/index.ts @@ -7,7 +7,7 @@ import { } from "./cross-linked"; // ensure ambient symbols are declared 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"); @@ -27,6 +27,7 @@ export function dotnetInitializeModule(internals: InternalExchange): void { installVfsFile, loadIcuData, initializeCoreCLR, + registerPdbBytes, }); dotnetUpdateInternals(internals, dotnetUpdateInternalsSubscriber); function browserHostExportsToTable(map: BrowserHostExports): BrowserHostExportsTable { @@ -36,6 +37,7 @@ export function dotnetInitializeModule(internals: InternalExchange): void { map.installVfsFile, map.loadIcuData, map.initializeCoreCLR, + map.registerPdbBytes, ]; } } diff --git a/src/native/corehost/browserhost/loader/assets.ts b/src/native/corehost/browserhost/loader/assets.ts index 779555ec24bb22..01492f8f40c682 100644 --- a/src/native/corehost/browserhost/loader/assets.ts +++ b/src/native/corehost/browserhost/loader/assets.ts @@ -1,40 +1,57 @@ // 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 } from "./types"; +import type { JsModuleExports, JsAsset, AssemblyAsset, WasmAsset, IcuAsset, EmscriptenModuleInternal, InstantiateWasmSuccessCallback, WebAssemblyBootResourceType, AssetEntryInternal, PromiseCompletionSource } from "./types"; -import { dotnetAssert, dotnetGetInternals, dotnetBrowserHostExports, dotnetUpdateInternals } from "./cross-module"; -import { ENVIRONMENT_IS_WEB } from "./per-module"; -import { createPromiseCompletionSource } from "./promise-completion-source"; +import { dotnetAssert, dotnetLogger, dotnetGetInternals, 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 { loadBootResourceCallback } from "./host-builder"; import { loaderConfig } from "./config"; +let throttlingPCS: PromiseCompletionSource | undefined; +// in order to prevent net::ERR_INSUFFICIENT_RESOURCES if we start downloading too many files at same time +let currentParallelDownloads = 0; +let downloadedAssetsCount = 0; +let totalAssetsToDownload = 0; + export let wasmBinaryPromise: Promise | undefined = undefined; export const nativeModulePromiseController = createPromiseCompletionSource(() => { dotnetUpdateInternals(dotnetGetInternals()); }); -export async function loadJSModule(asset: JsAsset): Promise { - 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 { + 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 { + 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 { + totalAssetsToDownload++; const assetInternal = asset as AssetEntryInternal; if (assetInternal.name && !asset.resolvedUrl) { asset.resolvedUrl = locateFile(assetInternal.name); @@ -51,10 +68,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); } @@ -73,6 +92,7 @@ export async function instantiateWasm(imports: WebAssembly.Imports, successCallb } export async function fetchIcu(asset: IcuAsset): Promise { + totalAssetsToDownload++; const assetInternal = asset as AssetEntryInternal; if (assetInternal.name && !asset.resolvedUrl) { asset.resolvedUrl = locateFile(assetInternal.name); @@ -80,10 +100,14 @@ export async function fetchIcu(asset: IcuAsset): Promise { 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 { + totalAssetsToDownload++; const assetInternal = asset as AssetEntryInternal; if (assetInternal.name && !asset.resolvedUrl) { asset.resolvedUrl = locateFile(assetInternal.name); @@ -92,10 +116,31 @@ export async function fetchDll(asset: AssemblyAsset): Promise { 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 { + 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 { + totalAssetsToDownload++; const assetInternal = asset as AssetEntryInternal; if (assetInternal.name && !asset.resolvedUrl) { asset.resolvedUrl = locateFile(assetInternal.name); @@ -103,27 +148,139 @@ export async function fetchVfs(asset: AssemblyAsset): Promise { 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 { +async function fetchBytes(asset: AssetEntryInternal): Promise { 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 { +function loadResource(asset: AssetEntryInternal): Promise { + 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); +} + +async function loadResourceRetry(asset: AssetEntryInternal): Promise { + let response: Response; + response = await loadResourceAttempt(); + if (response.ok || asset.isOptional) { + return response; + } + response = await loadResourceAttempt(); + if (response.ok) { + 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 { + 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; + } +} + +async function loadResourceThrottle(asset: AssetEntryInternal): Promise { + while (throttlingPCS) { + await throttlingPCS.promise; + } + try { + ++currentParallelDownloads; + if (currentParallelDownloads == loaderConfig.maxParallelDownloads) { + dotnetLogger.debug("Throttling further parallel downloads"); + throttlingPCS = createPromiseCompletionSource(); + } + const responsePromise = loadResourceFetch(asset); + const response = await responsePromise; + dotnetAssert.check(response, "Bad response in loadResourceThrottle"); + + asset.buffer = await response.arrayBuffer(); + ++downloadedAssetsCount; + 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 { + if (asset.buffer) { + return { + 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"); @@ -152,6 +309,17 @@ async function loadResource(asset: AssetEntryInternal): Promise { 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", diff --git a/src/native/corehost/browserhost/loader/config.ts b/src/native/corehost/browserhost/loader/config.ts index d8242a9eba80ec..5b9ef808d8ebc1 100644 --- a/src/native/corehost/browserhost/loader/config.ts +++ b/src/native/corehost/browserhost/loader/config.ts @@ -83,6 +83,7 @@ function normalizeConfig(target: LoaderConfigInternal) { 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; } function normalizeResources(target: Assets) { diff --git a/src/native/corehost/browserhost/loader/dotnet.d.ts b/src/native/corehost/browserhost/loader/dotnet.d.ts index de8cd3331a3539..9e9f2fd5ed7fc9 100644 --- a/src/native/corehost/browserhost/loader/dotnet.d.ts +++ b/src/native/corehost/browserhost/loader/dotnet.d.ts @@ -42,20 +42,7 @@ interface EmscriptenModule { stackSave(): VoidPtr; stackRestore(stack: VoidPtr): void; stackAlloc(size: number): VoidPtr; - instantiateWasm?: InstantiateWasmCallBack; - preInit?: (() => any)[] | (() => any); - preRun?: (() => any)[] | (() => any); - onRuntimeInitialized?: () => any; - postRun?: (() => any)[] | (() => any); - onAbort?: { - (error: any): void; - }; - onExit?: { - (code: number): void; - }; } -type InstantiateWasmSuccessCallback = (instance: WebAssembly.Instance, module: WebAssembly.Module | undefined) => void; -type InstantiateWasmCallBack = (imports: WebAssembly.Imports, successCallback: InstantiateWasmSuccessCallback) => any; type TypedArray = Int8Array | Uint8Array | Uint8ClampedArray | Int16Array | Uint16Array | Int32Array | Uint32Array | Float32Array | Float64Array; interface DotnetHostBuilder { @@ -142,10 +129,6 @@ interface DotnetHostBuilder { runMainAndExit(): Promise; } type LoaderConfig = { - /** - * Additional search locations for assets. - */ - remoteSources?: string[]; /** * It will not fail the startup is .pdb files can't be downloaded */ diff --git a/src/native/corehost/browserhost/loader/icu.ts b/src/native/corehost/browserhost/loader/icu.ts index 6aef6afa31e86d..9a7b2472f71f45 100644 --- a/src/native/corehost/browserhost/loader/icu.ts +++ b/src/native/corehost/browserhost/loader/icu.ts @@ -1,24 +1,24 @@ -import type { LoaderConfig } from "./types"; +import { loaderConfig } from "./config"; import { GlobalizationMode } from "./types"; import { ENVIRONMENT_IS_WEB } from "./per-module"; -export function getIcuResourceName(config: LoaderConfig): string | null { - if (config.resources?.icu && config.globalizationMode != GlobalizationMode.Invariant) { - const culture = config.applicationCulture || (ENVIRONMENT_IS_WEB ? (globalThis.navigator && globalThis.navigator.languages && globalThis.navigator.languages[0]) : Intl.DateTimeFormat().resolvedOptions().locale); - if (!config.applicationCulture) { - config.applicationCulture = culture; +export function getIcuResourceName(): string | null { + if (loaderConfig.resources?.icu && loaderConfig.globalizationMode != GlobalizationMode.Invariant) { + const culture = loaderConfig.applicationCulture || (ENVIRONMENT_IS_WEB ? (globalThis.navigator && globalThis.navigator.languages && globalThis.navigator.languages[0]) : Intl.DateTimeFormat().resolvedOptions().locale); + if (!loaderConfig.applicationCulture) { + loaderConfig.applicationCulture = culture; } - const icuFiles = config.resources.icu; + const icuFiles = loaderConfig.resources.icu; let icuFile = null; - if (config.globalizationMode === GlobalizationMode.Custom) { + if (loaderConfig.globalizationMode === GlobalizationMode.Custom) { // custom ICU file is saved in the resources with fingerprinting and does not require mapping if (icuFiles.length >= 1) { return icuFiles[0].name; } - } else if (!culture || config.globalizationMode === GlobalizationMode.All) { + } else if (!culture || loaderConfig.globalizationMode === GlobalizationMode.All) { icuFile = "icudt.dat"; - } else if (config.globalizationMode === GlobalizationMode.Sharded) { + } else if (loaderConfig.globalizationMode === GlobalizationMode.Sharded) { icuFile = getShardedIcuResourceName(culture); } @@ -32,8 +32,8 @@ export function getIcuResourceName(config: LoaderConfig): string | null { } } - config.globalizationMode = GlobalizationMode.Invariant; - config.environmentVariables!["DOTNET_SYSTEM_GLOBALIZATION_INVARIANT"] = "1"; + loaderConfig.globalizationMode = GlobalizationMode.Invariant; + loaderConfig.environmentVariables!["DOTNET_SYSTEM_GLOBALIZATION_INVARIANT"] = "1"; return null; } diff --git a/src/native/corehost/browserhost/loader/polyfills.ts b/src/native/corehost/browserhost/loader/polyfills.ts index 917a1deedc1265..923ff924612a5c 100644 --- a/src/native/corehost/browserhost/loader/polyfills.ts +++ b/src/native/corehost/browserhost/loader/polyfills.ts @@ -93,7 +93,9 @@ export async function fetchLike(url: string, init?: RequestInit): Promise arrayBuffer, - json: () => JSON.parse(arrayBuffer), + json: () => { + throw new Error("NotImplementedException"); + }, text: () => { throw new Error("NotImplementedException"); } diff --git a/src/native/corehost/browserhost/loader/promise-completion-source.ts b/src/native/corehost/browserhost/loader/promise-completion-source.ts index efb9f673feb4dc..cd00fab370d099 100644 --- a/src/native/corehost/browserhost/loader/promise-completion-source.ts +++ b/src/native/corehost/browserhost/loader/promise-completion-source.ts @@ -52,3 +52,6 @@ export function isControllablePromise(promise: Promise): promise is Contro return (promise as any)[promiseCompletionSourceSymbol] !== undefined; } +export function delay(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); +} diff --git a/src/native/corehost/browserhost/loader/run.ts b/src/native/corehost/browserhost/loader/run.ts index 8aefe96e8b0b44..a2b48e21c9b8e6 100644 --- a/src/native/corehost/browserhost/loader/run.ts +++ b/src/native/corehost/browserhost/loader/run.ts @@ -8,48 +8,51 @@ import { findResources, isNodeHosted, isShellHosted, validateWasmFeatures } from import { exit, runtimeState } from "./exit"; import { createPromiseCompletionSource } from "./promise-completion-source"; import { getIcuResourceName } from "./icu"; -import { getLoaderConfig } from "./config"; -import { fetchDll, fetchIcu, fetchVfs, fetchWasm, loadJSModule, nativeModulePromiseController } from "./assets"; +import { loaderConfig } from "./config"; +import { fetchDll, fetchIcu, fetchPdb, fetchVfs, fetchWasm, loadDotnetModule, loadJSModule, nativeModulePromiseController, verifyAllAssetsDownloaded } from "./assets"; const runMainPromiseController = createPromiseCompletionSource(); -// WASM-TODO: retry logic -// WASM-TODO: throttling logic -// WASM-TODO: Module.onDownloadResourceProgress -// WASM-TODO: invokeLibraryInitializers // WASM-TODO: webCIL // WASM-TODO: downloadOnly - blazor render mode auto pre-download. Really no start. -// WASM-TODO: fail fast for missing WASM features - SIMD, EH, BigInt detection -// WASM-TODO: Module.locateFile -// WASM-TODO: loadBootResource // WASM-TODO: loadAllSatelliteResources // WASM-TODO: runtimeOptions // WASM-TODO: debugLevel // WASM-TODO: load symbolication json https://github.com/dotnet/runtime/issues/122647 + +// many things happen in parallel here, but order matters for performance! +// ideally we want to utilize network and CPU at the same time export async function createRuntime(downloadOnly: boolean): Promise { - const config = getLoaderConfig(); - if (!config.resources || !config.resources.coreAssembly || !config.resources.coreAssembly.length) throw new Error("Invalid config, resources is not set"); + if (!loaderConfig.resources || !loaderConfig.resources.coreAssembly || !loaderConfig.resources.coreAssembly.length) throw new Error("Invalid config, resources is not set"); + + await validateWasmFeatures(); if (typeof Module.onConfigLoaded === "function") { - await Module.onConfigLoaded(config); + await Module.onConfigLoaded(loaderConfig); + } + const modulesAfterConfigLoaded = await Promise.all((loaderConfig.resources.modulesAfterConfigLoaded || []).map(loadJSModule)); + for (const afterConfigLoadedModule of modulesAfterConfigLoaded) { + await afterConfigLoadedModule.onRuntimeConfigLoaded?.(loaderConfig); } - await validateWasmFeatures(); - - if (config.resources.jsModuleDiagnostics && config.resources.jsModuleDiagnostics.length > 0) { - const diagnosticsModule = await loadJSModule(config.resources.jsModuleDiagnostics[0]); + if (loaderConfig.resources.jsModuleDiagnostics && loaderConfig.resources.jsModuleDiagnostics.length > 0) { + const diagnosticsModule = await loadDotnetModule(loaderConfig.resources.jsModuleDiagnostics[0]); diagnosticsModule.dotnetInitializeModule(dotnetGetInternals()); } - const nativeModulePromise: Promise = loadJSModule(config.resources.jsModuleNative[0]); - const runtimeModulePromise: Promise = loadJSModule(config.resources.jsModuleRuntime[0]); - const wasmNativePromise: Promise = fetchWasm(config.resources.wasmNative[0]); + const nativeModulePromise: Promise = loadDotnetModule(loaderConfig.resources.jsModuleNative[0]); + const runtimeModulePromise: Promise = loadDotnetModule(loaderConfig.resources.jsModuleRuntime[0]); + const wasmNativePromise: Promise = fetchWasm(loaderConfig.resources.wasmNative[0]); + + const coreAssembliesPromise = Promise.all(loaderConfig.resources.coreAssembly.map(fetchDll)); + const coreVfsPromise = Promise.all((loaderConfig.resources.coreVfs || []).map(fetchVfs)); + const assembliesPromise = Promise.all(loaderConfig.resources.assembly.map(fetchDll)); + const vfsPromise = Promise.all((loaderConfig.resources.vfs || []).map(fetchVfs)); + const icuResourceName = getIcuResourceName(); + const icuDataPromise = icuResourceName ? Promise.all((loaderConfig.resources.icu || []).filter(asset => asset.name === icuResourceName).map(fetchIcu)) : Promise.resolve([]); - const coreAssembliesPromise = Promise.all(config.resources.coreAssembly.map(fetchDll)); - const coreVfsPromise = Promise.all((config.resources.coreVfs || []).map(fetchVfs)); - const assembliesPromise = Promise.all(config.resources.assembly.map(fetchDll)); - const vfsPromise = Promise.all((config.resources.vfs || []).map(fetchVfs)); - const icuResourceName = getIcuResourceName(config); - const icuDataPromise = icuResourceName ? Promise.all((config.resources.icu || []).filter(asset => asset.name === icuResourceName).map(fetchIcu)) : Promise.resolve([]); + const corePDBsPromise = Promise.all((loaderConfig.resources.corePdb || []).map(fetchPdb)); + const pdbsPromise = Promise.all((loaderConfig.resources.pdb || []).map(fetchPdb)); + const modulesAfterRuntimeReadyPromise = Promise.all((loaderConfig.resources.modulesAfterRuntimeReady || []).map(loadJSModule)); const nativeModule = await nativeModulePromise; const modulePromise = nativeModule.dotnetInitializeModule(dotnetGetInternals()); @@ -70,11 +73,19 @@ export async function createRuntime(downloadOnly: boolean): Promise { } await assembliesPromise; + await corePDBsPromise; + await pdbsPromise; await runtimeModuleReady; + verifyAllAssetsDownloaded(); + if (typeof Module.onDotnetReady === "function") { await Module.onDotnetReady(); } + const modulesAfterRuntimeReady = await modulesAfterRuntimeReadyPromise; + for (const afterRuntimeReadyModule of modulesAfterRuntimeReady) { + await afterRuntimeReadyModule.onRuntimeReady?.(loaderConfig); + } } export function abortStartup(reason: any): void { diff --git a/src/native/libs/Common/JavaScript/cross-module/index.ts b/src/native/libs/Common/JavaScript/cross-module/index.ts index 3c874892112764..c8d75a35d32c15 100644 --- a/src/native/libs/Common/JavaScript/cross-module/index.ts +++ b/src/native/libs/Common/JavaScript/cross-module/index.ts @@ -140,6 +140,7 @@ export function dotnetUpdateInternalsSubscriber() { installVfsFile: table[1], loadIcuData: table[2], initializeCoreCLR: table[3], + registerPdbBytes: table[4], }; Object.assign(native, nativeLocal); } diff --git a/src/native/libs/Common/JavaScript/types/emscripten.ts b/src/native/libs/Common/JavaScript/types/emscripten.ts index e906165a35b7a4..9ef24f09274cb2 100644 --- a/src/native/libs/Common/JavaScript/types/emscripten.ts +++ b/src/native/libs/Common/JavaScript/types/emscripten.ts @@ -51,15 +51,6 @@ export interface EmscriptenModule { stackSave(): VoidPtr; stackRestore(stack: VoidPtr): void; stackAlloc(size: number): VoidPtr; - - - instantiateWasm?: InstantiateWasmCallBack; - preInit?: (() => any)[] | (() => any); - preRun?: (() => any)[] | (() => any); - onRuntimeInitialized?: () => any; - postRun?: (() => any)[] | (() => any); - onAbort?: { (error: any): void }; - onExit?: { (code: number): void }; } export type InstantiateWasmSuccessCallback = (instance: WebAssembly.Instance, module: WebAssembly.Module | undefined) => void; diff --git a/src/native/libs/Common/JavaScript/types/exchange.ts b/src/native/libs/Common/JavaScript/types/exchange.ts index b65e8264a94c72..bce41834bbec41 100644 --- a/src/native/libs/Common/JavaScript/types/exchange.ts +++ b/src/native/libs/Common/JavaScript/types/exchange.ts @@ -5,7 +5,7 @@ import type { check, error, info, warn, debug } from "../../../../corehost/brows import type { resolveRunMainPromise, rejectRunMainPromise, getRunMainPromise, abortStartup } from "../../../../corehost/browserhost/loader/run"; import type { addOnExitListener, isExited, isRuntimeRunning, quitNow } from "../../../../corehost/browserhost/loader/exit"; -import type { installVfsFile, registerDllBytes, loadIcuData, initializeCoreCLR } from "../../../../corehost/browserhost/host/host"; +import type { installVfsFile, registerDllBytes, loadIcuData, initializeCoreCLR, registerPdbBytes } from "../../../../corehost/browserhost/host/host"; import type { createPromiseCompletionSource, getPromiseCompletionSource, isControllablePromise } from "../../../../corehost/browserhost/loader/promise-completion-source"; import type { isSharedArrayBuffer, zeroRegion } from "../../../System.Native.Browser/utils/memory"; @@ -69,6 +69,7 @@ export type BrowserHostExports = { installVfsFile: typeof installVfsFile loadIcuData: typeof loadIcuData initializeCoreCLR: typeof initializeCoreCLR + registerPdbBytes: typeof registerPdbBytes } export type BrowserHostExportsTable = [ @@ -76,6 +77,7 @@ export type BrowserHostExportsTable = [ typeof installVfsFile, typeof loadIcuData, typeof initializeCoreCLR, + typeof registerPdbBytes, ] export type InteropJavaScriptExports = { diff --git a/src/native/libs/Common/JavaScript/types/internal.ts b/src/native/libs/Common/JavaScript/types/internal.ts index 368544295291fc..452f9dff444d92 100644 --- a/src/native/libs/Common/JavaScript/types/internal.ts +++ b/src/native/libs/Common/JavaScript/types/internal.ts @@ -2,8 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. import type { DotnetModuleConfig, RuntimeAPI, AssetEntry, LoaderConfig } from "./public-api"; -import type { EmscriptenModule, ManagedPointer, NativePointer, VoidPtr } from "./emscripten"; -import { InteropJavaScriptExportsTable as InteropJavaScriptExportsTable, LoaderExportsTable, BrowserHostExportsTable, RuntimeExportsTable, NativeBrowserExportsTable, BrowserUtilsExportsTable, DiagnosticsExportsTable } from "./exchange"; +import type { EmscriptenModule, InstantiateWasmCallBack, ManagedPointer, NativePointer, VoidPtr } from "./emscripten"; +import { InteropJavaScriptExportsTable, LoaderExportsTable, BrowserHostExportsTable, RuntimeExportsTable, NativeBrowserExportsTable, BrowserUtilsExportsTable, DiagnosticsExportsTable } from "./exchange"; export type GCHandle = { __brand: "GCHandle" @@ -37,7 +37,7 @@ export type EmscriptenInternals = { updateMemoryViews: () => void, }; -export declare interface EmscriptenModuleInternal extends EmscriptenModule { +export type EmscriptenModuleInternal = EmscriptenModule & DotnetModuleConfig & { HEAP8: Int8Array, HEAP16: Int16Array; HEAP32: Int32Array; @@ -48,24 +48,12 @@ export declare interface EmscriptenModuleInternal extends EmscriptenModule { HEAPF32: Float32Array; HEAPF64: Float64Array; - locateFile?: (path: string, prefix?: string) => string; - mainScriptUrlOrBlob?: string; - ENVIRONMENT_IS_PTHREAD?: boolean; FS: any; - wasmModule: WebAssembly.Instance | null; - ready: Promise; - wasmExports: any; - getWasmTableEntry(index: number): any; - removeRunDependency(id: string): void; - addRunDependency(id: string): void; - safeSetTimeout(func: Function, timeout: number): number; runtimeKeepalivePush(): void; runtimeKeepalivePop(): void; - maybeExit(): void; - print(message: string): void; - printErr(message: string): void; - abort(reason: any): void; - exitJS(status: number, implicit?: boolean | number): void; + instantiateWasm?: InstantiateWasmCallBack; + onAbort?: (reason: any, extraJson?: string) => void; + onExit?: (code: number) => void; } export interface AssetEntryInternal extends AssetEntry { diff --git a/src/native/libs/Common/JavaScript/types/public-api.ts b/src/native/libs/Common/JavaScript/types/public-api.ts index 1b9cda791af350..182eb25cce764a 100644 --- a/src/native/libs/Common/JavaScript/types/public-api.ts +++ b/src/native/libs/Common/JavaScript/types/public-api.ts @@ -91,10 +91,6 @@ export interface DotnetHostBuilder { runMainAndExit(): Promise; } export type LoaderConfig = { - /** - * Additional search locations for assets. - */ - remoteSources?: string[]; /** * It will not fail the startup is .pdb files can't be downloaded */ From a75dc53a406c8b6c5413e33008c6a6c1847a65f4 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Tue, 6 Jan 2026 13:28:21 +0100 Subject: [PATCH 02/10] fix --- src/native/corehost/browserhost/loader/assets.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/native/corehost/browserhost/loader/assets.ts b/src/native/corehost/browserhost/loader/assets.ts index fe22fe4ca6edc8..36147e34e2ad5b 100644 --- a/src/native/corehost/browserhost/loader/assets.ts +++ b/src/native/corehost/browserhost/loader/assets.ts @@ -243,7 +243,6 @@ async function loadResourceThrottle(asset: AssetEntryInternal): Promise Date: Tue, 6 Jan 2026 16:32:52 +0100 Subject: [PATCH 03/10] Update src/native/corehost/browserhost/loader/assets.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/native/corehost/browserhost/loader/assets.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/native/corehost/browserhost/loader/assets.ts b/src/native/corehost/browserhost/loader/assets.ts index 36147e34e2ad5b..0870252e6ea9c7 100644 --- a/src/native/corehost/browserhost/loader/assets.ts +++ b/src/native/corehost/browserhost/loader/assets.ts @@ -132,7 +132,7 @@ export async function fetchPdb(asset: AssemblyAsset): Promise { asset.resolvedUrl = locateFile(assetInternal.name); } assetInternal.behavior = "pdb"; - assetInternal.isOptional == assetInternal.isOptional || loaderConfig.ignorePdbLoadErrors; + assetInternal.isOptional = assetInternal.isOptional || loaderConfig.ignorePdbLoadErrors; const bytes = await fetchBytes(assetInternal); await nativeModulePromiseController.promise; From 2515ee6478e5ce265c9c602343873caffb334e2d Mon Sep 17 00:00:00 2001 From: Pavel Savara Date: Tue, 6 Jan 2026 16:33:21 +0100 Subject: [PATCH 04/10] Update src/native/corehost/browserhost/loader/assets.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/native/corehost/browserhost/loader/assets.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/native/corehost/browserhost/loader/assets.ts b/src/native/corehost/browserhost/loader/assets.ts index 0870252e6ea9c7..42c78ca96a195d 100644 --- a/src/native/corehost/browserhost/loader/assets.ts +++ b/src/native/corehost/browserhost/loader/assets.ts @@ -176,7 +176,7 @@ function loadResource(asset: AssetEntryInternal): Promise { // `response.arrayBuffer()` can't be called twice. return loadResourceFetch(asset); } - if (ENVIRONMENT_IS_SHELL || ENVIRONMENT_IS_NODE || asset.resolvedUrl && asset.resolvedUrl.indexOf("file://") != -1) { + 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); } From f143effe54bba7acac9518b097a1621badcd125f Mon Sep 17 00:00:00 2001 From: Pavel Savara Date: Tue, 6 Jan 2026 16:33:44 +0100 Subject: [PATCH 05/10] Update src/native/corehost/browserhost/loader/icu.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/native/corehost/browserhost/loader/icu.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/native/corehost/browserhost/loader/icu.ts b/src/native/corehost/browserhost/loader/icu.ts index 9a7b2472f71f45..eef1928425a306 100644 --- a/src/native/corehost/browserhost/loader/icu.ts +++ b/src/native/corehost/browserhost/loader/icu.ts @@ -3,7 +3,7 @@ import { GlobalizationMode } from "./types"; import { ENVIRONMENT_IS_WEB } from "./per-module"; export function getIcuResourceName(): string | null { - if (loaderConfig.resources?.icu && loaderConfig.globalizationMode != GlobalizationMode.Invariant) { + if (loaderConfig.resources?.icu && loaderConfig.globalizationMode !== GlobalizationMode.Invariant) { const culture = loaderConfig.applicationCulture || (ENVIRONMENT_IS_WEB ? (globalThis.navigator && globalThis.navigator.languages && globalThis.navigator.languages[0]) : Intl.DateTimeFormat().resolvedOptions().locale); if (!loaderConfig.applicationCulture) { loaderConfig.applicationCulture = culture; From e3669e6987d9623757e6f6c41e358decc2c12a57 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Tue, 6 Jan 2026 16:40:28 +0100 Subject: [PATCH 06/10] - fix defaults - fix WASM EXIT prefix --- .../corehost/browserhost/loader/config.ts | 17 ++++++++++------- .../System.Native.Browser/diagnostics/exit.ts | 3 ++- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/native/corehost/browserhost/loader/config.ts b/src/native/corehost/browserhost/loader/config.ts index 5b9ef808d8ebc1..5a8421a1b1db6c 100644 --- a/src/native/corehost/browserhost/loader/config.ts +++ b/src/native/corehost/browserhost/loader/config.ts @@ -20,7 +20,7 @@ export function validateLoaderConfig(): void { export function mergeLoaderConfig(source: Partial): void { - normalizeConfig(loaderConfig); + defaultConfig(loaderConfig); normalizeConfig(source); mergeConfigs(loaderConfig, source); } @@ -70,12 +70,7 @@ 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; @@ -84,6 +79,14 @@ function normalizeConfig(target: LoaderConfigInternal) { 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) { diff --git a/src/native/libs/System.Native.Browser/diagnostics/exit.ts b/src/native/libs/System.Native.Browser/diagnostics/exit.ts index 0bc4a91702d18a..4a08df72b4e1c8 100644 --- a/src/native/libs/System.Native.Browser/diagnostics/exit.ts +++ b/src/native/libs/System.Native.Browser/diagnostics/exit.ts @@ -84,7 +84,8 @@ function logExitCode(exitCode: number): void { if (config.forwardConsole) { teardownProxyConsole(message); } else if (message) { - dotnetLogger.info(message); + // eslint-disable-next-line no-console + console.log(message); } } From e76d623fac4df8f90f385281e6328e2c8ce4032c Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Tue, 6 Jan 2026 16:44:49 +0100 Subject: [PATCH 07/10] feedback --- src/native/corehost/browserhost/host/host.ts | 1 + src/native/corehost/browserhost/loader/assets.ts | 2 +- src/native/corehost/browserhost/loader/exit.ts | 4 ++-- src/native/corehost/browserhost/loader/host-builder.ts | 2 +- src/native/libs/Common/JavaScript/per-module/index.ts | 6 +++--- .../libs/System.Native.Browser/diagnostics/console-proxy.ts | 2 +- src/native/libs/System.Native.Browser/diagnostics/exit.ts | 2 +- 7 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/native/corehost/browserhost/host/host.ts b/src/native/corehost/browserhost/host/host.ts index 945a50c1ecc809..eca77c019f8d40 100644 --- a/src/native/corehost/browserhost/host/host.ts +++ b/src/native/corehost/browserhost/host/host.ts @@ -8,6 +8,7 @@ const loadedAssemblies: Map = 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 throw new Error("Not implemented"); } diff --git a/src/native/corehost/browserhost/loader/assets.ts b/src/native/corehost/browserhost/loader/assets.ts index 42c78ca96a195d..29f79c7f9a19f1 100644 --- a/src/native/corehost/browserhost/loader/assets.ts +++ b/src/native/corehost/browserhost/loader/assets.ts @@ -234,7 +234,7 @@ async function loadResourceThrottle(asset: AssetEntryInternal): Promise(); } diff --git a/src/native/corehost/browserhost/loader/exit.ts b/src/native/corehost/browserhost/loader/exit.ts index 6abf1bae2044ea..fdaec1ddb0429e 100644 --- a/src/native/corehost/browserhost/loader/exit.ts +++ b/src/native/corehost/browserhost/loader/exit.ts @@ -34,10 +34,10 @@ export function registerExit() { } function unregisterExit() { - if (Module.onAbort == onEmAbort) { + if (Module.onAbort === onEmAbort) { Module.onAbort = runtimeState.originalOnAbort; } - if (Module.onExit == onEmExit) { + if (Module.onExit === onEmExit) { Module.onExit = runtimeState.originalOnExit; } } diff --git a/src/native/corehost/browserhost/loader/host-builder.ts b/src/native/corehost/browserhost/loader/host-builder.ts index 062de202cda9fe..940be6fc79c202 100644 --- a/src/native/corehost/browserhost/loader/host-builder.ts +++ b/src/native/corehost/browserhost/loader/host-builder.ts @@ -68,7 +68,7 @@ export class HostBuilder implements DotnetHostBuilder { throw new Error("Missing window to the query parameters from"); } - if (typeof globalThis.URLSearchParams == "undefined") { + if (typeof globalThis.URLSearchParams === "undefined") { throw new Error("URLSearchParams is supported"); } diff --git a/src/native/libs/Common/JavaScript/per-module/index.ts b/src/native/libs/Common/JavaScript/per-module/index.ts index 585114e9875d90..648a4667d4aecf 100644 --- a/src/native/libs/Common/JavaScript/per-module/index.ts +++ b/src/native/libs/Common/JavaScript/per-module/index.ts @@ -3,11 +3,11 @@ import type { VoidPtr, CharPtr, NativePointer } from "../types"; -export const ENVIRONMENT_IS_NODE = typeof process == "object" && typeof process.versions == "object" && typeof process.versions.node == "string"; -export const ENVIRONMENT_IS_WEB_WORKER = typeof importScripts == "function"; +export const ENVIRONMENT_IS_NODE = typeof process === "object" && typeof process.versions === "object" && typeof process.versions.node === "string"; +export const ENVIRONMENT_IS_WEB_WORKER = typeof importScripts === "function"; export const ENVIRONMENT_IS_SIDECAR = ENVIRONMENT_IS_WEB_WORKER && typeof (globalThis as any).dotnetSidecar !== "undefined"; // sidecar is emscripten main running in a web worker export const ENVIRONMENT_IS_WORKER = ENVIRONMENT_IS_WEB_WORKER && !ENVIRONMENT_IS_SIDECAR; // we redefine what ENVIRONMENT_IS_WORKER, we replace it in emscripten internals, so that sidecar works -export const ENVIRONMENT_IS_WEB = typeof window == "object" || (ENVIRONMENT_IS_WEB_WORKER && !ENVIRONMENT_IS_NODE); +export const ENVIRONMENT_IS_WEB = typeof window === "object" || (ENVIRONMENT_IS_WEB_WORKER && !ENVIRONMENT_IS_NODE); export const ENVIRONMENT_IS_SHELL = !ENVIRONMENT_IS_WEB && !ENVIRONMENT_IS_NODE; export const VoidPtrNull: VoidPtr = 0; export const CharPtrNull: CharPtr = 0; diff --git a/src/native/libs/System.Native.Browser/diagnostics/console-proxy.ts b/src/native/libs/System.Native.Browser/diagnostics/console-proxy.ts index 21629a75267ba7..8b67859b175088 100644 --- a/src/native/libs/System.Native.Browser/diagnostics/console-proxy.ts +++ b/src/native/libs/System.Native.Browser/diagnostics/console-proxy.ts @@ -39,7 +39,7 @@ export function teardownProxyConsole(message?: string) { if (message && originalConsoleMethods) { originalConsoleMethods.log(message); } - } else if (consoleWebSocket.bufferedAmount == 0 || counter == 0) { + } else if (consoleWebSocket.bufferedAmount === 0 || counter === 0) { if (message) { // tell xharness WasmTestMessagesProcessor we are done. // note this sends last few bytes into the same WS diff --git a/src/native/libs/System.Native.Browser/diagnostics/exit.ts b/src/native/libs/System.Native.Browser/diagnostics/exit.ts index 4a08df72b4e1c8..86ede40ece8497 100644 --- a/src/native/libs/System.Native.Browser/diagnostics/exit.ts +++ b/src/native/libs/System.Native.Browser/diagnostics/exit.ts @@ -53,7 +53,7 @@ function onExit(exitCode: number, reason: any, silent: boolean): boolean { function logExitReason(exit_code: number, reason: any) { if (exit_code !== 0 && reason) { const exitStatus = isExitStatus(reason); - if (typeof reason == "string") { + if (typeof reason === "string") { dotnetLogger.error(reason); } else { if (reason.stack === undefined && !exitStatus) { From dcf45eba587601e326d9fd7764ca029f52324d81 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Tue, 6 Jan 2026 17:02:59 +0100 Subject: [PATCH 08/10] fix --- src/native/corehost/browserhost/host/host.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/native/corehost/browserhost/host/host.ts b/src/native/corehost/browserhost/host/host.ts index eca77c019f8d40..03ffe7a91eef76 100644 --- a/src/native/corehost/browserhost/host/host.ts +++ b/src/native/corehost/browserhost/host/host.ts @@ -9,7 +9,6 @@ const loadedAssemblies: Map = 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 - throw new Error("Not implemented"); } export function registerDllBytes(bytes: Uint8Array, asset: { name: string, virtualPath: string }) { From 75820c72fe619d72cc03bfb7764ae57d5502cf62 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 7 Jan 2026 17:31:22 +0100 Subject: [PATCH 09/10] - don't retry 404 - cleanup dotnetGetInternals --- src/native/corehost/browserhost/loader/assets.ts | 13 +++++++++---- src/native/corehost/browserhost/loader/run.ts | 8 ++++---- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/native/corehost/browserhost/loader/assets.ts b/src/native/corehost/browserhost/loader/assets.ts index 29f79c7f9a19f1..169879a4d365fd 100644 --- a/src/native/corehost/browserhost/loader/assets.ts +++ b/src/native/corehost/browserhost/loader/assets.ts @@ -3,7 +3,7 @@ import type { JsModuleExports, JsAsset, AssemblyAsset, WasmAsset, IcuAsset, EmscriptenModuleInternal, InstantiateWasmSuccessCallback, WebAssemblyBootResourceType, AssetEntryInternal, PromiseCompletionSource, LoadBootResourceCallback } from "./types"; -import { dotnetAssert, dotnetLogger, dotnetGetInternals, dotnetBrowserHostExports, dotnetUpdateInternals, Module } from "./cross-module"; +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"; @@ -22,7 +22,7 @@ export function setLoadBootResourceCallback(callback: LoadBootResourceCallback | export let wasmBinaryPromise: Promise | undefined = undefined; export const nativeModulePromiseController = createPromiseCompletionSource(() => { - dotnetUpdateInternals(dotnetGetInternals()); + dotnetUpdateInternals(dotnetInternals); }); export async function loadDotnetModule(asset: JsAsset): Promise { @@ -188,14 +188,19 @@ function loadResource(asset: AssetEntryInternal): Promise { return loadResourceRetry(asset); } +const noRetryStatusCodes = new Set([400, 401, 403, 404, 405, 406, 409, 410, 411, 413, 414, 415, 422, 426, 501, 505,]); async function loadResourceRetry(asset: AssetEntryInternal): Promise { let response: Response; response = await loadResourceAttempt(); - if (response.ok || asset.isOptional) { + 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) { + if (response.ok || noRetryStatusCodes.has(response.status)) { return response; } await delay(100); // wait 100ms before the last retry diff --git a/src/native/corehost/browserhost/loader/run.ts b/src/native/corehost/browserhost/loader/run.ts index a2b48e21c9b8e6..bfe043e648ba04 100644 --- a/src/native/corehost/browserhost/loader/run.ts +++ b/src/native/corehost/browserhost/loader/run.ts @@ -3,7 +3,7 @@ import type { DotnetHostBuilder, JsModuleExports, EmscriptenModuleInternal } from "./types"; -import { dotnetAssert, dotnetGetInternals, dotnetBrowserHostExports, Module } from "./cross-module"; +import { dotnetAssert, dotnetInternals, dotnetBrowserHostExports, Module } from "./cross-module"; import { findResources, isNodeHosted, isShellHosted, validateWasmFeatures } from "./bootstrap"; import { exit, runtimeState } from "./exit"; import { createPromiseCompletionSource } from "./promise-completion-source"; @@ -37,7 +37,7 @@ export async function createRuntime(downloadOnly: boolean): Promise { if (loaderConfig.resources.jsModuleDiagnostics && loaderConfig.resources.jsModuleDiagnostics.length > 0) { const diagnosticsModule = await loadDotnetModule(loaderConfig.resources.jsModuleDiagnostics[0]); - diagnosticsModule.dotnetInitializeModule(dotnetGetInternals()); + diagnosticsModule.dotnetInitializeModule(dotnetInternals); } const nativeModulePromise: Promise = loadDotnetModule(loaderConfig.resources.jsModuleNative[0]); const runtimeModulePromise: Promise = loadDotnetModule(loaderConfig.resources.jsModuleRuntime[0]); @@ -55,11 +55,11 @@ export async function createRuntime(downloadOnly: boolean): Promise { const modulesAfterRuntimeReadyPromise = Promise.all((loaderConfig.resources.modulesAfterRuntimeReady || []).map(loadJSModule)); const nativeModule = await nativeModulePromise; - const modulePromise = nativeModule.dotnetInitializeModule(dotnetGetInternals()); + const modulePromise = nativeModule.dotnetInitializeModule(dotnetInternals); nativeModulePromiseController.propagateFrom(modulePromise); const runtimeModule = await runtimeModulePromise; - const runtimeModuleReady = runtimeModule.dotnetInitializeModule(dotnetGetInternals()); + const runtimeModuleReady = runtimeModule.dotnetInitializeModule(dotnetInternals); await nativeModulePromiseController.promise; await coreAssembliesPromise; From 3ba6299f87a6bf753fdd0aa290e3350101f7d9e0 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 7 Jan 2026 18:49:14 +0100 Subject: [PATCH 10/10] - diagnosticTracing reduce noise - rename loaderConfig --- .../browserhost/loader/host-builder.ts | 8 +++----- .../corehost/browserhost/loader/logging.ts | 5 +++++ .../diagnostics/console-proxy.ts | 4 ++-- .../System.Native.Browser/diagnostics/exit.ts | 20 +++++++++---------- 4 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/native/corehost/browserhost/loader/host-builder.ts b/src/native/corehost/browserhost/loader/host-builder.ts index 940be6fc79c202..2bc210965974a1 100644 --- a/src/native/corehost/browserhost/loader/host-builder.ts +++ b/src/native/corehost/browserhost/loader/host-builder.ts @@ -4,7 +4,7 @@ import type { DotnetHostBuilder, LoaderConfig, RuntimeAPI, LoadBootResourceCallback, DotnetModuleConfig } from "./types"; import { Module, dotnetApi } from "./cross-module"; -import { getLoaderConfig, mergeLoaderConfig, validateLoaderConfig } from "./config"; +import { loaderConfig, mergeLoaderConfig, validateLoaderConfig } from "./config"; import { createRuntime } from "./run"; import { exit } from "./exit"; import { setLoadBootResourceCallback } from "./assets"; @@ -134,8 +134,7 @@ export class HostBuilder implements DotnetHostBuilder { await this.create(); } validateLoaderConfig(); - const config = getLoaderConfig(); - return this.dotnetApi!.runMain(config.mainAssemblyName, applicationArguments); + return this.dotnetApi!.runMain(loaderConfig.mainAssemblyName, applicationArguments); } catch (err) { exit(1, err); throw err; @@ -148,8 +147,7 @@ export class HostBuilder implements DotnetHostBuilder { await this.create(); } validateLoaderConfig(); - const config = getLoaderConfig(); - return this.dotnetApi!.runMainAndExit(config.mainAssemblyName, applicationArguments); + return this.dotnetApi!.runMainAndExit(loaderConfig.mainAssemblyName, applicationArguments); } catch (err) { exit(1, err); throw err; diff --git a/src/native/corehost/browserhost/loader/logging.ts b/src/native/corehost/browserhost/loader/logging.ts index ee20d8e6008f06..1f8530fa870fc1 100644 --- a/src/native/corehost/browserhost/loader/logging.ts +++ b/src/native/corehost/browserhost/loader/logging.ts @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +import { loaderConfig } from "./config"; + export function check(condition: unknown, message: string): asserts condition { if (!condition) { throw new Error(`dotnetAssert failed: ${message}`); @@ -22,6 +24,9 @@ export function fastCheck(condition: unknown, messageFactory: (() => string)): a const prefix = "DOTNET: "; export function debug(msg: string | (() => string), ...data: any) { + if (!loaderConfig.diagnosticTracing) { + return; + } if (typeof msg === "function") { msg = msg(); } diff --git a/src/native/libs/System.Native.Browser/diagnostics/console-proxy.ts b/src/native/libs/System.Native.Browser/diagnostics/console-proxy.ts index 8b67859b175088..5626acff589999 100644 --- a/src/native/libs/System.Native.Browser/diagnostics/console-proxy.ts +++ b/src/native/libs/System.Native.Browser/diagnostics/console-proxy.ts @@ -11,8 +11,8 @@ const methods = ["log", "debug", "info", "warn", "error", "trace"]; let originalConsoleMethods: { [key: string]: any } = {}; export function installLoggingProxy() { - const config = dotnetApi.getConfig() as LoaderConfigInternal; - if (ENVIRONMENT_IS_WEB && config.forwardConsole && typeof globalThis.WebSocket != "undefined") { + const loaderConfig = dotnetApi.getConfig() as LoaderConfigInternal; + if (ENVIRONMENT_IS_WEB && loaderConfig.forwardConsole && typeof globalThis.WebSocket != "undefined") { setupProxyConsole(globalThis.console, globalThis.location.origin); } } diff --git a/src/native/libs/System.Native.Browser/diagnostics/exit.ts b/src/native/libs/System.Native.Browser/diagnostics/exit.ts index 86ede40ece8497..d85697f973f626 100644 --- a/src/native/libs/System.Native.Browser/diagnostics/exit.ts +++ b/src/native/libs/System.Native.Browser/diagnostics/exit.ts @@ -7,13 +7,13 @@ import { ENVIRONMENT_IS_NODE, ENVIRONMENT_IS_WEB } from "./per-module"; import { teardownProxyConsole } from "./console-proxy"; import { symbolicateStackTrace } from "./symbolicate"; -let config: LoaderConfigInternal = null as any; +let loaderConfig: LoaderConfigInternal = null as any; export function registerExit() { if (!dotnetApi || !dotnetApi.getConfig || !dotnetLoaderExports) { return; } - config = dotnetApi.getConfig() as LoaderConfigInternal; - if (!config) { + loaderConfig = dotnetApi.getConfig() as LoaderConfigInternal; + if (!loaderConfig) { return; } installUnhandledErrorHandler(); @@ -22,21 +22,21 @@ export function registerExit() { } function onExit(exitCode: number, reason: any, silent: boolean): boolean { - if (!config) { + if (!loaderConfig) { return true; } uninstallUnhandledErrorHandler(); - if (config.logExitCode) { + if (loaderConfig.logExitCode) { if (!silent) { logExitReason(exitCode, reason); } logExitCode(exitCode); } - if (ENVIRONMENT_IS_WEB && config.appendElementOnExit) { + if (ENVIRONMENT_IS_WEB && loaderConfig.appendElementOnExit) { appendElementOnExit(exitCode); } - if (ENVIRONMENT_IS_NODE && config.asyncFlushOnExit && exitCode === 0) { + if (ENVIRONMENT_IS_NODE && loaderConfig.asyncFlushOnExit && exitCode === 0) { // this would NOT call Node's exit() immediately, it's a hanging promise (async function flush() { try { @@ -78,10 +78,10 @@ function isExitStatus(reason: any): boolean { } function logExitCode(exitCode: number): void { - const message = config.logExitCode + const message = loaderConfig.logExitCode ? "WASM EXIT " + exitCode : undefined; - if (config.forwardConsole) { + if (loaderConfig.forwardConsole) { teardownProxyConsole(message); } else if (message) { // eslint-disable-next-line no-console @@ -101,7 +101,7 @@ function appendElementOnExit(exitCode: number): void { function installUnhandledErrorHandler() { // it seems that emscripten already does the right thing for NodeJs and that there is no good solution for V8 shell. - if (ENVIRONMENT_IS_WEB && config.exitOnUnhandledError) { + if (ENVIRONMENT_IS_WEB && loaderConfig.exitOnUnhandledError) { globalThis.addEventListener("unhandledrejection", unhandledRejectionHandler); globalThis.addEventListener("error", errorHandler); }