From 37ff744a7833729198ac557377e00b1b558a3d98 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Mon, 15 Apr 2024 20:37:18 +0200 Subject: [PATCH 01/10] rebase, fix --- eng/testing/linker/trimmingTests.targets | 2 +- .../src/System/Globalization/CultureData.cs | 8 ++++++++ .../src/System/Globalization/GlobalizationMode.cs | 6 ++++++ .../TrimmingTests/InvariantGlobalizationTrue.cs | 13 +++++++------ src/mono/browser/runtime/assets.ts | 5 ++--- src/mono/browser/runtime/icu.ts | 15 +++++++++++++-- src/mono/browser/runtime/startup.ts | 3 +++ 7 files changed, 40 insertions(+), 12 deletions(-) diff --git a/eng/testing/linker/trimmingTests.targets b/eng/testing/linker/trimmingTests.targets index 926bafa52cfef..70e072844484b 100644 --- a/eng/testing/linker/trimmingTests.targets +++ b/eng/testing/linker/trimmingTests.targets @@ -156,7 +156,7 @@ - + diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.cs index 189ce06de7033..6c91bba74bd41 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.cs @@ -630,7 +630,11 @@ private static CultureData CreateCultureWithInvariantData() // all available calendar type(s). The first one is the default calendar invariant._waCalendars = new CalendarId[] { CalendarId.GREGORIAN }; +#if TARGET_BROWSER + if (!GlobalizationMode.InvariantFast) +#else if (!GlobalizationMode.Invariant) +#endif { // Store for specific data about each calendar invariant._calendars = new CalendarData[CalendarData.MAX_CALENDARS]; @@ -648,7 +652,11 @@ private static CultureData CreateCultureWithInvariantData() invariant._iDefaultMacCodePage = 10000; // default macintosh code page invariant._iDefaultEbcdicCodePage = 037; // default EBCDIC code page +#if TARGET_BROWSER + if (GlobalizationMode.InvariantFast) +#else if (GlobalizationMode.Invariant) +#endif { invariant._sLocalizedCountry = invariant._sNativeCountry; } diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/GlobalizationMode.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/GlobalizationMode.cs index 314bb33692dc7..b13312ae54a95 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/GlobalizationMode.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/GlobalizationMode.cs @@ -25,6 +25,12 @@ private static partial class Settings // This allows for the whole Settings nested class to be trimmed when Invariant=true, and allows for the Settings // static cctor (on Unix) to be preserved when Invariant=false. internal static bool Invariant => Settings.Invariant; + +#if TARGET_BROWSER + // same as GlobalizationMode.Invariant but doesn't trigger ICU load in GlobalizationMode.Settings.cctor during runtime startup + internal static bool InvariantFast { get; } = AppContextConfigHelper.GetBooleanConfig("System.Globalization.Invariant", "DOTNET_SYSTEM_GLOBALIZATION_INVARIANT"); +#endif + #if TARGET_MACCATALYST || TARGET_IOS || TARGET_TVOS || TARGET_BROWSER internal static bool Hybrid => Settings.Hybrid; #endif diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/TrimmingTests/InvariantGlobalizationTrue.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/TrimmingTests/InvariantGlobalizationTrue.cs index fe878e3a7459d..0f9628341e046 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/TrimmingTests/InvariantGlobalizationTrue.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/TrimmingTests/InvariantGlobalizationTrue.cs @@ -27,12 +27,13 @@ static int Main(string[] args) { return -3; } - + // Ensure the internal GlobalizationMode class is trimmed correctly. Type globalizationMode = GetCoreLibType("System.Globalization.GlobalizationMode"); - if (OperatingSystem.IsWindows()) + if (OperatingSystem.IsWindows() || OperatingSystem.IsBrowser()) { + string allowedMember = OperatingSystem.IsWindows() ? "UseNls" : "InvariantFast"; foreach (MemberInfo member in globalizationMode.GetMembers(allStatics)) { // properties and their backing getter methods are OK @@ -41,8 +42,8 @@ static int Main(string[] args) continue; } - // Windows still contains a static cctor and a backing field for UseNls. - if (member is ConstructorInfo || (member is FieldInfo field && field.Name.Contains("UseNls"))) + // Windows still contains a static cctor and a backing field for UseNls or InvariantFast. + if (member is ConstructorInfo || (member is FieldInfo field && field.Name.Contains(allowedMember))) { continue; } @@ -52,10 +53,10 @@ static int Main(string[] args) return -4; } } - // On non Windows platforms, the full type is trimmed. + // On non Windows platforms, the full type is trimmed, unless it's Browser where we use FastInvariant for lazy ICU loading else if (globalizationMode is not null) { - Console.WriteLine("It is expected to have System.Globalization.GlobalizationMode type trimmed in non-Windows platforms"); + Console.WriteLine("It is expected to have System.Globalization.GlobalizationMode type trimmed in non-Windows and non-Browser platforms"); return -5; } diff --git a/src/mono/browser/runtime/assets.ts b/src/mono/browser/runtime/assets.ts index 5c3f7ecce8822..7d0d79274d22e 100644 --- a/src/mono/browser/runtime/assets.ts +++ b/src/mono/browser/runtime/assets.ts @@ -4,7 +4,7 @@ import type { AssetEntryInternal } from "./types/internal"; import cwraps from "./cwraps"; -import { mono_wasm_load_icu_data } from "./icu"; +import { mono_wasm_prepare_icu_data } from "./icu"; import { Module, loaderHelpers, mono_assert, runtimeHelpers } from "./globals"; import { mono_log_info, mono_log_debug, parseSymbolMapFile } from "./logging"; import { mono_wasm_load_bytes_into_heap_persistent } from "./memory"; @@ -85,8 +85,7 @@ export function instantiate_asset (asset: AssetEntry, url: string, bytes: Uint8A } else if (asset.behavior === "pdb") { cwraps.mono_wasm_add_assembly(virtualName, offset!, bytes.length); } else if (asset.behavior === "icu") { - if (!mono_wasm_load_icu_data(offset!)) - Module.err(`Error loading ICU asset ${asset.name}`); + mono_wasm_prepare_icu_data(offset!); } else if (asset.behavior === "resource") { cwraps.mono_wasm_add_satellite_assembly(virtualName, asset.culture || "", offset!, bytes.length); } diff --git a/src/mono/browser/runtime/icu.ts b/src/mono/browser/runtime/icu.ts index 16b23871f9871..955b42e0e3c78 100644 --- a/src/mono/browser/runtime/icu.ts +++ b/src/mono/browser/runtime/icu.ts @@ -4,8 +4,19 @@ import cwraps from "./cwraps"; import { VoidPtr } from "./types/emscripten"; + +let icuDataOffset: VoidPtr | null = null; // @offset must be the address of an ICU data archive in the native heap. // returns true on success. -export function mono_wasm_load_icu_data (offset: VoidPtr): boolean { - return (cwraps.mono_wasm_load_icu_data(offset)) === 1; +export function mono_wasm_prepare_icu_data (offset: VoidPtr) { + icuDataOffset = offset; +} + +export function mono_wasm_load_icu_data () { + if (icuDataOffset === null) { + return; + } + if (!cwraps.mono_wasm_load_icu_data(icuDataOffset!)) { + throw new Error("Failed to load ICU data"); + } } diff --git a/src/mono/browser/runtime/startup.ts b/src/mono/browser/runtime/startup.ts index b89522caff198..38119db564f75 100644 --- a/src/mono/browser/runtime/startup.ts +++ b/src/mono/browser/runtime/startup.ts @@ -34,6 +34,7 @@ import { runtimeList } from "./exports"; import { nativeAbort, nativeExit } from "./run"; import { mono_wasm_init_diagnostics } from "./diagnostics"; import { replaceEmscriptenPThreadInit } from "./pthreads/worker-thread"; +import { mono_wasm_load_icu_data } from "./icu"; export async function configureRuntimeStartup (module: DotnetModuleInternal): Promise { if (!module.out) { @@ -307,6 +308,8 @@ async function onRuntimeInitializedAsync (userOnRuntimeInitialized: () => void) await start_runtime(); } + mono_wasm_load_icu_data(); + if (WasmEnableThreads) { await runtimeHelpers.afterIOStarted.promise; } From d750862f42a320899ec24fc167279ad87c2a0c3f Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Mon, 15 Apr 2024 20:44:53 +0200 Subject: [PATCH 02/10] rebase --- src/mono/browser/build/BrowserWasmApp.targets | 2 + src/mono/browser/runtime/assets.ts | 3 + src/mono/browser/runtime/dotnet.d.ts | 5 + src/mono/browser/runtime/globals.ts | 1 + src/mono/browser/runtime/loader/assets.ts | 186 ++++++++++++------ src/mono/browser/runtime/startup.ts | 19 +- src/mono/browser/runtime/types/index.ts | 3 + src/mono/browser/runtime/types/internal.ts | 2 + src/mono/mono/metadata/bundled-resources.c | 3 - ...rosoft.NET.Sdk.WebAssembly.Browser.targets | 8 +- .../wasm/browser-minimal-config/main.js | 12 +- .../Wasm.Build.Tests/ProjectProviderBase.cs | 2 +- src/mono/wasm/build/WasmApp.Common.targets | 2 +- .../WasmBasicTestApp/App/wwwroot/main.js | 2 +- .../BootJsonBuilderHelper.cs | 30 ++- .../BootJsonData.cs | 14 ++ .../GenerateWasmBootJson.cs | 48 ++++- src/tasks/WasmAppBuilder/WasmAppBuilder.cs | 35 +++- 18 files changed, 284 insertions(+), 93 deletions(-) diff --git a/src/mono/browser/build/BrowserWasmApp.targets b/src/mono/browser/build/BrowserWasmApp.targets index aa231c45d8b7c..749b4e4c2a509 100644 --- a/src/mono/browser/build/BrowserWasmApp.targets +++ b/src/mono/browser/build/BrowserWasmApp.targets @@ -156,6 +156,8 @@ RuntimeAssetsLocation="$(WasmRuntimeAssetsLocation)" CacheBootResources="$(BlazorCacheBootResources)" RuntimeConfigJsonPath="$(_WasmRuntimeConfigFilePath)" + IsAot="$(RunAOTCompilation)" + IsMultiThreaded="$(WasmEnableThreads)" > diff --git a/src/mono/browser/runtime/assets.ts b/src/mono/browser/runtime/assets.ts index 7d0d79274d22e..7f844b4cb4801 100644 --- a/src/mono/browser/runtime/assets.ts +++ b/src/mono/browser/runtime/assets.ts @@ -52,6 +52,9 @@ export function instantiate_asset (asset: AssetEntry, url: string, bytes: Uint8A if (fileName.startsWith("/")) fileName = fileName.substring(1); if (parentDirectory) { + if (!parentDirectory.startsWith("/")) + parentDirectory = "/" + parentDirectory; + mono_log_debug(`Creating directory '${parentDirectory}'`); Module.FS_createPath( diff --git a/src/mono/browser/runtime/dotnet.d.ts b/src/mono/browser/runtime/dotnet.d.ts index 16df07663ea7e..7a95d510b8b01 100644 --- a/src/mono/browser/runtime/dotnet.d.ts +++ b/src/mono/browser/runtime/dotnet.d.ts @@ -242,8 +242,10 @@ type ResourceExtensions = { }; interface ResourceGroups { hash?: string; + coreAssembly?: ResourceList; assembly?: ResourceList; lazyAssembly?: ResourceList; + corePdb?: ResourceList; pdb?: ResourceList; jsModuleWorker?: ResourceList; jsModuleNative: ResourceList; @@ -257,6 +259,9 @@ interface ResourceGroups { modulesAfterConfigLoaded?: ResourceList; modulesAfterRuntimeReady?: ResourceList; extensions?: ResourceExtensions; + coreVfs?: { + [virtualPath: string]: ResourceList; + }; vfs?: { [virtualPath: string]: ResourceList; }; diff --git a/src/mono/browser/runtime/globals.ts b/src/mono/browser/runtime/globals.ts index 047fcb60c9502..d5e352efff785 100644 --- a/src/mono/browser/runtime/globals.ts +++ b/src/mono/browser/runtime/globals.ts @@ -57,6 +57,7 @@ export function setRuntimeGlobals (globalObjects: GlobalObjects) { const rh: Partial = { gitHash, + coreAssetsInMemory: createPromiseController(), allAssetsInMemory: createPromiseController(), dotnetReady: createPromiseController(), afterInstantiateWasm: createPromiseController(), diff --git a/src/mono/browser/runtime/loader/assets.ts b/src/mono/browser/runtime/loader/assets.ts index ad9d3650791e4..846fb91133f34 100644 --- a/src/mono/browser/runtime/loader/assets.ts +++ b/src/mono/browser/runtime/loader/assets.ts @@ -18,6 +18,7 @@ import { mono_log_info } from "./logging"; 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 coreAssetsToLoad: AssetEntryInternal[] = []; const assetsToLoad: AssetEntryInternal[] = []; const singleAssets: Map = new Map(); @@ -152,21 +153,25 @@ export function resolve_single_asset_path (behavior: SingleAssetBehaviors): Asse export async function mono_download_assets (): Promise { mono_log_debug("mono_download_assets"); try { - const promises_of_assets: Promise[] = []; + const promises_of_assets_core: Promise[] = []; + const promises_of_assets_remaining: Promise[] = []; - const countAndStartDownload = (asset: AssetEntryInternal) => { + const countAndStartDownload = (asset: AssetEntryInternal, promises_list: Promise[]) => { if (!skipInstantiateByAssetTypes[asset.behavior] && shouldLoadIcuAsset(asset)) { loaderHelpers.expected_instantiated_assets_count++; } if (!skipDownloadsByAssetTypes[asset.behavior] && shouldLoadIcuAsset(asset)) { loaderHelpers.expected_downloaded_assets_count++; - promises_of_assets.push(start_asset_download(asset)); + promises_list.push(start_asset_download(asset)); } }; // start fetching assets in parallel + for (const asset of coreAssetsToLoad) { + countAndStartDownload(asset, promises_of_assets_core); + } for (const asset of assetsToLoad) { - countAndStartDownload(asset); + countAndStartDownload(asset, promises_of_assets_remaining); } loaderHelpers.allDownloadsQueued.promise_control.resolve(); @@ -174,54 +179,68 @@ export async function mono_download_assets (): Promise { // continue after the dotnet.runtime.js was loaded await loaderHelpers.runtimeModuleLoaded.promise; - const promises_of_asset_instantiation: Promise[] = []; - for (const downloadPromise of promises_of_assets) { - promises_of_asset_instantiation.push((async () => { - const asset = await downloadPromise; - if (asset.buffer) { - if (!skipInstantiateByAssetTypes[asset.behavior]) { - mono_assert(asset.buffer && typeof asset.buffer === "object", "asset buffer must be array-like or buffer-like or promise of these"); - mono_assert(typeof asset.resolvedUrl === "string", "resolvedUrl must be string"); - const url = asset.resolvedUrl!; - const buffer = await asset.buffer; - const data = new Uint8Array(buffer); - cleanupAsset(asset); - - // wait till after onRuntimeInitialized - - await runtimeHelpers.beforeOnRuntimeInitialized.promise; - runtimeHelpers.instantiate_asset(asset, url, data); + const instantiate = async (downloadPromise: Promise) => { + const asset = await downloadPromise; + if (asset.buffer) { + if (!skipInstantiateByAssetTypes[asset.behavior]) { + mono_assert(asset.buffer && typeof asset.buffer === "object", "asset buffer must be array-like or buffer-like or promise of these"); + mono_assert(typeof asset.resolvedUrl === "string", "resolvedUrl must be string"); + const url = asset.resolvedUrl!; + const buffer = await asset.buffer; + const data = new Uint8Array(buffer); + cleanupAsset(asset); + + // wait till after onRuntimeInitialized + + await runtimeHelpers.beforeOnRuntimeInitialized.promise; + runtimeHelpers.instantiate_asset(asset, url, data); + } + } else { + const headersOnly = skipBufferByAssetTypes[asset.behavior]; + if (!headersOnly) { + mono_assert(asset.isOptional, "Expected asset to have the downloaded buffer"); + if (!skipDownloadsByAssetTypes[asset.behavior] && shouldLoadIcuAsset(asset)) { + loaderHelpers.expected_downloaded_assets_count--; + } + if (!skipInstantiateByAssetTypes[asset.behavior] && shouldLoadIcuAsset(asset)) { + loaderHelpers.expected_instantiated_assets_count--; } } else { - const headersOnly = skipBufferByAssetTypes[asset.behavior]; - if (!headersOnly) { - mono_assert(asset.isOptional, "Expected asset to have the downloaded buffer"); - if (!skipDownloadsByAssetTypes[asset.behavior] && shouldLoadIcuAsset(asset)) { - loaderHelpers.expected_downloaded_assets_count--; - } - if (!skipInstantiateByAssetTypes[asset.behavior] && shouldLoadIcuAsset(asset)) { - loaderHelpers.expected_instantiated_assets_count--; - } - } else { - if (asset.behavior === "symbols") { - await runtimeHelpers.instantiate_symbols_asset(asset); - cleanupAsset(asset); - } else if (asset.behavior === "segmentation-rules") { - await runtimeHelpers.instantiate_segmentation_rules_asset(asset); - cleanupAsset(asset); - } - - if (skipBufferByAssetTypes[asset.behavior]) { - ++loaderHelpers.actual_downloaded_assets_count; - } + if (asset.behavior === "symbols") { + await runtimeHelpers.instantiate_symbols_asset(asset); + cleanupAsset(asset); + } else if (asset.behavior === "segmentation-rules") { + await runtimeHelpers.instantiate_segmentation_rules_asset(asset); + cleanupAsset(asset); + } + + if (skipBufferByAssetTypes[asset.behavior]) { + ++loaderHelpers.actual_downloaded_assets_count; } } - })()); + } + }; + + const promises_of_asset_instantiation_core: Promise[] = []; + const promises_of_asset_instantiation_remaining: Promise[] = []; + for (const downloadPromise of promises_of_assets_core) { + promises_of_asset_instantiation_core.push(instantiate(downloadPromise)); + } + for (const downloadPromise of promises_of_assets_remaining) { + promises_of_asset_instantiation_remaining.push(instantiate(downloadPromise)); } // this await will get past the onRuntimeInitialized because we are not blocking via addRunDependency - // and we are not awating it here - Promise.all(promises_of_asset_instantiation).then(() => { + // and we are not awaiting it here + Promise.all(promises_of_asset_instantiation_core).then(() => { + runtimeHelpers.coreAssetsInMemory.promise_control.resolve(); + }).catch(err => { + loaderHelpers.err("Error in mono_download_assets: " + err); + mono_exit(1, err); + throw err; + }); + Promise.all(promises_of_asset_instantiation_remaining).then(async () => { + await runtimeHelpers.coreAssetsInMemory.promise; runtimeHelpers.allAssetsInMemory.promise_control.resolve(); }).catch(err => { loaderHelpers.err("Error in mono_download_assets: " + err); @@ -251,7 +270,11 @@ export function prepareAssets () { mono_assert(!asset.resolvedUrl || typeof asset.resolvedUrl === "string", "asset resolvedUrl could be string"); mono_assert(!asset.hash || typeof asset.hash === "string", "asset resolvedUrl could be string"); mono_assert(!asset.pendingDownload || typeof asset.pendingDownload === "object", "asset pendingDownload could be object"); - assetsToLoad.push(asset); + if (asset.isCore) { + coreAssetsToLoad.push(asset); + } else { + assetsToLoad.push(asset); + } set_single_asset(asset); } } else if (config.resources) { @@ -268,23 +291,55 @@ export function prepareAssets () { convert_single_asset(modulesAssets, resources.jsModuleWorker, "js-module-threads"); } + const addAsset = (asset: AssetEntryInternal, isCore: boolean) => { + if (isCore) { + asset.isCore = true; + coreAssetsToLoad.push(asset); + } else { + assetsToLoad.push(asset); + } + }; + + if (resources.coreAssembly) { + for (const name in resources.coreAssembly) { + addAsset({ + name, + hash: resources.coreAssembly[name], + behavior: "assembly" + }, true); + } + } + if (resources.assembly) { for (const name in resources.assembly) { - assetsToLoad.push({ + addAsset({ name, hash: resources.assembly[name], behavior: "assembly" - }); + }, !resources.coreAssembly); // if there are no core assemblies, then all assemblies are core } } - if (config.debugLevel != 0 && loaderHelpers.isDebuggingSupported() && resources.pdb) { - for (const name in resources.pdb) { - assetsToLoad.push({ - name, - hash: resources.pdb[name], - behavior: "pdb" - }); + + if (config.debugLevel != 0 && loaderHelpers.isDebuggingSupported()) { + if (resources.corePdb) { + for (const name in resources.corePdb) { + addAsset({ + name, + hash: resources.corePdb[name], + behavior: "pdb" + }, true); + } + } + + if (resources.pdb) { + for (const name in resources.pdb) { + addAsset({ + name, + hash: resources.pdb[name], + behavior: "pdb" + }, !resources.corePdb); // if there are no core pdbs, then all pdbs are core + } } } @@ -301,15 +356,28 @@ export function prepareAssets () { } } + if (resources.coreVfs) { + for (const virtualPath in resources.coreVfs) { + for (const name in resources.coreVfs[virtualPath]) { + addAsset({ + name, + hash: resources.coreVfs[virtualPath][name], + behavior: "vfs", + virtualPath + }, true); + } + } + } + if (resources.vfs) { for (const virtualPath in resources.vfs) { for (const name in resources.vfs[virtualPath]) { - assetsToLoad.push({ + addAsset({ name, hash: resources.vfs[virtualPath][name], behavior: "vfs", virtualPath - }); + }, !resources.coreVfs); } } } @@ -336,7 +404,7 @@ export function prepareAssets () { if (resources.wasmSymbols) { for (const name in resources.wasmSymbols) { - assetsToLoad.push({ + coreAssetsToLoad.push({ name, hash: resources.wasmSymbols[name], behavior: "symbols" @@ -363,7 +431,7 @@ export function prepareAssets () { } } - config.assets = [...assetsToLoad, ...modulesAssets]; + config.assets = [...coreAssetsToLoad, ...assetsToLoad, ...modulesAssets]; } export function prepareAssetsWorker () { diff --git a/src/mono/browser/runtime/startup.ts b/src/mono/browser/runtime/startup.ts index 38119db564f75..75ab5200d68e8 100644 --- a/src/mono/browser/runtime/startup.ts +++ b/src/mono/browser/runtime/startup.ts @@ -256,16 +256,21 @@ async function onRuntimeInitializedAsync (userOnRuntimeInitialized: () => void) threadsReady = mono_wasm_init_threads(); } - await wait_for_all_assets(); + await runtimeHelpers.coreAssetsInMemory.promise; if (runtimeHelpers.config.virtualWorkingDirectory) { const FS = Module.FS; const cwd = runtimeHelpers.config.virtualWorkingDirectory; - const wds = FS.stat(cwd); - if (!wds) { + try { + const wds = FS.stat(cwd); + if (!wds) { + Module.FS_createPath("/", cwd, true, true); + } else { + mono_assert(wds && FS.isDir(wds.mode), () => `FS.chdir: ${cwd} is not a directory`); + } + } catch (e) { Module.FS_createPath("/", cwd, true, true); } - mono_assert(wds && FS.isDir(wds.mode), () => `FS.chdir: ${cwd} is not a directory`); FS.chdir(cwd); } @@ -308,12 +313,14 @@ async function onRuntimeInitializedAsync (userOnRuntimeInitialized: () => void) await start_runtime(); } - mono_wasm_load_icu_data(); - if (WasmEnableThreads) { await runtimeHelpers.afterIOStarted.promise; } + await wait_for_all_assets(); + + mono_wasm_load_icu_data(); + runtimeList.registerRuntime(exportedRuntimeAPI); if (!runtimeHelpers.mono_wasm_runtime_is_ready) mono_wasm_runtime_ready(); diff --git a/src/mono/browser/runtime/types/index.ts b/src/mono/browser/runtime/types/index.ts index dad313a47a07e..df2fb6b69c81b 100644 --- a/src/mono/browser/runtime/types/index.ts +++ b/src/mono/browser/runtime/types/index.ts @@ -199,8 +199,10 @@ export type ResourceExtensions = { [extensionName: string]: ResourceList }; export interface ResourceGroups { hash?: string; + coreAssembly?: ResourceList; // nullable only temporarily assembly?: ResourceList; // nullable only temporarily lazyAssembly?: ResourceList; // nullable only temporarily + corePdb?: ResourceList; pdb?: ResourceList; jsModuleWorker?: ResourceList; @@ -216,6 +218,7 @@ export interface ResourceGroups { modulesAfterRuntimeReady?: ResourceList extensions?: ResourceExtensions + coreVfs?: { [virtualPath: string]: ResourceList }; vfs?: { [virtualPath: string]: ResourceList }; } diff --git a/src/mono/browser/runtime/types/internal.ts b/src/mono/browser/runtime/types/internal.ts index e5579bfeba0f8..4224d294d2d62 100644 --- a/src/mono/browser/runtime/types/internal.ts +++ b/src/mono/browser/runtime/types/internal.ts @@ -111,6 +111,7 @@ export interface AssetEntryInternal extends AssetEntry { pendingDownloadInternal?: LoadingResource noCache?: boolean useCredentials?: boolean + isCore?: boolean } export type LoaderHelpers = { @@ -214,6 +215,7 @@ export type RuntimeHelpers = { isPendingSynchronousCall: boolean, // true when we are in the middle of a synchronous call from managed code from same thread cspPolicy: boolean, + coreAssetsInMemory: PromiseAndController, allAssetsInMemory: PromiseAndController, dotnetReady: PromiseAndController, afterInstantiateWasm: PromiseAndController, diff --git a/src/mono/mono/metadata/bundled-resources.c b/src/mono/mono/metadata/bundled-resources.c index 9eae4f5db8fa8..c280919bf642c 100644 --- a/src/mono/mono/metadata/bundled-resources.c +++ b/src/mono/mono/metadata/bundled-resources.c @@ -126,9 +126,6 @@ bundled_resources_resource_id_hash (const char *id) void mono_bundled_resources_add (MonoBundledResource **resources_to_bundle, uint32_t len) { - MonoDomain *domain = mono_get_root_domain (); - g_assert (!domain); - if (!bundled_resources) bundled_resources = g_hash_table_new_full ((GHashFunc)bundled_resources_resource_id_hash, (GEqualFunc)bundled_resources_resource_id_equal, NULL, bundled_resources_value_destroy_func); diff --git a/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets b/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets index fcf283375f826..f21da1b251ee5 100644 --- a/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets +++ b/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets @@ -343,7 +343,9 @@ Copyright (c) .NET Foundation. All rights reserved. TargetFrameworkVersion="$(TargetFrameworkVersion)" ModuleAfterConfigLoaded="@(WasmModuleAfterConfigLoaded)" ModuleAfterRuntimeReady="@(WasmModuleAfterRuntimeReady)" - IsPublish="false" /> + IsPublish="false" + IsAot="$(RunAOTCompilation)" + IsMultiThreaded="$(WasmEnableThreads)" /> @@ -586,7 +588,9 @@ Copyright (c) .NET Foundation. All rights reserved. TargetFrameworkVersion="$(TargetFrameworkVersion)" ModuleAfterConfigLoaded="@(WasmModuleAfterConfigLoaded)" ModuleAfterRuntimeReady="@(WasmModuleAfterRuntimeReady)" - IsPublish="true" /> + IsPublish="true" + IsAot="$(RunAOTCompilation)" + IsMultiThreaded="$(WasmEnableThreads)" /> diff --git a/src/mono/sample/wasm/browser-minimal-config/main.js b/src/mono/sample/wasm/browser-minimal-config/main.js index acd81b2899a39..702234c984df5 100644 --- a/src/mono/sample/wasm/browser-minimal-config/main.js +++ b/src/mono/sample/wasm/browser-minimal-config/main.js @@ -34,11 +34,13 @@ const assets = [ }, { name: "System.Private.CoreLib.wasm", - behavior: "assembly" + behavior: "assembly", + isCore: true, }, { name: "System.Runtime.InteropServices.JavaScript.wasm", - behavior: "assembly" + behavior: "assembly", + isCore: true, }, { name: "Wasm.Browser.Config.Sample.wasm", @@ -62,10 +64,12 @@ const resources = { "wasmNative": { "dotnet.native.wasm": "" }, - "assembly": { - "System.Console.wasm": "", + "coreAssembly": { "System.Private.CoreLib.wasm": "", "System.Runtime.InteropServices.JavaScript.wasm": "", + }, + "assembly": { + "System.Console.wasm": "", "Wasm.Browser.Config.Sample.wasm": "" }, }; diff --git a/src/mono/wasm/Wasm.Build.Tests/ProjectProviderBase.cs b/src/mono/wasm/Wasm.Build.Tests/ProjectProviderBase.cs index 581a187270ae4..8e725222cbca7 100644 --- a/src/mono/wasm/Wasm.Build.Tests/ProjectProviderBase.cs +++ b/src/mono/wasm/Wasm.Build.Tests/ProjectProviderBase.cs @@ -404,7 +404,7 @@ public void AssertBootJson(AssertBundleOptionsBase options) BootJsonData bootJson = ParseBootData(bootJsonPath); string spcExpectedFilename = $"System.Private.CoreLib{WasmAssemblyExtension}"; - string? spcActualFilename = bootJson.resources.assembly.Keys + string? spcActualFilename = bootJson.resources.coreAssembly.Keys .Where(a => Path.GetFileNameWithoutExtension(a) == "System.Private.CoreLib") .SingleOrDefault(); if (spcActualFilename is null) diff --git a/src/mono/wasm/build/WasmApp.Common.targets b/src/mono/wasm/build/WasmApp.Common.targets index 5c7e4353fee0a..0679eb9ae2d2e 100644 --- a/src/mono/wasm/build/WasmApp.Common.targets +++ b/src/mono/wasm/build/WasmApp.Common.targets @@ -421,7 +421,7 @@ - + diff --git a/src/mono/wasm/testassets/WasmBasicTestApp/App/wwwroot/main.js b/src/mono/wasm/testassets/WasmBasicTestApp/App/wwwroot/main.js index 5aeeb024096d5..3e1404daa88a3 100644 --- a/src/mono/wasm/testassets/WasmBasicTestApp/App/wwwroot/main.js +++ b/src/mono/wasm/testassets/WasmBasicTestApp/App/wwwroot/main.js @@ -83,7 +83,7 @@ switch (testCase) { const { getAssemblyExports, getConfig, INTERNAL } = await dotnet.create(); const config = getConfig(); const exports = await getAssemblyExports(config.mainAssemblyName); -const assemblyExtension = config.resources.assembly['System.Private.CoreLib.wasm'] !== undefined ? ".wasm" : ".dll"; +const assemblyExtension = config.resources.coreAssembly['System.Private.CoreLib.wasm'] !== undefined ? ".wasm" : ".dll"; // Run the test case try { diff --git a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/BootJsonBuilderHelper.cs b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/BootJsonBuilderHelper.cs index 17760e84aad06..6436fc1aecb93 100644 --- a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/BootJsonBuilderHelper.cs +++ b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/BootJsonBuilderHelper.cs @@ -10,8 +10,29 @@ namespace Microsoft.NET.Sdk.WebAssembly { - public class BootJsonBuilderHelper(TaskLoggingHelper Log) + public class BootJsonBuilderHelper(TaskLoggingHelper Log, bool IsMultiThreaded) { + private static readonly string[] coreAssemblyNames = [ + "System.Private.CoreLib", + "System.Runtime.InteropServices.JavaScript", + ]; + + private static readonly string[] extraMultiThreadedCoreAssemblyName = [ + "System.Threading.Channels" + ]; + + public bool IsCoreAssembly(string fileName) + { + var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(fileName); + if (coreAssemblyNames.Contains(fileNameWithoutExtension)) + return true; + + if (IsMultiThreaded && extraMultiThreadedCoreAssemblyName.Contains(fileNameWithoutExtension)) + return true; + + return false; + } + public void ComputeResourcesHash(BootJsonData bootConfig) { var sb = new StringBuilder(); @@ -26,6 +47,7 @@ static void AddDictionary(StringBuilder sb, Dictionary? res) } AddDictionary(sb, bootConfig.resources.assembly); + AddDictionary(sb, bootConfig.resources.coreAssembly); AddDictionary(sb, bootConfig.resources.jsModuleWorker); AddDictionary(sb, bootConfig.resources.jsModuleNative); @@ -48,6 +70,12 @@ static void AddDictionary(StringBuilder sb, Dictionary? res) AddDictionary(sb, entry.Value); } + if (bootConfig.resources.coreVfs != null) + { + foreach (var entry in bootConfig.resources.coreVfs) + AddDictionary(sb, entry.Value); + } + bootConfig.resources.hash = Utils.ComputeTextIntegrity(sb.ToString()); } diff --git a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/BootJsonData.cs b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/BootJsonData.cs index 22edc2c68fbe1..8c28db9b2d4c3 100644 --- a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/BootJsonData.cs +++ b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/BootJsonData.cs @@ -152,11 +152,22 @@ public class ResourcesData [DataMember(EmitDefaultValue = false)] public ResourceHashesByNameDictionary icu { get; set; } + /// + /// "assembly" (.dll) resources needed to start MonoVM + /// + public ResourceHashesByNameDictionary coreAssembly { get; set; } = new ResourceHashesByNameDictionary(); + /// /// "assembly" (.dll) resources /// public ResourceHashesByNameDictionary assembly { get; set; } = new ResourceHashesByNameDictionary(); + /// + /// "debug" (.pdb) resources needed to start MonoVM + /// + [DataMember(EmitDefaultValue = false)] + public ResourceHashesByNameDictionary corePdb { get; set; } + /// /// "debug" (.pdb) resources /// @@ -201,6 +212,9 @@ public class ResourcesData [DataMember(EmitDefaultValue = false)] public Dictionary runtimeAssets { get; set; } + [DataMember(EmitDefaultValue = false)] + public Dictionary coreVfs { get; set; } + [DataMember(EmitDefaultValue = false)] public Dictionary vfs { get; set; } diff --git a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/GenerateWasmBootJson.cs b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/GenerateWasmBootJson.cs index d5dc83e4252df..fbe0aa7e3733e 100644 --- a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/GenerateWasmBootJson.cs +++ b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/GenerateWasmBootJson.cs @@ -22,7 +22,11 @@ namespace Microsoft.NET.Sdk.WebAssembly; public class GenerateWasmBootJson : Task { - private static readonly string[] jiterpreterOptions = new[] { "jiterpreter-traces-enabled", "jiterpreter-interp-entry-enabled", "jiterpreter-jit-call-enabled" }; + private static readonly string[] jiterpreterOptions = [ + "jiterpreter-traces-enabled", + "jiterpreter-interp-entry-enabled", + "jiterpreter-jit-call-enabled" + ]; [Required] public string AssemblyPath { get; set; } @@ -73,6 +77,10 @@ public class GenerateWasmBootJson : Task public bool IsPublish { get; set; } + public bool IsAot { get; set; } + + public bool IsMultiThreaded { get; set; } + public override bool Execute() { using var fileStream = File.Create(OutputPath); @@ -93,7 +101,7 @@ public override bool Execute() // Internal for tests public void WriteBootJson(Stream output, string entryAssemblyName) { - var helper = new BootJsonBuilderHelper(Log); + var helper = new BootJsonBuilderHelper(Log, IsMultiThreaded); var result = new BootJsonData { @@ -205,15 +213,32 @@ public void WriteBootJson(Stream output, string entryAssemblyName) } else { - Log.LogMessage(MessageImportance.Low, "Candidate '{0}' is defined as symbols file.", resource.ItemSpec); - resourceData.pdb ??= new ResourceHashesByNameDictionary(); - resourceList = resourceData.pdb; + if (IsTargeting90OrLater() && (IsAot || helper.IsCoreAssembly(resourceName))) + { + Log.LogMessage(MessageImportance.Low, "Candidate '{0}' is defined as core symbols file.", resource.ItemSpec); + resourceData.corePdb ??= new ResourceHashesByNameDictionary(); + resourceList = resourceData.corePdb; + } + else + { + Log.LogMessage(MessageImportance.Low, "Candidate '{0}' is defined as symbols file.", resource.ItemSpec); + resourceData.pdb ??= new ResourceHashesByNameDictionary(); + resourceList = resourceData.pdb; + } } } else if (string.Equals("runtime", assetTraitValue, StringComparison.OrdinalIgnoreCase)) { - Log.LogMessage(MessageImportance.Low, "Candidate '{0}' is defined as an app assembly.", resource.ItemSpec); - resourceList = resourceData.assembly; + if (IsTargeting90OrLater() && (IsAot || helper.IsCoreAssembly(resourceName))) + { + Log.LogMessage(MessageImportance.Low, "Candidate '{0}' is defined as core assembly.", resource.ItemSpec); + resourceList = resourceData.coreAssembly; + } + else + { + Log.LogMessage(MessageImportance.Low, "Candidate '{0}' is defined as an app assembly.", resource.ItemSpec); + resourceList = resourceData.assembly; + } } else if (string.Equals(assetTraitName, "WasmResource", StringComparison.OrdinalIgnoreCase) && string.Equals(assetTraitValue, "native", StringComparison.OrdinalIgnoreCase)) @@ -445,8 +470,15 @@ private bool TryGetLazyLoadedAssembly(string fileName, out ITaskItem lazyLoadedA private Version? parsedTargetFrameworkVersion; private static readonly Version version80 = new Version(8, 0); + private static readonly Version version90 = new Version(9, 0); private bool IsTargeting80OrLater() + => IsTargetingVersionOrLater(version80); + + private bool IsTargeting90OrLater() + => IsTargetingVersionOrLater(version90); + + private bool IsTargetingVersionOrLater(Version version) { if (parsedTargetFrameworkVersion == null) { @@ -457,6 +489,6 @@ private bool IsTargeting80OrLater() parsedTargetFrameworkVersion = Version.Parse(tfv); } - return parsedTargetFrameworkVersion >= version80; + return parsedTargetFrameworkVersion >= version; } } diff --git a/src/tasks/WasmAppBuilder/WasmAppBuilder.cs b/src/tasks/WasmAppBuilder/WasmAppBuilder.cs index 7847039163b11..12acecd93ce6f 100644 --- a/src/tasks/WasmAppBuilder/WasmAppBuilder.cs +++ b/src/tasks/WasmAppBuilder/WasmAppBuilder.cs @@ -31,6 +31,8 @@ public class WasmAppBuilder : WasmAppBuilderBaseTask public string? RuntimeAssetsLocation { get; set; } public bool CacheBootResources { get; set; } public int DebugLevel { get; set; } + public bool IsAot { get; set; } + public bool IsMultiThreaded { get; set; } private static readonly JsonSerializerOptions s_jsonOptions = new JsonSerializerOptions { @@ -95,7 +97,7 @@ private GlobalizationMode GetGlobalizationMode() protected override bool ExecuteInternal() { - var helper = new BootJsonBuilderHelper(Log); + var helper = new BootJsonBuilderHelper(Log, IsMultiThreaded); var logAdapter = new LogAdapter(Log); if (!ValidateArguments()) @@ -206,16 +208,30 @@ protected override bool ExecuteInternal() bytes = File.ReadAllBytes(assemblyPath); } - bootConfig.resources.assembly[Path.GetFileName(assemblyPath)] = Utils.ComputeIntegrity(bytes); + var assemblyName = Path.GetFileName(assemblyPath); + bool isCoreAssembly = IsAot || helper.IsCoreAssembly(assemblyName); + + var assemblyList = isCoreAssembly ? bootConfig.resources.coreAssembly : bootConfig.resources.assembly; + assemblyList[assemblyName] = Utils.ComputeIntegrity(bytes); + if (DebugLevel != 0) { var pdb = Path.ChangeExtension(assembly, ".pdb"); if (File.Exists(pdb)) { - if (bootConfig.resources.pdb == null) - bootConfig.resources.pdb = new(); - - bootConfig.resources.pdb[Path.GetFileName(pdb)] = Utils.ComputeIntegrity(pdb); + if (isCoreAssembly) + { + if (bootConfig.resources.corePdb == null) + bootConfig.resources.corePdb = new(); + } + else + { + if (bootConfig.resources.pdb == null) + bootConfig.resources.pdb = new(); + } + + var pdbList = isCoreAssembly ? bootConfig.resources.corePdb : bootConfig.resources.pdb; + pdbList[Path.GetFileName(pdb)] = Utils.ComputeIntegrity(pdb); } } } @@ -268,9 +284,11 @@ protected override bool ExecuteInternal() var i = 0; StringDictionary targetPathTable = new(); var vfs = new Dictionary>(); + var coreVfs = new Dictionary>(); foreach (var item in FilesToIncludeInFileSystem) { string? targetPath = item.GetMetadata("TargetPath"); + string? loadingStage = item.GetMetadata("LoadingStage"); if (string.IsNullOrEmpty(targetPath)) { targetPath = Path.GetFileName(item.ItemSpec); @@ -298,7 +316,7 @@ protected override bool ExecuteInternal() var vfsPath = Path.Combine(supportFilesDir, generatedFileName); FileCopyChecked(item.ItemSpec, vfsPath, "FilesToIncludeInFileSystem"); - vfs[targetPath] = new() + (loadingStage == "Core" ? coreVfs : vfs)[targetPath] = new() { [$"supportFiles/{generatedFileName}"] = Utils.ComputeIntegrity(vfsPath) }; @@ -306,6 +324,9 @@ protected override bool ExecuteInternal() if (vfs.Count > 0) bootConfig.resources.vfs = vfs; + + if (coreVfs.Count > 0) + bootConfig.resources.coreVfs = coreVfs; } if (!InvariantGlobalization) From e9e042be71366978421dcc780f18f3b569064c8e Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Tue, 16 Apr 2024 12:41:14 +0200 Subject: [PATCH 03/10] where ? --- .../src/System/Globalization/GlobalizationMode.Unix.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/GlobalizationMode.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/GlobalizationMode.Unix.cs index df937e56e34fe..adc0ab3a07c60 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/GlobalizationMode.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/GlobalizationMode.Unix.cs @@ -41,7 +41,7 @@ private static string GetIcuLoadFailureMessage() if (OperatingSystem.IsBrowser() || OperatingSystem.IsAndroid() || OperatingSystem.IsIOS() || OperatingSystem.IsTvOS() || OperatingSystem.IsWatchOS()) { - return "Unable to load required ICU Globalization data. Please see https://aka.ms/dotnet-missing-libicu for more information"; + return "Unable to load required ICU Globalization data. Please see https://aka.ms/dotnet-missing-libicu for more information.\n" + Environment.StackTrace; } else { From f43a0c97e6705237feba3caee223557d15686a42 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Tue, 16 Apr 2024 15:34:53 +0200 Subject: [PATCH 04/10] Fix "Unable to load required ICU Globalization dat" on MT. --- .../src/System/Globalization/CultureData.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.cs index 6c91bba74bd41..8d0c9716871bb 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.cs @@ -2256,7 +2256,11 @@ private string[] GetNativeDigits() internal void GetNFIValues(NumberFormatInfo nfi) { +#if TARGET_BROWSER + if (GlobalizationMode.InvariantFast || IsInvariantCulture) +#else if (GlobalizationMode.Invariant || IsInvariantCulture) +#endif { nfi._positiveSign = _sPositiveSign!; nfi._negativeSign = _sNegativeSign!; From ac84f0771d68e777c41750299e1c598326ef8940 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 17 Apr 2024 14:39:49 +0200 Subject: [PATCH 05/10] fix icu --- src/mono/browser/runtime/assets.ts | 4 ++-- src/mono/browser/runtime/globals.ts | 3 ++- src/mono/browser/runtime/icu.ts | 15 ++------------- src/mono/browser/runtime/loader/assets.ts | 12 ++++++++---- .../browser/runtime/pthreads/deputy-thread.ts | 19 ++++++++++++++++++- .../browser/runtime/pthreads/ui-thread.ts | 6 +++++- src/mono/browser/runtime/startup.ts | 18 +++++++++++------- src/mono/browser/runtime/types/internal.ts | 8 ++++++-- 8 files changed, 54 insertions(+), 31 deletions(-) diff --git a/src/mono/browser/runtime/assets.ts b/src/mono/browser/runtime/assets.ts index 7f844b4cb4801..f8725a36e1250 100644 --- a/src/mono/browser/runtime/assets.ts +++ b/src/mono/browser/runtime/assets.ts @@ -4,7 +4,7 @@ import type { AssetEntryInternal } from "./types/internal"; import cwraps from "./cwraps"; -import { mono_wasm_prepare_icu_data } from "./icu"; +import { mono_wasm_load_icu_data } from "./icu"; import { Module, loaderHelpers, mono_assert, runtimeHelpers } from "./globals"; import { mono_log_info, mono_log_debug, parseSymbolMapFile } from "./logging"; import { mono_wasm_load_bytes_into_heap_persistent } from "./memory"; @@ -88,7 +88,7 @@ export function instantiate_asset (asset: AssetEntry, url: string, bytes: Uint8A } else if (asset.behavior === "pdb") { cwraps.mono_wasm_add_assembly(virtualName, offset!, bytes.length); } else if (asset.behavior === "icu") { - mono_wasm_prepare_icu_data(offset!); + mono_wasm_load_icu_data(offset!); } else if (asset.behavior === "resource") { cwraps.mono_wasm_add_satellite_assembly(virtualName, asset.culture || "", offset!, bytes.length); } diff --git a/src/mono/browser/runtime/globals.ts b/src/mono/browser/runtime/globals.ts index d5e352efff785..e1eeed5ddce1d 100644 --- a/src/mono/browser/runtime/globals.ts +++ b/src/mono/browser/runtime/globals.ts @@ -65,7 +65,8 @@ export function setRuntimeGlobals (globalObjects: GlobalObjects) { afterPreInit: createPromiseController(), afterPreRun: createPromiseController(), beforeOnRuntimeInitialized: createPromiseController(), - afterMonoStarted: createPromiseController(), + afterMonoStarted: createPromiseController(), + afterDeputyReady: createPromiseController(), afterIOStarted: createPromiseController(), afterOnRuntimeInitialized: createPromiseController(), afterPostRun: createPromiseController(), diff --git a/src/mono/browser/runtime/icu.ts b/src/mono/browser/runtime/icu.ts index 955b42e0e3c78..b6c789f1138f6 100644 --- a/src/mono/browser/runtime/icu.ts +++ b/src/mono/browser/runtime/icu.ts @@ -4,19 +4,8 @@ import cwraps from "./cwraps"; import { VoidPtr } from "./types/emscripten"; - -let icuDataOffset: VoidPtr | null = null; -// @offset must be the address of an ICU data archive in the native heap. -// returns true on success. -export function mono_wasm_prepare_icu_data (offset: VoidPtr) { - icuDataOffset = offset; -} - -export function mono_wasm_load_icu_data () { - if (icuDataOffset === null) { - return; - } - if (!cwraps.mono_wasm_load_icu_data(icuDataOffset!)) { +export function mono_wasm_load_icu_data (offset: VoidPtr) { + if (!cwraps.mono_wasm_load_icu_data(offset)) { throw new Error("Failed to load ICU data"); } } diff --git a/src/mono/browser/runtime/loader/assets.ts b/src/mono/browser/runtime/loader/assets.ts index 846fb91133f34..926042d57e7dd 100644 --- a/src/mono/browser/runtime/loader/assets.ts +++ b/src/mono/browser/runtime/loader/assets.ts @@ -5,7 +5,7 @@ import WasmEnableThreads from "consts:wasmEnableThreads"; import { PThreadPtrNull, type AssetEntryInternal, type PThreadWorker, type PromiseAndController } from "../types/internal"; import type { AssetBehaviors, AssetEntry, LoadingResource, ResourceList, SingleAssetBehaviors as SingleAssetBehaviors, WebAssemblyBootResourceType } from "../types"; -import { ENVIRONMENT_IS_NODE, ENVIRONMENT_IS_SHELL, ENVIRONMENT_IS_WEB, loaderHelpers, mono_assert, runtimeHelpers } from "./globals"; +import { ENVIRONMENT_IS_NODE, ENVIRONMENT_IS_SHELL, ENVIRONMENT_IS_WEB, ENVIRONMENT_IS_WORKER, loaderHelpers, mono_assert, runtimeHelpers } from "./globals"; import { createPromiseController } from "./promise-controller"; import { mono_log_debug, mono_log_warn } from "./logging"; import { mono_exit } from "./exit"; @@ -233,15 +233,19 @@ export async function mono_download_assets (): Promise { // this await will get past the onRuntimeInitialized because we are not blocking via addRunDependency // and we are not awaiting it here Promise.all(promises_of_asset_instantiation_core).then(() => { - runtimeHelpers.coreAssetsInMemory.promise_control.resolve(); + if (!ENVIRONMENT_IS_WORKER) { + runtimeHelpers.coreAssetsInMemory.promise_control.resolve(); + } }).catch(err => { loaderHelpers.err("Error in mono_download_assets: " + err); mono_exit(1, err); throw err; }); Promise.all(promises_of_asset_instantiation_remaining).then(async () => { - await runtimeHelpers.coreAssetsInMemory.promise; - runtimeHelpers.allAssetsInMemory.promise_control.resolve(); + if (!ENVIRONMENT_IS_WORKER) { + await runtimeHelpers.coreAssetsInMemory.promise; + runtimeHelpers.allAssetsInMemory.promise_control.resolve(); + } }).catch(err => { loaderHelpers.err("Error in mono_download_assets: " + err); mono_exit(1, err); diff --git a/src/mono/browser/runtime/pthreads/deputy-thread.ts b/src/mono/browser/runtime/pthreads/deputy-thread.ts index 4b514b28a4aed..1e2041fa44dae 100644 --- a/src/mono/browser/runtime/pthreads/deputy-thread.ts +++ b/src/mono/browser/runtime/pthreads/deputy-thread.ts @@ -8,8 +8,10 @@ import { mono_log_error, mono_log_info } from "../logging"; import { monoThreadInfo, postMessageToMain, update_thread_info } from "./shared"; import { Module, loaderHelpers, runtimeHelpers } from "../globals"; import { start_runtime } from "../startup"; -import { WorkerToMainMessageType } from "../types/internal"; +import { MainToWorkerMessageType, WorkerToMainMessageType } from "../types/internal"; import { forceThreadMemoryViewRefresh } from "../memory"; +import { install_main_synchronization_context } from "../managed-exports"; +import { pthread_self } from "./worker-thread"; export function mono_wasm_start_deputy_thread_async () { if (!WasmEnableThreads) return; @@ -31,11 +33,26 @@ export function mono_wasm_start_deputy_thread_async () { try { forceThreadMemoryViewRefresh(); + pthread_self.addEventListenerFromBrowser((message) => { + if (message.data.cmd == MainToWorkerMessageType.allAssetsLoaded) { + runtimeHelpers.allAssetsInMemory.promise_control.resolve(); + } + }); + await start_runtime(); postMessageToMain({ monoCmd: WorkerToMainMessageType.deputyStarted, info: monoThreadInfo, + }); + + await runtimeHelpers.allAssetsInMemory.promise; + + runtimeHelpers.proxyGCHandle = install_main_synchronization_context(runtimeHelpers.config.jsThreadBlockingMode!); + + postMessageToMain({ + monoCmd: WorkerToMainMessageType.deputyReady, + info: monoThreadInfo, deputyProxyGCHandle: runtimeHelpers.proxyGCHandle, }); } catch (err) { diff --git a/src/mono/browser/runtime/pthreads/ui-thread.ts b/src/mono/browser/runtime/pthreads/ui-thread.ts index 0dc2b80c57e0f..94016a529424c 100644 --- a/src/mono/browser/runtime/pthreads/ui-thread.ts +++ b/src/mono/browser/runtime/pthreads/ui-thread.ts @@ -96,7 +96,11 @@ function monoWorkerMessageHandler (worker: PThreadWorker, ev: MessageEvent) worker.info = Object.assign(worker.info!, message.info, {}); break; case WorkerToMainMessageType.deputyStarted: - runtimeHelpers.afterMonoStarted.promise_control.resolve(message.deputyProxyGCHandle); + runtimeHelpers.deputyWorker = worker; + runtimeHelpers.afterMonoStarted.promise_control.resolve(); + break; + case WorkerToMainMessageType.deputyReady: + runtimeHelpers.afterDeputyReady.promise_control.resolve(message.deputyProxyGCHandle); break; case WorkerToMainMessageType.ioStarted: runtimeHelpers.afterIOStarted.promise_control.resolve(); diff --git a/src/mono/browser/runtime/startup.ts b/src/mono/browser/runtime/startup.ts index 75ab5200d68e8..4d07e712358b0 100644 --- a/src/mono/browser/runtime/startup.ts +++ b/src/mono/browser/runtime/startup.ts @@ -4,7 +4,7 @@ import WasmEnableThreads from "consts:wasmEnableThreads"; import BuildConfiguration from "consts:configuration"; -import { DotnetModuleInternal, CharPtrNull } from "./types/internal"; +import { DotnetModuleInternal, CharPtrNull, MainToWorkerMessageType } from "./types/internal"; import { exportedRuntimeAPI, INTERNAL, loaderHelpers, Module, runtimeHelpers, createPromiseController, mono_assert } from "./globals"; import cwraps, { init_c_exports, threads_c_functions as tcwraps } from "./cwraps"; import { mono_wasm_raise_debug_event, mono_wasm_runtime_ready } from "./debug"; @@ -14,7 +14,7 @@ import { initialize_marshalers_to_cs } from "./marshal-to-cs"; import { initialize_marshalers_to_js } from "./marshal-to-js"; import { init_polyfills_async } from "./polyfills"; import { strings_init, utf8ToString } from "./strings"; -import { init_managed_exports, install_main_synchronization_context } from "./managed-exports"; +import { init_managed_exports } from "./managed-exports"; import { cwraps_internal } from "./exports-internal"; import { CharPtr, InstantiateWasmCallBack, InstantiateWasmSuccessCallback } from "./types/emscripten"; import { wait_for_all_assets } from "./assets"; @@ -34,7 +34,6 @@ import { runtimeList } from "./exports"; import { nativeAbort, nativeExit } from "./run"; import { mono_wasm_init_diagnostics } from "./diagnostics"; import { replaceEmscriptenPThreadInit } from "./pthreads/worker-thread"; -import { mono_wasm_load_icu_data } from "./icu"; export async function configureRuntimeStartup (module: DotnetModuleInternal): Promise { if (!module.out) { @@ -293,7 +292,7 @@ async function onRuntimeInitializedAsync (userOnRuntimeInitialized: () => void) runtimeHelpers.managedThreadTID = tcwraps.mono_wasm_create_deputy_thread(); // await mono started on deputy thread - runtimeHelpers.proxyGCHandle = await runtimeHelpers.afterMonoStarted.promise; + await runtimeHelpers.afterMonoStarted.promise; runtimeHelpers.ioThreadTID = tcwraps.mono_wasm_create_io_thread(); // TODO make UI thread not managed/attached https://github.com/dotnet/runtime/issues/100411 @@ -319,7 +318,13 @@ async function onRuntimeInitializedAsync (userOnRuntimeInitialized: () => void) await wait_for_all_assets(); - mono_wasm_load_icu_data(); + if (WasmEnableThreads) { + runtimeHelpers.deputyWorker.thread!.postMessageToWorker({ + type:"deputyThread", + cmd: MainToWorkerMessageType.allAssetsLoaded, + }); + runtimeHelpers.proxyGCHandle = await runtimeHelpers.afterDeputyReady.promise; + } runtimeList.registerRuntime(exportedRuntimeAPI); @@ -555,12 +560,11 @@ export async function start_runtime () { monoThreadInfo.isRegistered = true; runtimeHelpers.currentThreadTID = monoThreadInfo.pthreadId = runtimeHelpers.managedThreadTID = mono_wasm_pthread_ptr(); update_thread_info(); - runtimeHelpers.proxyGCHandle = install_main_synchronization_context(runtimeHelpers.config.jsThreadBlockingMode!); runtimeHelpers.isManagedRunningOnCurrentThread = true; } // get GCHandle of the ctx - runtimeHelpers.afterMonoStarted.promise_control.resolve(runtimeHelpers.proxyGCHandle); + runtimeHelpers.afterMonoStarted.promise_control.resolve(); if (runtimeHelpers.config.interpreterPgo) { await interp_pgo_load_data(); diff --git a/src/mono/browser/runtime/types/internal.ts b/src/mono/browser/runtime/types/internal.ts index f27ad08360630..6b9f16cafa05f 100644 --- a/src/mono/browser/runtime/types/internal.ts +++ b/src/mono/browser/runtime/types/internal.ts @@ -210,6 +210,7 @@ export type RuntimeHelpers = { proxyGCHandle: GCHandle | undefined, managedThreadTID: PThreadPtr, ioThreadTID: PThreadPtr, + deputyWorker: PThreadWorker, currentThreadTID: PThreadPtr, isManagedRunningOnCurrentThread: boolean, isPendingSynchronousCall: boolean, // true when we are in the middle of a synchronous call from managed code from same thread @@ -223,7 +224,8 @@ export type RuntimeHelpers = { afterPreInit: PromiseAndController, afterPreRun: PromiseAndController, beforeOnRuntimeInitialized: PromiseAndController, - afterMonoStarted: PromiseAndController, + afterMonoStarted: PromiseAndController, + afterDeputyReady: PromiseAndController, afterIOStarted: PromiseAndController, afterOnRuntimeInitialized: PromiseAndController, afterPostRun: PromiseAndController, @@ -503,12 +505,14 @@ export const enum WorkerToMainMessageType { deputyCreated = "createdDeputy", deputyFailed = "deputyFailed", deputyStarted = "monoStarted", + deputyReady = "deputyReady", ioStarted = "ioStarted", preload = "preload", } export const enum MainToWorkerMessageType { - applyConfig = "apply_mono_config", + applyConfig = "applyConfig", + allAssetsLoaded = "allAssetsLoaded", } export interface PThreadWorker extends Worker { From cbf4829dfa94f372d5801e08f328db2cd7f78302 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Fi=C5=A1era?= Date: Mon, 13 May 2024 11:29:36 +0200 Subject: [PATCH 06/10] Feedback --- src/tasks/WasmAppBuilder/WasmAppBuilder.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/tasks/WasmAppBuilder/WasmAppBuilder.cs b/src/tasks/WasmAppBuilder/WasmAppBuilder.cs index 12acecd93ce6f..cf2357854c442 100644 --- a/src/tasks/WasmAppBuilder/WasmAppBuilder.cs +++ b/src/tasks/WasmAppBuilder/WasmAppBuilder.cs @@ -316,7 +316,13 @@ protected override bool ExecuteInternal() var vfsPath = Path.Combine(supportFilesDir, generatedFileName); FileCopyChecked(item.ItemSpec, vfsPath, "FilesToIncludeInFileSystem"); - (loadingStage == "Core" ? coreVfs : vfs)[targetPath] = new() + var vfsDict = loadingStage switch + { + null => vfs, + "Core" => coreVfs, + _ => throw new LogAsErrorException($"The WasmFilesToIncludeInFileSystem '{item.ItemSpec}' has LoadingStage set to unsupported '{loadingStage}' (empty or 'Core' is currently supported).") + }; + vfsDict[targetPath] = new() { [$"supportFiles/{generatedFileName}"] = Utils.ComputeIntegrity(vfsPath) }; From ac5ff896e21678060d5632c24d78d6ab5e5bbdb5 Mon Sep 17 00:00:00 2001 From: Pavel Savara Date: Mon, 13 May 2024 14:35:12 +0200 Subject: [PATCH 07/10] feedback --- .../src/System/Globalization/CultureData.cs | 12 ++---------- .../System/Globalization/GlobalizationMode.Unix.cs | 2 +- .../src/System/Globalization/GlobalizationMode.cs | 13 +++++++++++-- .../TrimmingTests/InvariantGlobalizationTrue.cs | 13 ++++++------- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.cs index 135280a71cb86..f16995c3d94f5 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.cs @@ -628,11 +628,7 @@ private static CultureData CreateCultureWithInvariantData() // all available calendar type(s). The first one is the default calendar invariant._waCalendars = new CalendarId[] { CalendarId.GREGORIAN }; -#if TARGET_BROWSER - if (!GlobalizationMode.InvariantFast) -#else - if (!GlobalizationMode.Invariant) -#endif + if (!GlobalizationMode.InvariantNoLoad) { // Store for specific data about each calendar invariant._calendars = new CalendarData[CalendarData.MAX_CALENDARS]; @@ -650,11 +646,7 @@ private static CultureData CreateCultureWithInvariantData() invariant._iDefaultMacCodePage = 10000; // default macintosh code page invariant._iDefaultEbcdicCodePage = 037; // default EBCDIC code page -#if TARGET_BROWSER - if (GlobalizationMode.InvariantFast) -#else - if (GlobalizationMode.Invariant) -#endif + if (GlobalizationMode.InvariantNoLoad) { invariant._sLocalizedCountry = invariant._sNativeCountry; } diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/GlobalizationMode.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/GlobalizationMode.Unix.cs index adc0ab3a07c60..df937e56e34fe 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/GlobalizationMode.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/GlobalizationMode.Unix.cs @@ -41,7 +41,7 @@ private static string GetIcuLoadFailureMessage() if (OperatingSystem.IsBrowser() || OperatingSystem.IsAndroid() || OperatingSystem.IsIOS() || OperatingSystem.IsTvOS() || OperatingSystem.IsWatchOS()) { - return "Unable to load required ICU Globalization data. Please see https://aka.ms/dotnet-missing-libicu for more information.\n" + Environment.StackTrace; + return "Unable to load required ICU Globalization data. Please see https://aka.ms/dotnet-missing-libicu for more information"; } else { diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/GlobalizationMode.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/GlobalizationMode.cs index b13312ae54a95..09673f8d507e4 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/GlobalizationMode.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/GlobalizationMode.cs @@ -26,10 +26,19 @@ private static partial class Settings // static cctor (on Unix) to be preserved when Invariant=false. internal static bool Invariant => Settings.Invariant; + // same as GlobalizationMode.Invariant but doesn't trigger ICU load in GlobalizationMode.Settings.cctor + // during runtime startup on Browser platform + internal static bool InvariantNoLoad + { + get + { #if TARGET_BROWSER - // same as GlobalizationMode.Invariant but doesn't trigger ICU load in GlobalizationMode.Settings.cctor during runtime startup - internal static bool InvariantFast { get; } = AppContextConfigHelper.GetBooleanConfig("System.Globalization.Invariant", "DOTNET_SYSTEM_GLOBALIZATION_INVARIANT"); + return AppContextConfigHelper.GetBooleanConfig("System.Globalization.Invariant", "DOTNET_SYSTEM_GLOBALIZATION_INVARIANT"); +#else + return Settings.Invariant; #endif + } + } #if TARGET_MACCATALYST || TARGET_IOS || TARGET_TVOS || TARGET_BROWSER internal static bool Hybrid => Settings.Hybrid; diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/TrimmingTests/InvariantGlobalizationTrue.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/TrimmingTests/InvariantGlobalizationTrue.cs index 56c6cda698636..27ab1b136b5a4 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/TrimmingTests/InvariantGlobalizationTrue.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/TrimmingTests/InvariantGlobalizationTrue.cs @@ -40,13 +40,12 @@ static int Main(string[] args) return 100; } - + // Ensure the internal GlobalizationMode class is trimmed correctly. Type globalizationMode = GetCoreLibType("System.Globalization.GlobalizationMode"); - if (OperatingSystem.IsWindows() || OperatingSystem.IsBrowser()) + if (OperatingSystem.IsWindows()) { - string allowedMember = OperatingSystem.IsWindows() ? "UseNls" : "InvariantFast"; foreach (MemberInfo member in globalizationMode.GetMembers(allStatics)) { // properties and their backing getter methods are OK @@ -55,8 +54,8 @@ static int Main(string[] args) continue; } - // Windows still contains a static cctor and a backing field for UseNls or InvariantFast. - if (member is ConstructorInfo || (member is FieldInfo field && field.Name.Contains(allowedMember))) + // Windows still contains a static cctor and a backing field for UseNls. + if (member is ConstructorInfo || (member is FieldInfo field && field.Name.Contains("UseNls"))) { continue; } @@ -66,10 +65,10 @@ static int Main(string[] args) return -4; } } - // On non Windows platforms, the full type is trimmed, unless it's Browser where we use FastInvariant for lazy ICU loading + // On non Windows platforms, the full type is trimmed. else if (globalizationMode is not null) { - Console.WriteLine("It is expected to have System.Globalization.GlobalizationMode type trimmed in non-Windows and non-Browser platforms"); + Console.WriteLine("It is expected to have System.Globalization.GlobalizationMode type trimmed in non-Windows platforms"); return -5; } From e1bbd27d827b198877addc68a8d5b164f9d7d632 Mon Sep 17 00:00:00 2001 From: Pavel Savara Date: Mon, 13 May 2024 14:58:19 +0200 Subject: [PATCH 08/10] whitespace --- .../src/System/Globalization/GlobalizationMode.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/GlobalizationMode.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/GlobalizationMode.cs index 09673f8d507e4..78cebb45b9a00 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/GlobalizationMode.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/GlobalizationMode.cs @@ -28,9 +28,9 @@ private static partial class Settings // same as GlobalizationMode.Invariant but doesn't trigger ICU load in GlobalizationMode.Settings.cctor // during runtime startup on Browser platform - internal static bool InvariantNoLoad - { - get + internal static bool InvariantNoLoad + { + get { #if TARGET_BROWSER return AppContextConfigHelper.GetBooleanConfig("System.Globalization.Invariant", "DOTNET_SYSTEM_GLOBALIZATION_INVARIANT"); From 80b323058134b51ee836b682853389943c29d936 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Tue, 14 May 2024 11:50:01 +0200 Subject: [PATCH 09/10] fix --- .../src/ILLink/ILLink.Substitutions.Shared.xml | 1 + .../src/System/Globalization/CultureData.cs | 6 +----- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/ILLink/ILLink.Substitutions.Shared.xml b/src/libraries/System.Private.CoreLib/src/ILLink/ILLink.Substitutions.Shared.xml index d1f5af506a247..5561632ceaaaa 100644 --- a/src/libraries/System.Private.CoreLib/src/ILLink/ILLink.Substitutions.Shared.xml +++ b/src/libraries/System.Private.CoreLib/src/ILLink/ILLink.Substitutions.Shared.xml @@ -11,6 +11,7 @@ to be trimmed when Invariant=true, and allows for the Settings static cctor (on Unix) to be preserved when Invariant=false. --> + diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.cs index f16995c3d94f5..32d284e85989c 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.cs @@ -2228,11 +2228,7 @@ private string[] GetNativeDigits() internal void GetNFIValues(NumberFormatInfo nfi) { -#if TARGET_BROWSER - if (GlobalizationMode.InvariantFast || IsInvariantCulture) -#else - if (GlobalizationMode.Invariant || IsInvariantCulture) -#endif + if (GlobalizationMode.InvariantNoLoad || IsInvariantCulture) { nfi._positiveSign = _sPositiveSign!; nfi._negativeSign = _sNegativeSign!; From 9ec70739195aa12de95aee027350c3b6fb0606d9 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Tue, 14 May 2024 12:35:41 +0200 Subject: [PATCH 10/10] fix --- src/tasks/WasmAppBuilder/WasmAppBuilder.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/tasks/WasmAppBuilder/WasmAppBuilder.cs b/src/tasks/WasmAppBuilder/WasmAppBuilder.cs index cf2357854c442..fa5cf03f9a560 100644 --- a/src/tasks/WasmAppBuilder/WasmAppBuilder.cs +++ b/src/tasks/WasmAppBuilder/WasmAppBuilder.cs @@ -319,6 +319,7 @@ protected override bool ExecuteInternal() var vfsDict = loadingStage switch { null => vfs, + "" => vfs, "Core" => coreVfs, _ => throw new LogAsErrorException($"The WasmFilesToIncludeInFileSystem '{item.ItemSpec}' has LoadingStage set to unsupported '{loadingStage}' (empty or 'Core' is currently supported).") };