From: Pavel Savara Date: Tue, 8 Aug 2023 12:00:57 +0000 (+0200) Subject: [browser] app start benchmark fix, loadBootResource fix (#89857) X-Git-Tag: accepted/tizen/unified/riscv/20231226.055536~454 X-Git-Url: http://review.tizen.org/git/?a=commitdiff_plain;h=749b2d2f70d8d60834fd8628ed70c694a0a45403;p=platform%2Fupstream%2Fdotnet%2Fruntime.git [browser] app start benchmark fix, loadBootResource fix (#89857) Co-authored-by: Ankit Jain --- diff --git a/src/mono/sample/wasm/browser-advanced/Wasm.Advanced.Sample.csproj b/src/mono/sample/wasm/browser-advanced/Wasm.Advanced.Sample.csproj index fee0941..a16c120 100644 --- a/src/mono/sample/wasm/browser-advanced/Wasm.Advanced.Sample.csproj +++ b/src/mono/sample/wasm/browser-advanced/Wasm.Advanced.Sample.csproj @@ -21,6 +21,7 @@ + diff --git a/src/mono/sample/wasm/browser-advanced/advanced-sample.lib.module.js b/src/mono/sample/wasm/browser-advanced/advanced-sample.lib.module.js new file mode 100644 index 0000000..be57002 --- /dev/null +++ b/src/mono/sample/wasm/browser-advanced/advanced-sample.lib.module.js @@ -0,0 +1,10 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +export function onRuntimeConfigLoaded(config) { + console.log("advanced-sample onRuntimeConfigLoaded") +} + +export async function onRuntimeReady({ getAssemblyExports, getConfig }) { + console.log("advanced-sample onRuntimeReady") +} \ No newline at end of file diff --git a/src/mono/sample/wasm/browser-advanced/index.html b/src/mono/sample/wasm/browser-advanced/index.html index eb1032f..c8961d7 100644 --- a/src/mono/sample/wasm/browser-advanced/index.html +++ b/src/mono/sample/wasm/browser-advanced/index.html @@ -9,14 +9,14 @@ - - + + - + diff --git a/src/mono/sample/wasm/browser-advanced/main.js b/src/mono/sample/wasm/browser-advanced/main.js index 5dbbf75..7b58f77 100644 --- a/src/mono/sample/wasm/browser-advanced/main.js +++ b/src/mono/sample/wasm/browser-advanced/main.js @@ -38,6 +38,9 @@ try { // config is loaded and could be tweaked before the rest of the runtime startup sequence config.environmentVariables["MONO_LOG_LEVEL"] = "debug"; config.browserProfilerOptions = {}; + config.resources.modulesAfterConfigLoaded = { + "advanced-sample.lib.module.js": "" + } }, preInit: () => { console.log('user code Module.preInit'); }, preRun: () => { console.log('user code Module.preRun'); }, diff --git a/src/mono/sample/wasm/browser-bench/appstart-frame.html b/src/mono/sample/wasm/browser-bench/appstart-frame.html index 697f7ac..5bb75e3 100644 --- a/src/mono/sample/wasm/browser-bench/appstart-frame.html +++ b/src/mono/sample/wasm/browser-bench/appstart-frame.html @@ -9,12 +9,12 @@ - - - - + + + + - + diff --git a/src/mono/sample/wasm/simple-server/Program.cs b/src/mono/sample/wasm/simple-server/Program.cs index 64e6e10..e1a520d 100644 --- a/src/mono/sample/wasm/simple-server/Program.cs +++ b/src/mono/sample/wasm/simple-server/Program.cs @@ -6,6 +6,7 @@ using System.Diagnostics; using System.Collections.Concurrent; using System.Runtime.InteropServices; using System; +using System.Security.Cryptography; namespace HttpServer { @@ -16,11 +17,13 @@ namespace HttpServer public int Finished { get; set; } = 0; } + public sealed record FileContent(byte[] buffer, string hash); + public sealed class Program { private bool Verbose = false; private ConcurrentDictionary Sessions = new ConcurrentDictionary(); - private Dictionary cache = new Dictionary(StringComparer.OrdinalIgnoreCase); + private Dictionary cache = new(StringComparer.OrdinalIgnoreCase); public static int Main() { @@ -104,7 +107,7 @@ namespace HttpServer ReceivePostAsync(context); } - private async Task GetFileContent(string path) + private async Task GetFileContent(string path) { if (Verbose) await Console.Out.WriteLineAsync($"get content for: {path}"); @@ -124,9 +127,13 @@ namespace HttpServer if (Verbose) await Console.Out.WriteLineAsync($"adding content to cache for: {path}"); - cache[path] = content; + using HashAlgorithm hashAlgorithm = SHA256.Create(); + byte[] hash = hashAlgorithm.ComputeHash(content); + var fc = new FileContent(content, "sha256-" + Convert.ToBase64String(hash)); + + cache[path] = fc; - return content; + return fc; } private async void ReceivePostAsync(HttpListenerContext context) @@ -189,14 +196,14 @@ namespace HttpServer else if (path.StartsWith("/")) path = path.Substring(1); - byte[]? buffer; + FileContent? fc; try { - buffer = await GetFileContent(path); + fc = await GetFileContent(path); - if (buffer != null && throttleMbps > 0) + if (fc != null && throttleMbps > 0) { - double delaySeconds = (buffer.Length * 8) / (throttleMbps * 1024 * 1024); + double delaySeconds = (fc.buffer.Length * 8) / (throttleMbps * 1024 * 1024); int delayMs = (int)(delaySeconds * 1000); if (session != null) { @@ -246,12 +253,20 @@ namespace HttpServer } } } - catch (Exception) + catch (System.IO.DirectoryNotFoundException) + { + if (Verbose) + Console.WriteLine($"Not found: {path}"); + fc = null; + } + catch (Exception ex) { - buffer = null; + if (Verbose) + Console.WriteLine($"Exception: {ex}"); + fc = null; } - if (buffer != null) + if (fc != null) { string? contentType = null; if (path.EndsWith(".wasm")) @@ -270,7 +285,7 @@ namespace HttpServer { Console.WriteLine("Faking 500 " + url); context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; - await stream.WriteAsync(buffer, 0, 0).ConfigureAwait(false); + await stream.WriteAsync(fc.buffer, 0, 0).ConfigureAwait(false); await stream.FlushAsync(); context.Response.Close(); return; @@ -279,25 +294,36 @@ namespace HttpServer if (contentType != null) context.Response.ContentType = contentType; - context.Response.ContentLength64 = buffer.Length; - context.Response.AppendHeader("cache-control", "public, max-age=31536000"); + // context.Response.AppendHeader("cache-control", "public, max-age=31536000"); context.Response.AppendHeader("Cross-Origin-Embedder-Policy", "require-corp"); context.Response.AppendHeader("Cross-Origin-Opener-Policy", "same-origin"); + context.Response.AppendHeader("ETag", fc.hash); // test download re-try if (url.Query.Contains("testAbort")) { Console.WriteLine("Faking abort " + url); - await stream.WriteAsync(buffer, 0, 10).ConfigureAwait(false); + context.Response.ContentLength64 = fc.buffer.Length; + await stream.WriteAsync(fc.buffer, 0, 10).ConfigureAwait(false); await stream.FlushAsync(); await Task.Delay(100); context.Response.Abort(); return; } + var ifNoneMatch = context.Request.Headers.Get("If-None-Match"); + if (ifNoneMatch == fc.hash) + { + context.Response.StatusCode = 304; + await stream.FlushAsync(); + stream.Close(); + context.Response.Close(); + return; + } try { - await stream.WriteAsync(buffer).ConfigureAwait(false); + context.Response.ContentLength64 = fc.buffer.Length; + await stream.WriteAsync(fc.buffer).ConfigureAwait(false); } catch (Exception e) { diff --git a/src/mono/wasm/features.md b/src/mono/wasm/features.md index 74f6ba6..afdcc14 100644 --- a/src/mono/wasm/features.md +++ b/src/mono/wasm/features.md @@ -191,7 +191,7 @@ See also [fetch integrity on MDN](https://developer.mozilla.org/en-US/docs/Web/A In order to start downloading application resources as soon as possible you can add HTML elements to `` of your page similar to: ```html - + diff --git a/src/mono/wasm/runtime/dotnet.d.ts b/src/mono/wasm/runtime/dotnet.d.ts index c758978..2f8f000 100644 --- a/src/mono/wasm/runtime/dotnet.d.ts +++ b/src/mono/wasm/runtime/dotnet.d.ts @@ -130,6 +130,14 @@ type MonoConfig = { */ cacheBootResources?: boolean; /** + * Configures use of the `integrity` directive for fetching assets + */ + disableIntegrityCheck?: boolean; + /** + * Configures use of the `no-cache` directive for fetching assets + */ + disableNoCacheFetch?: boolean; + /** * Enables diagnostic log messages during startup */ diagnosticTracing?: boolean; @@ -212,18 +220,28 @@ type ResourceList = { * When returned string is not qualified with `./` or absolute URL, it will be resolved against the application base URI. */ type LoadBootResourceCallback = (type: WebAssemblyBootResourceType, name: string, defaultUri: string, integrity: string, behavior: AssetBehaviors) => string | Promise | null | undefined; -interface ResourceRequest { - name: string; - behavior: AssetBehaviors; - resolvedUrl?: string; - hash?: string | null | ""; -} interface LoadingResource { name: string; url: string; response: Promise; } -interface AssetEntry extends ResourceRequest { +interface AssetEntry { + /** + * the name of the asset, including extension. + */ + name: string; + /** + * determines how the asset will be handled once loaded + */ + behavior: AssetBehaviors; + /** + * this should be absolute url to the asset + */ + resolvedUrl?: string; + /** + * the integrity hash of the asset (if any) + */ + hash?: string | null | ""; /** * If specified, overrides the path of the asset in the virtual filesystem and similar data structures once downloaded. */ @@ -442,4 +460,4 @@ declare global { } declare const createDotnetRuntime: CreateDotnetRuntimeType; -export { AssetBehaviors, AssetEntry, CreateDotnetRuntimeType, DotnetHostBuilder, DotnetModuleConfig, EmscriptenModule, GlobalizationMode, IMemoryView, ModuleAPI, MonoConfig, ResourceRequest, RuntimeAPI, createDotnetRuntime as default, dotnet, exit }; +export { AssetBehaviors, AssetEntry, CreateDotnetRuntimeType, DotnetHostBuilder, DotnetModuleConfig, EmscriptenModule, GlobalizationMode, IMemoryView, ModuleAPI, MonoConfig, RuntimeAPI, createDotnetRuntime as default, dotnet, exit }; diff --git a/src/mono/wasm/runtime/loader/assets.ts b/src/mono/wasm/runtime/loader/assets.ts index 339ae32..b39ce6a 100644 --- a/src/mono/wasm/runtime/loader/assets.ts +++ b/src/mono/wasm/runtime/loader/assets.ts @@ -1,23 +1,27 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +import MonoWasmThreads from "consts:monoWasmThreads"; + import type { AssetEntryInternal, PromiseAndController } from "../types/internal"; -import type { AssetBehaviors, AssetEntry, LoadingResource, ResourceList, ResourceRequest, SingleAssetBehaviors as SingleAssetBehaviors, WebAssemblyBootResourceType } from "../types"; +import type { AssetBehaviors, AssetEntry, LoadingResource, ResourceList, SingleAssetBehaviors as SingleAssetBehaviors, WebAssemblyBootResourceType } from "../types"; import { ENVIRONMENT_IS_NODE, ENVIRONMENT_IS_SHELL, loaderHelpers, mono_assert, runtimeHelpers } from "./globals"; import { createPromiseController } from "./promise-controller"; import { mono_log_debug } from "./logging"; import { mono_exit } from "./exit"; -import { addCachedReponse, findCachedResponse, isCacheAvailable } from "./assetsCache"; +import { addCachedReponse, findCachedResponse } from "./assetsCache"; import { getIcuResourceName } from "./icu"; -import { mono_log_warn } from "./logging"; import { makeURLAbsoluteWithApplicationBase } from "./polyfills"; let throttlingPromise: PromiseAndController | undefined; // in order to prevent net::ERR_INSUFFICIENT_RESOURCES if we start downloading too many files at same time let parallel_count = 0; +const containedInSnapshotAssets: AssetEntryInternal[] = []; +const alwaysLoadedAssets: AssetEntryInternal[] = []; +const singleAssets: Map = new Map(); -const jsModulesAssetTypes: { +const jsRuntimeModulesAssetTypes: { [k: string]: boolean } = { "js-module-threads": true, @@ -26,6 +30,30 @@ const jsModulesAssetTypes: { "js-module-native": true, }; +const jsModulesAssetTypes: { + [k: string]: boolean +} = { + ...jsRuntimeModulesAssetTypes, + "js-module-library-initializer": true, +}; + +const singleAssetTypes: { + [k: string]: boolean +} = { + ...jsRuntimeModulesAssetTypes, + "dotnetwasm": true, + "heap": true, + "manifest": true, +}; + +// append query to asset url to prevent reusing state +const appendQueryAssetTypes: { + [k: string]: boolean +} = { + ...jsModulesAssetTypes, + "manifest": true, +}; + // don't `fetch` javaScript and wasm files const skipDownloadsByAssetTypes: { [k: string]: boolean @@ -67,45 +95,55 @@ export function shouldLoadIcuAsset(asset: AssetEntryInternal): boolean { return !(asset.behavior == "icu" && asset.name != loaderHelpers.preferredIcuAsset); } -function getSingleAssetWithResolvedUrl(resources: ResourceList | undefined, behavior: SingleAssetBehaviors): AssetEntry { - const keys = Object.keys(resources || {}); +function convert_single_asset(modulesAssets: AssetEntryInternal[], resource: ResourceList | undefined, behavior: SingleAssetBehaviors): AssetEntryInternal { + const keys = Object.keys(resource || {}); mono_assert(keys.length == 1, `Expect to have one ${behavior} asset in resources`); const name = keys[0]; + const asset = { name, - hash: resources![name], + hash: resource![name], behavior, - resolvedUrl: appendUniqueQuery(loaderHelpers.locateFile(name), behavior) }; - const customSrc = invokeLoadBootResource(asset); - if (typeof (customSrc) === "string") { - asset.resolvedUrl = makeURLAbsoluteWithApplicationBase(customSrc); - } else if (customSrc) { - mono_log_warn(`For ${behavior} resource: ${name}, custom loaders must supply a URI string.`); - // we apply a default URL + set_single_asset(asset); + + // so that we can use it on the worker too + modulesAssets.push(asset); + return asset; +} + +function set_single_asset(asset: AssetEntryInternal) { + if (singleAssetTypes[asset.behavior]) { + singleAssets.set(asset.behavior, asset); } +} +function get_single_asset(behavior: SingleAssetBehaviors): AssetEntryInternal { + mono_assert(singleAssetTypes[behavior], `Unknown single asset behavior ${behavior}`); + const asset = singleAssets.get(behavior); + mono_assert(asset, `Single asset for ${behavior} not found`); return asset; } export function resolve_single_asset_path(behavior: SingleAssetBehaviors): AssetEntryInternal { - const resources = loaderHelpers.config.resources; - mono_assert(resources, "Can't find resources in config"); - - switch (behavior) { - case "dotnetwasm": - return getSingleAssetWithResolvedUrl(resources.wasmNative, behavior); - case "js-module-threads": - return getSingleAssetWithResolvedUrl(resources.jsModuleWorker, behavior); - case "js-module-native": - return getSingleAssetWithResolvedUrl(resources.jsModuleNative, behavior); - case "js-module-runtime": - return getSingleAssetWithResolvedUrl(resources.jsModuleRuntime, behavior); - default: - throw new Error(`Unknown single asset behavior ${behavior}`); + const asset = get_single_asset(behavior); + asset.resolvedUrl = loaderHelpers.locateFile(asset.name); + + if (jsRuntimeModulesAssetTypes[asset.behavior]) { + // give loadBootResource chance to override the url for JS modules with 'dotnetjs' type + const customLoadResult = invokeLoadBootResource(asset); + if (customLoadResult) { + mono_assert(typeof customLoadResult === "string", "loadBootResource response for 'dotnetjs' type should be a URL string"); + asset.resolvedUrl = customLoadResult; + } else { + asset.resolvedUrl = appendUniqueQuery(asset.resolvedUrl, asset.behavior); + } + } else if (asset.behavior !== "dotnetwasm") { + throw new Error(`Unknown single asset behavior ${behavior}`); } + return asset; } export async function mono_download_assets(): Promise { @@ -113,12 +151,8 @@ export async function mono_download_assets(): Promise { loaderHelpers.maxParallelDownloads = loaderHelpers.config.maxParallelDownloads || loaderHelpers.maxParallelDownloads; loaderHelpers.enableDownloadRetry = loaderHelpers.config.enableDownloadRetry || loaderHelpers.enableDownloadRetry; try { - const alwaysLoadedAssets: AssetEntryInternal[] = []; - const containedInSnapshotAssets: AssetEntryInternal[] = []; const promises_of_assets: Promise[] = []; - prepareAssets(containedInSnapshotAssets, alwaysLoadedAssets); - const countAndStartDownload = (asset: AssetEntryInternal) => { if (!skipInstantiateByAssetTypes[asset.behavior] && shouldLoadIcuAsset(asset)) { loaderHelpers.expected_instantiated_assets_count++; @@ -222,13 +256,13 @@ export async function mono_download_assets(): Promise { } } -function prepareAssets(containedInSnapshotAssets: AssetEntryInternal[], alwaysLoadedAssets: AssetEntryInternal[]) { +export function prepareAssets() { const config = loaderHelpers.config; + const modulesAssets: AssetEntryInternal[] = []; // if assets exits, we will assume Net7 legacy and not process resources object if (config.assets) { - for (const a of config.assets) { - const asset: AssetEntryInternal = a; + for (const asset of config.assets) { mono_assert(typeof asset === "object", () => `asset must be object, it was ${typeof asset} : ${asset}`); mono_assert(typeof asset.behavior === "string", "asset behavior must be known string"); mono_assert(typeof asset.name === "string", "asset name must be string"); @@ -240,9 +274,22 @@ function prepareAssets(containedInSnapshotAssets: AssetEntryInternal[], alwaysLo } else { alwaysLoadedAssets.push(asset); } + set_single_asset(asset); } } else if (config.resources) { const resources = config.resources; + + mono_assert(resources.wasmNative, "resources.wasmNative must be defined"); + mono_assert(resources.jsModuleNative, "resources.jsModuleNative must be defined"); + mono_assert(resources.jsModuleRuntime, "resources.jsModuleRuntime must be defined"); + mono_assert(!MonoWasmThreads || resources.jsModuleWorker, "resources.jsModuleWorker must be defined"); + convert_single_asset(modulesAssets, resources.wasmNative, "dotnetwasm"); + convert_single_asset(modulesAssets, resources.jsModuleNative, "js-module-native"); + convert_single_asset(modulesAssets, resources.jsModuleRuntime, "js-module-runtime"); + if (MonoWasmThreads) { + convert_single_asset(modulesAssets, resources.jsModuleWorker, "js-module-threads"); + } + if (resources.assembly) { for (const name in resources.assembly) { containedInSnapshotAssets.push({ @@ -314,21 +361,34 @@ function prepareAssets(containedInSnapshotAssets: AssetEntryInternal[], alwaysLo } } + // FIXME: should we also load Net7 backward compatible `config.configs` in a same way ? if (config.appsettings) { for (let i = 0; i < config.appsettings.length; i++) { const configUrl = config.appsettings[i]; const configFileName = fileName(configUrl); if (configFileName === "appsettings.json" || configFileName === `appsettings.${config.applicationEnvironment}.json`) { alwaysLoadedAssets.push({ - name: configFileName, - resolvedUrl: appendUniqueQuery(loaderHelpers.locateFile(configUrl), "vfs"), - behavior: "vfs" + name: configUrl, + behavior: "vfs", + // TODO what should be the virtualPath ? + noCache: true, + useCredentials: true }); } + // FIXME: why are not loading all the other named files in appsettings ? https://github.com/dotnet/runtime/issues/89861 } } - config.assets = [...containedInSnapshotAssets, ...alwaysLoadedAssets]; + config.assets = [...containedInSnapshotAssets, ...alwaysLoadedAssets, ...modulesAssets]; +} + +export function prepareAssetsWorker() { + const config = loaderHelpers.config; + mono_assert(config.assets, "config.assets must be defined"); + + for (const asset of config.assets) { + set_single_asset(asset); + } } export function delay(ms: number): Promise { @@ -524,7 +584,7 @@ function resolve_path(asset: AssetEntry, sourcePrefix: string): string { export function appendUniqueQuery(attemptUrl: string, behavior: AssetBehaviors): string { // apply unique query to js modules to make the module state independent of the other runtime instances - if (loaderHelpers.modulesUniqueQuery && jsModulesAssetTypes[behavior]) { + if (loaderHelpers.modulesUniqueQuery && appendQueryAssetTypes[behavior]) { attemptUrl = attemptUrl + loaderHelpers.modulesUniqueQuery; } @@ -534,16 +594,16 @@ export function appendUniqueQuery(attemptUrl: string, behavior: AssetBehaviors): let resourcesLoaded = 0; const totalResources = new Set(); -function download_resource(request: ResourceRequest): LoadingResource { +function download_resource(asset: AssetEntryInternal): LoadingResource { try { - mono_assert(request.resolvedUrl, "Request's resolvedUrl must be set"); - const fetchResponse = download_resource_with_cache(request); - const response = { name: request.name, url: request.resolvedUrl, response: fetchResponse }; + mono_assert(asset.resolvedUrl, "Request's resolvedUrl must be set"); + const fetchResponse = download_resource_with_cache(asset); + const response = { name: asset.name, url: asset.resolvedUrl, response: fetchResponse }; - totalResources.add(request.name!); + totalResources.add(asset.name!); response.response.then(() => { - if (request.behavior == "assembly") { - loaderHelpers.loadedAssemblies.push(request.resolvedUrl!); + if (asset.behavior == "assembly") { + loaderHelpers.loadedAssemblies.push(asset.resolvedUrl!); } resourcesLoaded++; @@ -554,56 +614,57 @@ function download_resource(request: ResourceRequest): LoadingResource { } catch (err) { const response = { ok: false, - url: request.resolvedUrl, + url: asset.resolvedUrl, status: 500, statusText: "ERR29: " + err, arrayBuffer: () => { throw err; }, json: () => { throw err; } }; return { - name: request.name, url: request.resolvedUrl!, response: Promise.resolve(response) + name: asset.name, url: asset.resolvedUrl!, response: Promise.resolve(response) }; } } -async function download_resource_with_cache(request: ResourceRequest): Promise { - let response = await findCachedResponse(request); +async function download_resource_with_cache(asset: AssetEntryInternal): Promise { + let response = await findCachedResponse(asset); if (!response) { - response = await fetchResource(request); - addCachedReponse(request, response); + response = await fetchResource(asset); + addCachedReponse(asset, response); } return response; } -const credentialsIncludeAssetBehaviors: AssetBehaviors[] = ["vfs"]; // Previously only configuration - -function fetchResource(request: ResourceRequest): Promise { +function fetchResource(asset: AssetEntryInternal): Promise { // Allow developers to override how the resource is loaded - let url = request.resolvedUrl!; + let url = asset.resolvedUrl!; if (loaderHelpers.loadBootResource) { - const customLoadResult = invokeLoadBootResource(request); + const customLoadResult = invokeLoadBootResource(asset); if (customLoadResult instanceof Promise) { // They are supplying an entire custom response, so just use that return customLoadResult; } else if (typeof customLoadResult === "string") { - url = makeURLAbsoluteWithApplicationBase(customLoadResult); + url = customLoadResult; } } - const fetchOptions: RequestInit = { - cache: "no-cache" - }; - - if (credentialsIncludeAssetBehaviors.includes(request.behavior)) { + const fetchOptions: RequestInit = {}; + if (!loaderHelpers.config.disableNoCacheFetch) { + // FIXME: "no-cache" is how blazor works in Net7, but this prevents caching on HTTP level + // if we would like to get rid of our own cache and only use HTTP cache, we need to remove this + // https://github.com/dotnet/runtime/issues/74815 + fetchOptions.cache = "no-cache"; + } + if (asset.useCredentials) { // Include credentials so the server can allow download / provide user specific file fetchOptions.credentials = "include"; } else { - // Any other resource than configuration should provide integrity check - // Note that if cacheBootResources was explicitly disabled, we also bypass hash checking - // This is to give developers an easy opt-out from the entire caching/validation flow if - // there's anything they don't like about it. - fetchOptions.integrity = isCacheAvailable() ? (request.hash ?? "") : undefined; + // `disableIntegrityCheck` is to give developers an easy opt-out from the integrity check + if (!loaderHelpers.config.disableIntegrityCheck && asset.hash) { + // Any other resource than configuration should provide integrity check + fetchOptions.integrity = asset.hash; + } } return loaderHelpers.fetch_like(url, fetchOptions); @@ -623,14 +684,18 @@ const monoToBlazorAssetTypeMap: { [key: string]: WebAssemblyBootResourceType | u "js-module-threads": "dotnetjs" }; -function invokeLoadBootResource(request: ResourceRequest): string | Promise | null | undefined { +function invokeLoadBootResource(asset: AssetEntryInternal): string | Promise | null | undefined { if (loaderHelpers.loadBootResource) { - const requestHash = request.hash ?? ""; - const url = request.resolvedUrl!; + const requestHash = asset.hash ?? ""; + const url = asset.resolvedUrl!; - const resourceType = monoToBlazorAssetTypeMap[request.behavior]; + const resourceType = monoToBlazorAssetTypeMap[asset.behavior]; if (resourceType) { - return loaderHelpers.loadBootResource(resourceType, request.name, url, requestHash, request.behavior); + const customLoadResult = loaderHelpers.loadBootResource(resourceType, asset.name, url, requestHash, asset.behavior); + if (typeof customLoadResult === "string") { + return makeURLAbsoluteWithApplicationBase(customLoadResult); + } + return customLoadResult; } } diff --git a/src/mono/wasm/runtime/loader/assetsCache.ts b/src/mono/wasm/runtime/loader/assetsCache.ts index b9c44f3..0990ba2 100644 --- a/src/mono/wasm/runtime/loader/assetsCache.ts +++ b/src/mono/wasm/runtime/loader/assetsCache.ts @@ -1,19 +1,15 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -import type { AssetBehaviors, MonoConfig, ResourceRequest } from "../types"; -import { loaderHelpers } from "./globals"; +import type { MonoConfig } from "../types"; +import type { AssetEntryInternal } from "../types/internal"; +import { ENVIRONMENT_IS_WEB, loaderHelpers } from "./globals"; -const cacheSkipAssetBehaviors: AssetBehaviors[] = ["vfs"]; // Previously only configuration const usedCacheKeys: { [key: string]: boolean } = {}; const networkLoads: { [name: string]: LoadLogEntry } = {}; const cacheLoads: { [name: string]: LoadLogEntry } = {}; let cacheIfUsed: Cache | null; -export function isCacheAvailable(): boolean { - return !!cacheIfUsed; -} - export function logDownloadStatsToConsole(): void { const cacheLoadsEntries = Object.values(cacheLoads); const networkLoadsEntries = Object.values(networkLoads); @@ -24,10 +20,14 @@ export function logDownloadStatsToConsole(): void { // We have no perf stats to display, likely because caching is not in use. return; } - - const linkerDisabledWarning = loaderHelpers.config.linkerEnabled ? "%c" : "\n%cThis application was built with linking (tree shaking) disabled. Published applications will be significantly smaller if you install wasm-tools workload. See also https://aka.ms/dotnet-wasm-features"; + const useStyle = ENVIRONMENT_IS_WEB ? "%c" : ""; + const style = ENVIRONMENT_IS_WEB ? ["background: purple; color: white; padding: 1px 3px; border-radius: 3px;", + "font-weight: bold;", + "font-weight: normal;", + ] : []; + const linkerDisabledWarning = !loaderHelpers.config.linkerEnabled ? "\nThis application was built with linking (tree shaking) disabled. \nPublished applications will be significantly smaller if you install wasm-tools workload. \nSee also https://aka.ms/dotnet-wasm-features" : ""; // eslint-disable-next-line no-console - console.groupCollapsed(`%cdotnet%c Loaded ${toDataSizeString(totalResponseBytes)} resources${linkerDisabledWarning}`, "background: purple; color: white; padding: 1px 3px; border-radius: 3px;", "font-weight: bold;", "font-weight: normal;"); + console.groupCollapsed(`${useStyle}dotnet${useStyle} Loaded ${toDataSizeString(totalResponseBytes)} resources${useStyle}${linkerDisabledWarning}`, ...style); if (cacheLoadsEntries.length) { // eslint-disable-next-line no-console @@ -67,13 +67,13 @@ export async function purgeUnusedCacheEntriesAsync(): Promise { } } -export async function findCachedResponse(request: ResourceRequest): Promise { +export async function findCachedResponse(asset: AssetEntryInternal): Promise { const cache = cacheIfUsed; - if (!cache || cacheSkipAssetBehaviors.includes(request.behavior) || !request.hash || request.hash.length === 0) { + if (!cache || asset.noCache || !asset.hash || asset.hash.length === 0) { return undefined; } - const cacheKey = getCacheKey(request); + const cacheKey = getCacheKey(asset); usedCacheKeys[cacheKey] = true; let cachedResponse: Response | undefined; @@ -90,34 +90,38 @@ export async function findCachedResponse(request: ResourceRequest): Promise { + const cacheKey = getCacheKey(asset); + addToCacheAsync(cache, asset.name, cacheKey, clonedResponse); // Don't await - add to cache in background + }, 0); } -function getCacheKey(request: ResourceRequest) { - return `${request.resolvedUrl}.${request.hash}`; +function getCacheKey(asset: AssetEntryInternal) { + return `${asset.resolvedUrl}.${asset.hash}`; } -async function addToCacheAsync(cache: Cache, name: string, cacheKey: string, response: Response) { +async function addToCacheAsync(cache: Cache, name: string, cacheKey: string, clonedResponse: Response) { // We have to clone in order to put this in the cache *and* not prevent other code from // reading the original response stream. - const responseData = await response.clone().arrayBuffer(); + const responseData = await clonedResponse.arrayBuffer(); // Now is an ideal moment to capture the performance stats for the request, since it // only just completed and is most likely to still be in the buffer. However this is // only done on a 'best effort' basis. Even if we do receive an entry, some of its // properties may be blanked out if it was a CORS request. - const performanceEntry = getPerformanceEntry(response.url); + const performanceEntry = getPerformanceEntry(clonedResponse.url); const responseBytes = (performanceEntry && performanceEntry.encodedBodySize) || undefined; networkLoads[name] = { responseBytes }; @@ -125,8 +129,8 @@ async function addToCacheAsync(cache: Cache, name: string, cacheKey: string, res // We can't rely on the server sending content-length (ASP.NET Core doesn't by default) const responseToCache = new Response(responseData, { headers: { - "content-type": response.headers.get("content-type") || "", - "content-length": (responseBytes || response.headers.get("content-length") || "").toString(), + "content-type": clonedResponse.headers.get("content-type") || "", + "content-length": (responseBytes || clonedResponse.headers.get("content-length") || "").toString(), }, }); diff --git a/src/mono/wasm/runtime/loader/config.ts b/src/mono/wasm/runtime/loader/config.ts index dd24733..053d769 100644 --- a/src/mono/wasm/runtime/loader/config.ts +++ b/src/mono/wasm/runtime/loader/config.ts @@ -6,9 +6,10 @@ import type { DotnetModuleInternal, MonoConfigInternal } from "../types/internal import type { DotnetModuleConfig, MonoConfig, ResourceGroups, ResourceList } from "../types"; import { ENVIRONMENT_IS_WEB, exportedRuntimeAPI, loaderHelpers, runtimeHelpers } from "./globals"; import { mono_log_error, mono_log_debug } from "./logging"; -import { invokeLibraryInitializers } from "./libraryInitializers"; +import { importLibraryInitializers, invokeLibraryInitializers } from "./libraryInitializers"; import { mono_exit } from "./exit"; import { makeURLAbsoluteWithApplicationBase } from "./polyfills"; +import { appendUniqueQuery } from "./assets"; export function deep_merge_config(target: MonoConfigInternal, source: MonoConfigInternal): MonoConfigInternal { // no need to merge the same object @@ -227,8 +228,6 @@ export async function mono_wasm_load_config(module: DotnetModuleInternal): Promi normalizeConfig(); - await invokeLibraryInitializers("onRuntimeConfigLoaded", [loaderHelpers.config], "modulesAfterConfigLoaded"); - if (module.onConfigLoaded) { try { await module.onConfigLoaded(loaderHelpers.config, exportedRuntimeAPI); @@ -239,6 +238,11 @@ export async function mono_wasm_load_config(module: DotnetModuleInternal): Promi throw err; } } + + await importLibraryInitializers(loaderHelpers.config.resources?.modulesAfterConfigLoaded); + await invokeLibraryInitializers("onRuntimeConfigLoaded", [loaderHelpers.config]); + normalizeConfig(); + loaderHelpers.afterConfigLoaded.promise_control.resolve(loaderHelpers.config); } catch (err) { const errMessage = `Failed to load config file ${configFilePath} ${err} ${(err as Error)?.stack}`; @@ -268,7 +272,7 @@ async function loadBootConfig(module: DotnetModuleInternal): Promise { let loadConfigResponse: Response; if (!loaderResponse) { - loadConfigResponse = await defaultLoadBootConfig(defaultConfigSrc); + loadConfigResponse = await defaultLoadBootConfig(appendUniqueQuery(defaultConfigSrc, "manifest")); } else if (typeof loaderResponse === "string") { loadConfigResponse = await defaultLoadBootConfig(makeURLAbsoluteWithApplicationBase(loaderResponse)); } else { diff --git a/src/mono/wasm/runtime/loader/globals.ts b/src/mono/wasm/runtime/loader/globals.ts index 7b2225f..09c7cad 100644 --- a/src/mono/wasm/runtime/loader/globals.ts +++ b/src/mono/wasm/runtime/loader/globals.ts @@ -74,6 +74,7 @@ export function setLoaderGlobals( _loaded_files: [], loadedFiles: [], loadedAssemblies: [], + libraryInitializers: [], actual_downloaded_assets_count: 0, actual_instantiated_assets_count: 0, expected_downloaded_assets_count: 0, @@ -92,7 +93,7 @@ export function setLoaderGlobals( getPromiseController, assertIsControllablePromise, mono_download_assets, - resolve_asset_path: resolve_single_asset_path, + resolve_single_asset_path, setup_proxy_console, logDownloadStatsToConsole, purgeUnusedCacheEntriesAsync, diff --git a/src/mono/wasm/runtime/loader/libraryInitializers.ts b/src/mono/wasm/runtime/loader/libraryInitializers.ts index 999d847..48fd652 100644 --- a/src/mono/wasm/runtime/loader/libraryInitializers.ts +++ b/src/mono/wasm/runtime/loader/libraryInitializers.ts @@ -1,25 +1,13 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -import { mono_log_warn } from "./logging"; -import { MonoConfig } from "../types"; +import { mono_log_debug, mono_log_warn } from "./logging"; import { appendUniqueQuery } from "./assets"; import { loaderHelpers } from "./globals"; import { mono_exit } from "./exit"; +import { ResourceList } from "../types"; -export type LibraryInitializerTypes = - "modulesAfterConfigLoaded" - | "modulesAfterRuntimeReady"; - -async function fetchLibraryInitializers(config: MonoConfig, type: LibraryInitializerTypes): Promise { - if (!loaderHelpers.libraryInitializers) { - loaderHelpers.libraryInitializers = []; - } - - const libraryInitializers = type == "modulesAfterConfigLoaded" - ? config.resources?.modulesAfterConfigLoaded - : config.resources?.modulesAfterRuntimeReady; - +export async function importLibraryInitializers(libraryInitializers: ResourceList | undefined): Promise { if (!libraryInitializers) { return; } @@ -30,6 +18,7 @@ async function fetchLibraryInitializers(config: MonoConfig, type: LibraryInitial async function importInitializer(path: string): Promise { try { const adjustedPath = appendUniqueQuery(loaderHelpers.locateFile(path), "js-module-library-initializer"); + mono_log_debug(`Attempting to import '${adjustedPath}' for ${path}`); const initializer = await import(/* webpackIgnore: true */ adjustedPath); loaderHelpers.libraryInitializers!.push({ scriptName: path, exports: initializer }); @@ -39,11 +28,7 @@ async function fetchLibraryInitializers(config: MonoConfig, type: LibraryInitial } } -export async function invokeLibraryInitializers(functionName: string, args: any[], type?: LibraryInitializerTypes) { - if (type) { - await fetchLibraryInitializers(loaderHelpers.config, type); - } - +export async function invokeLibraryInitializers(functionName: string, args: any[]) { if (!loaderHelpers.libraryInitializers) { return; } diff --git a/src/mono/wasm/runtime/loader/run.ts b/src/mono/wasm/runtime/loader/run.ts index 1c0e545..e6bd201 100644 --- a/src/mono/wasm/runtime/loader/run.ts +++ b/src/mono/wasm/runtime/loader/run.ts @@ -9,13 +9,13 @@ import type { MonoConfigInternal, EmscriptenModuleInternal, RuntimeModuleExports import { ENVIRONMENT_IS_NODE, ENVIRONMENT_IS_WEB, exportedRuntimeAPI, globalObjectsRoot, mono_assert } from "./globals"; import { deep_merge_config, deep_merge_module, mono_wasm_load_config } from "./config"; import { mono_exit } from "./exit"; -import { setup_proxy_console, mono_log_info } from "./logging"; -import { resolve_single_asset_path, start_asset_download } from "./assets"; +import { setup_proxy_console, mono_log_info, mono_log_debug } from "./logging"; +import { mono_download_assets, prepareAssets, prepareAssetsWorker, resolve_single_asset_path, start_asset_download } from "./assets"; import { detect_features_and_polyfill } from "./polyfills"; import { runtimeHelpers, loaderHelpers } from "./globals"; import { init_globalization } from "./icu"; import { setupPreloadChannelToMainThread } from "./worker"; -import { invokeLibraryInitializers } from "./libraryInitializers"; +import { importLibraryInitializers, invokeLibraryInitializers } from "./libraryInitializers"; import { initCacheToUseIfEnabled } from "./assetsCache"; const module = globalObjectsRoot.module; @@ -429,12 +429,14 @@ export async function createEmscripten(moduleFactory: DotnetModuleConfig | ((api } function importModules() { - runtimeHelpers.runtimeModuleUrl = resolve_single_asset_path("js-module-runtime").resolvedUrl!; - runtimeHelpers.nativeModuleUrl = resolve_single_asset_path("js-module-native").resolvedUrl!; + const jsModuleRuntimeAsset = resolve_single_asset_path("js-module-runtime"); + const jsModuleNativeAsset = resolve_single_asset_path("js-module-native"); + mono_log_debug(`Attempting to import '${jsModuleRuntimeAsset.resolvedUrl}' for ${jsModuleRuntimeAsset.name}`); + mono_log_debug(`Attempting to import '${jsModuleNativeAsset.resolvedUrl}' for ${jsModuleNativeAsset.name}`); return [ // keep js module names dynamic by using config, in the future we can use feature detection to load different flavors - import(/* webpackIgnore: true */runtimeHelpers.runtimeModuleUrl), - import(/* webpackIgnore: true */runtimeHelpers.nativeModuleUrl), + import(/* webpackIgnore: true */jsModuleRuntimeAsset.resolvedUrl!), + import(/* webpackIgnore: true */jsModuleNativeAsset.resolvedUrl!), ]; } @@ -466,6 +468,8 @@ async function createEmscriptenMain(): Promise { // download config await mono_wasm_load_config(module); + prepareAssets(); + const promises = importModules(); await initCacheToUseIfEnabled(); @@ -479,13 +483,18 @@ async function createEmscriptenMain(): Promise { init_globalization(); - // TODO call mono_download_assets(); here in parallel ? + setTimeout(() => { + mono_download_assets(); // intentionally not awaited + }, 0); + const es6Modules = await Promise.all(promises); + await initializeModules(es6Modules as any); await runtimeHelpers.dotnetReady.promise; - await invokeLibraryInitializers("onRuntimeReady", [globalObjectsRoot.api], "modulesAfterRuntimeReady"); + await importLibraryInitializers(loaderHelpers.config.resources?.modulesAfterRuntimeReady); + await invokeLibraryInitializers("onRuntimeReady", [globalObjectsRoot.api]); return exportedRuntimeAPI; } @@ -495,6 +504,8 @@ async function createEmscriptenWorker(): Promise { await loaderHelpers.afterConfigLoaded.promise; + prepareAssetsWorker(); + const promises = importModules(); const es6Modules = await Promise.all(promises); await initializeModules(es6Modules as any); diff --git a/src/mono/wasm/runtime/pthreads/shared/emscripten-replacements.ts b/src/mono/wasm/runtime/pthreads/shared/emscripten-replacements.ts index 6487699..9b5b9fa 100644 --- a/src/mono/wasm/runtime/pthreads/shared/emscripten-replacements.ts +++ b/src/mono/wasm/runtime/pthreads/shared/emscripten-replacements.ts @@ -35,7 +35,7 @@ export function replaceEmscriptenPThreadLibrary(replacements: PThreadReplacement /// We replace Module["PThreads"].allocateUnusedWorker with this version that knows about assets function replacementAllocateUnusedWorker(): void { mono_log_debug("replacementAllocateUnusedWorker"); - const asset = loaderHelpers.resolve_asset_path("js-module-threads"); + const asset = loaderHelpers.resolve_single_asset_path("js-module-threads"); const uri = asset.resolvedUrl; mono_assert(uri !== undefined, "could not resolve the uri for the js-module-threads asset"); const worker = new Worker(uri); diff --git a/src/mono/wasm/runtime/startup.ts b/src/mono/wasm/runtime/startup.ts index 3ea00a8..3cc7317 100644 --- a/src/mono/wasm/runtime/startup.ts +++ b/src/mono/wasm/runtime/startup.ts @@ -167,9 +167,6 @@ function preInit(userPreInit: (() => void)[]) { // - init the rest of the polyfills await mono_wasm_pre_init_essential_async(); - // - start download assets like DLLs - await mono_wasm_pre_init_full(); - endMeasure(mark, MeasuredBlock.preInit); } catch (err) { loaderHelpers.mono_exit(1, err); @@ -272,7 +269,10 @@ async function onRuntimeInitializedAsync(userOnRuntimeInitialized: () => void) { if (loaderHelpers.config.debugLevel !== 0 && loaderHelpers.config.cacheBootResources) { loaderHelpers.logDownloadStatsToConsole(); } - loaderHelpers.purgeUnusedCacheEntriesAsync(); // Don't await - it's fine to run in background + const afterStartupRushIsOver = 10000;// 10 seconds + setTimeout(() => { + loaderHelpers.purgeUnusedCacheEntriesAsync(); // Don't await - it's fine to run in background + }, afterStartupRushIsOver); // call user code try { @@ -382,15 +382,6 @@ async function mono_wasm_pre_init_essential_async(): Promise { Module.removeRunDependency("mono_wasm_pre_init_essential_async"); } -async function mono_wasm_pre_init_full(): Promise { - mono_log_debug("mono_wasm_pre_init_full"); - Module.addRunDependency("mono_wasm_pre_init_full"); - - await loaderHelpers.mono_download_assets(); - - Module.removeRunDependency("mono_wasm_pre_init_full"); -} - async function mono_wasm_after_user_runtime_initialized(): Promise { mono_log_debug("mono_wasm_after_user_runtime_initialized"); try { diff --git a/src/mono/wasm/runtime/types/export-types.ts b/src/mono/wasm/runtime/types/export-types.ts index 857c953..cdd34f3 100644 --- a/src/mono/wasm/runtime/types/export-types.ts +++ b/src/mono/wasm/runtime/types/export-types.ts @@ -2,7 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. import type { IMemoryView } from "../marshal"; -import type { CreateDotnetRuntimeType, DotnetHostBuilder, DotnetModuleConfig, RuntimeAPI, MonoConfig, ModuleAPI, AssetEntry, ResourceRequest, GlobalizationMode, AssetBehaviors } from "."; +import type { CreateDotnetRuntimeType, DotnetHostBuilder, DotnetModuleConfig, RuntimeAPI, MonoConfig, ModuleAPI, AssetEntry, GlobalizationMode, AssetBehaviors } from "."; import type { EmscriptenModule } from "./emscripten"; import type { dotnet, exit } from "../loader/index"; @@ -21,6 +21,6 @@ export default createDotnetRuntime; export { EmscriptenModule, - RuntimeAPI, ModuleAPI, DotnetHostBuilder, DotnetModuleConfig, CreateDotnetRuntimeType, MonoConfig, IMemoryView, AssetEntry, ResourceRequest, GlobalizationMode, AssetBehaviors, + RuntimeAPI, ModuleAPI, DotnetHostBuilder, DotnetModuleConfig, CreateDotnetRuntimeType, MonoConfig, IMemoryView, AssetEntry, GlobalizationMode, AssetBehaviors, dotnet, exit }; diff --git a/src/mono/wasm/runtime/types/index.ts b/src/mono/wasm/runtime/types/index.ts index c6001ee..7be774e 100644 --- a/src/mono/wasm/runtime/types/index.ts +++ b/src/mono/wasm/runtime/types/index.ts @@ -64,6 +64,14 @@ export type MonoConfig = { */ cacheBootResources?: boolean, /** + * Configures use of the `integrity` directive for fetching assets + */ + disableIntegrityCheck?: boolean, + /** + * Configures use of the `no-cache` directive for fetching assets + */ + disableNoCacheFetch?: boolean, + /** * Enables diagnostic log messages during startup */ diagnosticTracing?: boolean @@ -149,13 +157,6 @@ export type ResourceList = { [name: string]: string | null | "" }; */ export type LoadBootResourceCallback = (type: WebAssemblyBootResourceType, name: string, defaultUri: string, integrity: string, behavior: AssetBehaviors) => string | Promise | null | undefined; -export interface ResourceRequest { - name: string, // the name of the asset, including extension. - behavior: AssetBehaviors, // determines how the asset will be handled once loaded - resolvedUrl?: string; // this should be absolute url to the asset - hash?: string | null | ""; // the integrity hash of the asset (if any) -} - export interface LoadingResource { name: string; url: string; @@ -163,7 +164,23 @@ export interface LoadingResource { } // Types of assets that can be in the _framework/blazor.boot.json file (taken from /src/tasks/WasmAppBuilder/WasmAppBuilder.cs) -export interface AssetEntry extends ResourceRequest { +export interface AssetEntry { + /** + * the name of the asset, including extension. + */ + name: string, + /** + * determines how the asset will be handled once loaded + */ + behavior: AssetBehaviors, + /** + * this should be absolute url to the asset + */ + resolvedUrl?: string; + /** + * the integrity hash of the asset (if any) + */ + hash?: string | null | ""; // /** * If specified, overrides the path of the asset in the virtual filesystem and similar data structures once downloaded. */ diff --git a/src/mono/wasm/runtime/types/internal.ts b/src/mono/wasm/runtime/types/internal.ts index dda104d..3e218a4 100644 --- a/src/mono/wasm/runtime/types/internal.ts +++ b/src/mono/wasm/runtime/types/internal.ts @@ -69,7 +69,7 @@ export function coerceNull(ptr: T | nu // when adding new fields, please consider if it should be impacting the snapshot hash. If not, please drop it in the snapshot getCacheKey() export type MonoConfigInternal = MonoConfig & { linkerEnabled?: boolean, - assets?: AssetEntry[], + assets?: AssetEntryInternal[], runtimeOptions?: string[], // array of runtime options as strings aotProfilerOptions?: AOTProfilerOptions, // dictionary-style Object. If omitted, aot profiler will not be initialized. browserProfilerOptions?: BrowserProfilerOptions, // dictionary-style Object. If omitted, browser profiler will not be initialized. @@ -93,8 +93,10 @@ export type RunArguments = { } export interface AssetEntryInternal extends AssetEntry { - // this is almost the same as pendingDownload, but it could have multiple values in time, because of re-try download logic + // this could have multiple values in time, because of re-try download logic pendingDownloadInternal?: LoadingResource + noCache?: boolean + useCredentials?: boolean } export type LoaderHelpers = { @@ -134,7 +136,7 @@ export type LoaderHelpers = { getPromiseController: (promise: ControllablePromise) => PromiseController, assertIsControllablePromise: (promise: Promise) => asserts promise is ControllablePromise, mono_download_assets: () => Promise, - resolve_asset_path: (behavior: AssetBehaviors) => AssetEntryInternal, + resolve_single_asset_path: (behavior: AssetBehaviors) => AssetEntryInternal, setup_proxy_console: (id: string, console: Console, origin: string) => void fetch_like: (url: string, init?: RequestInit) => Promise; locateFile: (path: string, prefix?: string) => string, @@ -187,8 +189,6 @@ export type RuntimeHelpers = { jsSynchronizationContextInstalled: boolean, cspPolicy: boolean, - runtimeModuleUrl: string - nativeModuleUrl: string allAssetsInMemory: PromiseAndController, dotnetReady: PromiseAndController, memorySnapshotSkippedOrDone: PromiseAndController,