diff --git a/src/mono/mono/mini/interp/jiterpreter.c b/src/mono/mono/mini/interp/jiterpreter.c
index cff40a400384b..2f867757c96a9 100644
--- a/src/mono/mono/mini/interp/jiterpreter.c
+++ b/src/mono/mono/mini/interp/jiterpreter.c
@@ -1075,6 +1075,9 @@ mono_jiterp_update_jit_call_dispatcher (WasmDoJitCall dispatcher)
// blocked the use of Module.addFunction
if (!dispatcher)
dispatcher = (WasmDoJitCall)mono_llvm_cpp_catch_exception;
+ else if (((int)(void*)dispatcher)==-1)
+ dispatcher = mono_jiterp_do_jit_call_indirect;
+
jiterpreter_do_jit_call = dispatcher;
}
diff --git a/src/mono/sample/wasm/browser-advanced/index.html b/src/mono/sample/wasm/browser-advanced/index.html
index b4cefe75f4072..0532ef8fcef32 100644
--- a/src/mono/sample/wasm/browser-advanced/index.html
+++ b/src/mono/sample/wasm/browser-advanced/index.html
@@ -11,6 +11,7 @@
+
diff --git a/src/mono/sample/wasm/browser-bench/appstart-frame.html b/src/mono/sample/wasm/browser-bench/appstart-frame.html
index 1582a356568d8..b54590f423895 100644
--- a/src/mono/sample/wasm/browser-bench/appstart-frame.html
+++ b/src/mono/sample/wasm/browser-bench/appstart-frame.html
@@ -11,6 +11,7 @@
+
diff --git a/src/mono/wasm/build/WasmApp.targets b/src/mono/wasm/build/WasmApp.targets
index df0858f237e10..b5ab71e762566 100644
--- a/src/mono/wasm/build/WasmApp.targets
+++ b/src/mono/wasm/build/WasmApp.targets
@@ -364,7 +364,7 @@
<_WasmAppIncludeThreadsWorker Condition="'$(WasmEnableThreads)' == 'true' or '$(WasmEnablePerfTracing)' == 'true'">true
- <_WasmPThreadPoolSize Condition="'$(_WasmPThreadPoolSize)' == '' and ('$(WasmEnableThreads)' == 'true' or '$(WasmEnablePerfTracing)' == 'true')">-1
+ <_WasmPThreadPoolSize Condition="'$(_WasmPThreadPoolSize)' == ''">-1
diff --git a/src/mono/wasm/memory-snapshot.md b/src/mono/wasm/memory-snapshot.md
new file mode 100644
index 0000000000000..d9d392555cb10
--- /dev/null
+++ b/src/mono/wasm/memory-snapshot.md
@@ -0,0 +1,27 @@
+# Memory snapshot of the Mono runtime
+
+We take snapshot of WASM memory after first cold start at run time.
+We store it on the client side in the browser cache.
+For subsequent runs with the same configuration and same assets, we use the snapshot
+instead of downloading everything again and doing the runtime startup again.
+These subsequent starts are significantly faster.
+
+### Implementation details
+
+- the consistency of inputs (configuration and assets) with the snapshot is done by calculating SHA256 of the inputs.
+ - the DLLs and other downloaded assets each have SHA256 which is used to validate 'integrity' by the browser.
+ - the mono-config has field `assetsHash` which is summary SHA256 of all the assets.
+ - the configuration could be changed programmatically and so we calculate the hash at the runtime, just before taking snapshot.
+- the snapshot is taken just after we initialize the Mono runtime.
+ - before cwraps are initialized (they would be initialized again on subsequent start).
+ - after loading all the DLLs into memory.
+ - after loading ICU and timezone data.
+ - after applying environment variables and other runtime options.
+ - before any worker threads initialization.
+ - before any JavaScript interop initialization.
+ - before any Managed code executes.
+ - therefore we do not expect to store any application state in the snapshot.
+
+### How to opt out
+You can turn this feature of by calling `withStartupMemoryCache (false)` on [dotnet API](https://github.com/dotnet/runtime/blob/main/src/mono/wasm/runtime/dotnet.d.ts).
+
diff --git a/src/mono/wasm/runtime/assets.ts b/src/mono/wasm/runtime/assets.ts
index d3ae9456bd3e8..2852e1f4d463b 100644
--- a/src/mono/wasm/runtime/assets.ts
+++ b/src/mono/wasm/runtime/assets.ts
@@ -8,7 +8,7 @@ import { mono_wasm_load_bytes_into_heap } from "./memory";
import { endMeasure, MeasuredBlock, startMeasure } from "./profiler";
import { createPromiseController, PromiseAndController } from "./promise-controller";
import { delay } from "./promise-utils";
-import { abort_startup, beforeOnRuntimeInitialized } from "./startup";
+import { abort_startup, beforeOnRuntimeInitialized, memorySnapshotSkippedOrDone } from "./startup";
import { AssetBehaviours, AssetEntry, AssetEntryInternal, LoadingResource, mono_assert, ResourceRequest } from "./types";
import { InstantiateWasmSuccessCallback, VoidPtr } from "./types/emscripten";
@@ -38,6 +38,19 @@ const skipBufferByAssetTypes: {
"dotnetwasm": true,
};
+const containedInSnapshotByAssetTypes: {
+ [k: string]: boolean
+} = {
+ "resource": true,
+ "assembly": true,
+ "pdb": true,
+ "heap": true,
+ "icu": true,
+ "js-module-threads": true,
+ "dotnetwasm": true,
+};
+
+
// these assets are instantiated differently than the main flow
const skipInstantiateByAssetTypes: {
[k: string]: boolean
@@ -63,8 +76,10 @@ export async function mono_download_assets(): Promise {
runtimeHelpers.maxParallelDownloads = runtimeHelpers.config.maxParallelDownloads || runtimeHelpers.maxParallelDownloads;
runtimeHelpers.enableDownloadRetry = runtimeHelpers.config.enableDownloadRetry || runtimeHelpers.enableDownloadRetry;
try {
+ const alwaysLoadedAssets: AssetEntryInternal[] = [];
+ const containedInSnapshotAssets: AssetEntryInternal[] = [];
const promises_of_assets: Promise[] = [];
- // start fetching and instantiating all assets in parallel
+
for (const a of runtimeHelpers.config.assets!) {
const asset: AssetEntryInternal = a;
mono_assert(typeof asset === "object", "asset must be object");
@@ -73,6 +88,14 @@ export async function mono_download_assets(): Promise {
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");
+ if (containedInSnapshotByAssetTypes[asset.behavior]) {
+ containedInSnapshotAssets.push(asset);
+ } else {
+ alwaysLoadedAssets.push(asset);
+ }
+ }
+
+ const countAndStartDownload = (asset: AssetEntryInternal) => {
if (!skipInstantiateByAssetTypes[asset.behavior] && shouldLoadIcuAsset(asset)) {
expected_instantiated_assets_count++;
}
@@ -80,7 +103,36 @@ export async function mono_download_assets(): Promise {
expected_downloaded_assets_count++;
promises_of_assets.push(start_asset_download(asset));
}
+ };
+
+ // start fetching assets in parallel, only assets which are not part of memory snapshot
+ for (const asset of alwaysLoadedAssets) {
+ countAndStartDownload(asset);
+ }
+
+ // continue after we know if memory snapshot is available or not
+ await memorySnapshotSkippedOrDone.promise;
+
+ // start fetching assets in parallel, only if memory snapshot is not available.
+ for (const asset of containedInSnapshotAssets) {
+ if (!runtimeHelpers.loadedMemorySnapshot) {
+ countAndStartDownload(asset);
+ } else {
+ // Otherwise cleanup in case we were given pending download. It would be even better if we could abort the download.
+ asset.pendingDownloadInternal = null as any; // GC
+ asset.pendingDownload = null as any; // GC
+ asset.buffer = null as any; // GC
+ // tell the debugger it is loaded
+ if (asset.behavior == "resource" || asset.behavior == "assembly" || asset.behavior == "pdb") {
+ const url = resolve_path(asset, "");
+ const virtualName: string = typeof (asset.virtualPath) === "string"
+ ? asset.virtualPath
+ : asset.name;
+ loaded_files.push({ url: url, file: virtualName });
+ }
+ }
}
+
allDownloadsQueued.promise_control.resolve();
const promises_of_asset_instantiation: Promise[] = [];
@@ -96,8 +148,9 @@ export async function mono_download_assets(): Promise {
asset.pendingDownload = null as any; // GC
asset.buffer = null as any; // GC
+ // wait till after onRuntimeInitialized and after memory snapshot is loaded or skipped
+ await memorySnapshotSkippedOrDone.promise;
await beforeOnRuntimeInitialized.promise;
- // this is after onRuntimeInitialized
_instantiate_asset(asset, url, data);
}
} else {
diff --git a/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts b/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts
index 01587e0bc8511..4f757d092970c 100644
--- a/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts
+++ b/src/mono/wasm/runtime/diagnostics/server_pthread/index.ts
@@ -230,7 +230,7 @@ class DiagnosticServerImpl implements DiagnosticServer {
}
async stopEventPipe(ws: WebSocket | MockRemoteSocket, sessionID: EventPipeSessionIDImpl): Promise {
- console.info("MONO_WASM: stopEventPipe", sessionID);
+ console.debug("MONO_WASM: stopEventPipe", sessionID);
cwraps.mono_wasm_event_pipe_session_disable(sessionID);
// we might send OK before the session is actually stopped since the websocket is async
// but the client end should be robust to that.
@@ -266,7 +266,7 @@ class DiagnosticServerImpl implements DiagnosticServer {
resumeRuntime(): void {
if (!this.runtimeResumed) {
- console.info("MONO_WASM: resuming runtime startup");
+ console.debug("MONO_WASM: resuming runtime startup");
cwraps.mono_wasm_diagnostic_server_post_resume_runtime();
this.runtimeResumed = true;
}
diff --git a/src/mono/wasm/runtime/dotnet.d.ts b/src/mono/wasm/runtime/dotnet.d.ts
index 4e3b4610accff..d80c3a4fb72c5 100644
--- a/src/mono/wasm/runtime/dotnet.d.ts
+++ b/src/mono/wasm/runtime/dotnet.d.ts
@@ -18,6 +18,7 @@ interface DotnetHostBuilder {
withDebugging(level: number): DotnetHostBuilder;
withMainAssembly(mainAssemblyName: string): DotnetHostBuilder;
withApplicationArgumentsFromQuery(): DotnetHostBuilder;
+ withStartupMemoryCache(value: boolean): DotnetHostBuilder;
create(): Promise;
run(): Promise;
}
@@ -38,6 +39,7 @@ declare interface EmscriptenModule {
HEAP8: Int8Array;
HEAP16: Int16Array;
HEAP32: Int32Array;
+ HEAP64: BigInt64Array;
HEAPU8: Uint8Array;
HEAPU16: Uint16Array;
HEAPU32: Uint32Array;
@@ -128,6 +130,10 @@ type MonoConfig = {
* initial number of workers to add to the emscripten pthread pool
*/
pthreadPoolSize?: number;
+ /**
+ * If true, the snapshot of runtime's memory will be stored in the browser and used for faster startup next time. Default is true.
+ */
+ startupMemoryCache?: boolean;
/**
* hash of assets
*/
diff --git a/src/mono/wasm/runtime/icu.ts b/src/mono/wasm/runtime/icu.ts
index f362ec82861da..5175453f34167 100644
--- a/src/mono/wasm/runtime/icu.ts
+++ b/src/mono/wasm/runtime/icu.ts
@@ -5,53 +5,50 @@ import cwraps from "./cwraps";
import { Module, runtimeHelpers } from "./imports";
import { VoidPtr } from "./types/emscripten";
-let num_icu_assets_loaded_successfully = 0;
-
// @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 {
- const ok = (cwraps.mono_wasm_load_icu_data(offset)) === 1;
- if (ok)
- num_icu_assets_loaded_successfully++;
- return ok;
+ return (cwraps.mono_wasm_load_icu_data(offset)) === 1;
}
-// Performs setup for globalization.
-// @globalizationMode is one of "icu", "invariant", or "auto".
-// "auto" will use "icu" if any ICU data archives have been loaded,
-// otherwise "invariant".
-export function mono_wasm_globalization_init(): void {
- const config = runtimeHelpers.config;
- let invariantMode = false;
- if (!config.globalizationMode)
- config.globalizationMode = "auto";
- if (config.globalizationMode === "invariant")
- invariantMode = true;
+export function init_globalization() {
+ runtimeHelpers.invariantMode = runtimeHelpers.config.globalizationMode === "invariant";
+ runtimeHelpers.preferredIcuAsset = get_preferred_icu_asset();
- if (!invariantMode) {
- if (num_icu_assets_loaded_successfully > 0) {
- if (runtimeHelpers.diagnosticTracing) {
- console.debug("MONO_WASM: ICU data archive(s) loaded, disabling invariant mode");
- }
- } else if (config.globalizationMode !== "icu") {
- if (runtimeHelpers.diagnosticTracing) {
- console.debug("MONO_WASM: ICU data archive(s) not loaded, using invariant globalization mode");
- }
- invariantMode = true;
+ if (!runtimeHelpers.invariantMode) {
+ if (runtimeHelpers.preferredIcuAsset) {
+ if (runtimeHelpers.diagnosticTracing) console.debug("MONO_WASM: ICU data archive(s) available, disabling invariant mode");
+ } else if (runtimeHelpers.config.globalizationMode !== "icu") {
+ if (runtimeHelpers.diagnosticTracing) console.debug("MONO_WASM: ICU data archive(s) not available, using invariant globalization mode");
+ runtimeHelpers.invariantMode = true;
+ runtimeHelpers.preferredIcuAsset = null;
} else {
- const msg = "invariant globalization mode is inactive and no ICU data archives were loaded";
+ const msg = "invariant globalization mode is inactive and no ICU data archives are available";
Module.err(`MONO_WASM: ERROR: ${msg}`);
throw new Error(msg);
}
}
- if (invariantMode) {
- cwraps.mono_wasm_setenv("DOTNET_SYSTEM_GLOBALIZATION_INVARIANT", "1");
+ const invariantEnv = "DOTNET_SYSTEM_GLOBALIZATION_INVARIANT";
+ const env_variables = runtimeHelpers.config.environmentVariables!;
+ if (env_variables[invariantEnv] === undefined && runtimeHelpers.invariantMode) {
+ env_variables[invariantEnv] = "1";
+ }
+ if (env_variables["TZ"] === undefined) {
+ try {
+ // this call is relatively expensive, so we call it during download of other assets
+ const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || null;
+ if (timezone) {
+ env_variables!["TZ"] = timezone;
+ }
+ } catch {
+ console.info("MONO_WASM: failed to detect timezone, will fallback to UTC");
+ }
}
}
export function get_preferred_icu_asset(): string | null {
- if (!runtimeHelpers.config.assets)
+ if (!runtimeHelpers.config.assets || runtimeHelpers.invariantMode)
return null;
// By setting user can define what ICU source file they want to load.
diff --git a/src/mono/wasm/runtime/jiterpreter-jit-call.ts b/src/mono/wasm/runtime/jiterpreter-jit-call.ts
index 28ca9db7c30ff..e84fc620da4ff 100644
--- a/src/mono/wasm/runtime/jiterpreter-jit-call.ts
+++ b/src/mono/wasm/runtime/jiterpreter-jit-call.ts
@@ -3,7 +3,7 @@
import { mono_assert, MonoType, MonoMethod } from "./types";
import { NativePointer, Int32Ptr, VoidPtr } from "./types/emscripten";
-import { Module } from "./imports";
+import { Module, runtimeHelpers } from "./imports";
import {
getU8, getI32_unaligned, getU32_unaligned, setU32_unchecked
} from "./memory";
@@ -256,6 +256,7 @@ function getIsWasmEhSupported () : boolean {
export function mono_jiterp_do_jit_call_indirect (
jit_call_cb: number, cb_data: VoidPtr, thrown: Int32Ptr
) : void {
+ mono_assert(!runtimeHelpers.storeMemorySnapshotPending, "Attempting to set function into table during creation of memory snapshot");
const table = getWasmFunctionTable();
const jitCallCb = table.get(jit_call_cb);
diff --git a/src/mono/wasm/runtime/jiterpreter-support.ts b/src/mono/wasm/runtime/jiterpreter-support.ts
index 1a49c4eeee627..c7d4524b5573f 100644
--- a/src/mono/wasm/runtime/jiterpreter-support.ts
+++ b/src/mono/wasm/runtime/jiterpreter-support.ts
@@ -3,7 +3,7 @@
import { mono_assert } from "./types";
import { NativePointer, ManagedPointer, VoidPtr } from "./types/emscripten";
-import { Module } from "./imports";
+import { Module, runtimeHelpers } from "./imports";
import { WasmOpcode } from "./jiterpreter-opcodes";
import cwraps from "./cwraps";
@@ -1201,8 +1201,9 @@ export function getWasmFunctionTable () {
}
export function addWasmFunctionPointer (f: Function) {
- if (!f)
- throw new Error("Attempting to set null function into table");
+ mono_assert(f,"Attempting to set null function into table");
+ mono_assert(!runtimeHelpers.storeMemorySnapshotPending, "Attempting to set function into table during creation of memory snapshot");
+
const table = getWasmFunctionTable();
if (wasmFunctionIndicesFree <= 0) {
wasmNextFunctionIndex = table.length;
diff --git a/src/mono/wasm/runtime/jiterpreter.ts b/src/mono/wasm/runtime/jiterpreter.ts
index 58f50804a1d05..c5cb2e636bc89 100644
--- a/src/mono/wasm/runtime/jiterpreter.ts
+++ b/src/mono/wasm/runtime/jiterpreter.ts
@@ -3,7 +3,7 @@
import { mono_assert, MonoMethod } from "./types";
import { NativePointer } from "./types/emscripten";
-import { Module } from "./imports";
+import { Module, runtimeHelpers } from "./imports";
import {
getU16, getU32_unaligned
} from "./memory";
@@ -762,6 +762,8 @@ function generate_wasm (
// independently jitting traces will not stomp on each other and all threads
// have a globally consistent view of which function pointer maps to each trace.
rejected = false;
+ mono_assert(!runtimeHelpers.storeMemorySnapshotPending, "Attempting to set function into table during creation of memory snapshot");
+
const idx =
trapTraceErrors
? Module.addFunction(
diff --git a/src/mono/wasm/runtime/polyfills.ts b/src/mono/wasm/runtime/polyfills.ts
index 78fdd1f345bbf..edbc423123d9b 100644
--- a/src/mono/wasm/runtime/polyfills.ts
+++ b/src/mono/wasm/runtime/polyfills.ts
@@ -215,6 +215,7 @@ export async function init_polyfills_async(): Promise {
}
}
}
+ runtimeHelpers.subtle = globalThis.crypto?.subtle;
}
const dummyPerformance = {
diff --git a/src/mono/wasm/runtime/profiler.ts b/src/mono/wasm/runtime/profiler.ts
index 1948fc85a19b5..489cd4ff86856 100644
--- a/src/mono/wasm/runtime/profiler.ts
+++ b/src/mono/wasm/runtime/profiler.ts
@@ -41,6 +41,7 @@ export const enum MeasuredBlock {
preRunWorker = "mono.preRunWorker",
onRuntimeInitialized = "mono.onRuntimeInitialized",
postRun = "mono.postRun",
+ memorySnapshot = "mono.memorySnapshot",
loadRuntime = "mono.loadRuntime",
bindingsInit = "mono.bindingsInit",
bindJsFunction = "mono.bindJsFunction:",
diff --git a/src/mono/wasm/runtime/run-outer.ts b/src/mono/wasm/runtime/run-outer.ts
index 7f6902e2d9712..c49317523403a 100644
--- a/src/mono/wasm/runtime/run-outer.ts
+++ b/src/mono/wasm/runtime/run-outer.ts
@@ -18,6 +18,7 @@ export interface DotnetHostBuilder {
withDebugging(level: number): DotnetHostBuilder
withMainAssembly(mainAssemblyName: string): DotnetHostBuilder
withApplicationArgumentsFromQuery(): DotnetHostBuilder
+ withStartupMemoryCache(value: boolean): DotnetHostBuilder
create(): Promise
run(): Promise
}
@@ -137,6 +138,19 @@ class HostBuilder implements DotnetHostBuilder {
}
}
+ withStartupMemoryCache(value: boolean): DotnetHostBuilder {
+ try {
+ const configInternal: MonoConfigInternal = {
+ startupMemoryCache: value
+ };
+ Object.assign(this.moduleConfig.config!, configInternal);
+ return this;
+ } catch (err) {
+ mono_exit(1, err);
+ throw err;
+ }
+ }
+
withConfig(config: MonoConfig): DotnetHostBuilder {
try {
const providedConfig = { ...config };
diff --git a/src/mono/wasm/runtime/run.ts b/src/mono/wasm/runtime/run.ts
index 7fc3729896638..27d1a65fd6fb6 100644
--- a/src/mono/wasm/runtime/run.ts
+++ b/src/mono/wasm/runtime/run.ts
@@ -118,8 +118,10 @@ function set_exit_code_and_quit_now(exit_code: number, reason?: any): void {
Module.err(JSON.stringify(reason));
}
}
- else {
+ else if (!reason) {
reason = new runtimeHelpers.ExitStatus(exit_code);
+ } else if (typeof reason.status === "number") {
+ exit_code = reason.status;
}
}
logErrorOnExit(exit_code, reason);
diff --git a/src/mono/wasm/runtime/snapshot.ts b/src/mono/wasm/runtime/snapshot.ts
new file mode 100644
index 0000000000000..eb96dec46c561
--- /dev/null
+++ b/src/mono/wasm/runtime/snapshot.ts
@@ -0,0 +1,188 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+import ProductVersion from "consts:productVersion";
+import GitHash from "consts:gitHash";
+import MonoWasmThreads from "consts:monoWasmThreads";
+import { runtimeHelpers } from "./imports";
+
+const memoryPrefix = "https://dotnet.generated.invalid/wasm-memory";
+
+// adapted from Blazor's WebAssemblyResourceLoader.ts
+async function openCache(): Promise {
+ // caches will be undefined if we're running on an insecure origin (secure means https or localhost)
+ if (typeof globalThis.caches === "undefined") {
+ return null;
+ }
+
+ // cache integrity is compromised if the first request has been served over http (except localhost)
+ // in this case, we want to disable caching and integrity validation
+ if (ENVIRONMENT_IS_WEB && globalThis.window.isSecureContext === false) {
+ return null;
+ }
+
+ // Define a separate cache for each base href, so we're isolated from any other
+ // Blazor application running on the same origin. We need this so that we're free
+ // to purge from the cache anything we're not using and don't let it keep growing,
+ // since we don't want to be worst offenders for space usage.
+ const relativeBaseHref = document.baseURI.substring(document.location.origin.length);
+ const cacheName = `dotnet-resources${relativeBaseHref}`;
+
+ try {
+ // There's a Chromium bug we need to be aware of here: the CacheStorage APIs say that when
+ // caches.open(name) returns a promise that succeeds, the value is meant to be a Cache instance.
+ // However, if the browser was launched with a --user-data-dir param that's "too long" in some sense,
+ // then even through the promise resolves as success, the value given is `undefined`.
+ // See https://stackoverflow.com/a/46626574 and https://bugs.chromium.org/p/chromium/issues/detail?id=1054541
+ // If we see this happening, return "null" to mean "proceed without caching".
+ return (await globalThis.caches.open(cacheName)) || null;
+ } catch {
+ // There's no known scenario where we should get an exception here, but considering the
+ // Chromium bug above, let's tolerate it and treat as "proceed without caching".
+ console.warn("MONO_WASM: Failed to open cache");
+ return null;
+ }
+}
+
+export async function getMemorySnapshotSize(): Promise {
+ try {
+ const cacheKey = await getCacheKey();
+ if (!cacheKey) {
+ return undefined;
+ }
+ const cache = await openCache();
+ if (!cache) {
+ return undefined;
+ }
+ const res = await cache.match(cacheKey);
+ const contentLength = res?.headers.get("content-length");
+ return contentLength ? parseInt(contentLength) : undefined;
+ } catch (ex) {
+ console.warn("MONO_WASM: Failed find memory snapshot in the cache", ex);
+ return undefined;
+ }
+}
+
+export async function getMemorySnapshot(): Promise {
+ try {
+ const cacheKey = await getCacheKey();
+ if (!cacheKey) {
+ return undefined;
+ }
+ const cache = await openCache();
+ if (!cache) {
+ return undefined;
+ }
+ const res = await cache.match(cacheKey);
+ if (!res) {
+ return undefined;
+ }
+ return res.arrayBuffer();
+ } catch (ex) {
+ console.warn("MONO_WASM: Failed load memory snapshot from the cache", ex);
+ return undefined;
+ }
+}
+
+export async function storeMemorySnapshot(memory: ArrayBuffer) {
+ try {
+ const cacheKey = await getCacheKey();
+ if (!cacheKey) {
+ return;
+ }
+ const cache = await openCache();
+ if (!cache) {
+ return;
+ }
+ const copy = MonoWasmThreads
+ // storing SHaredArrayBuffer in the cache is not working
+ ? (new Int8Array(memory)).slice(0)
+ : memory;
+
+ const responseToCache = new Response(copy, {
+ headers: {
+ "content-type": "wasm-memory",
+ "content-length": memory.byteLength.toString(),
+ },
+ });
+
+ await cache.put(cacheKey, responseToCache);
+
+ cleanupMemorySnapshots(cacheKey); // no await
+ } catch (ex) {
+ console.warn("MONO_WASM: Failed to store memory snapshot in the cache", ex);
+ return;
+ }
+}
+
+export async function cleanupMemorySnapshots(protectKey: string) {
+ try {
+ const cache = await openCache();
+ if (!cache) {
+ return;
+ }
+ const items = await cache.keys();
+ for (const item of items) {
+ if (item.url && item.url !== protectKey && item.url.startsWith(memoryPrefix)) {
+ await cache.delete(item);
+ }
+ }
+ } catch (ex) {
+ return;
+ }
+}
+
+// calculate hash of things which affect the memory snapshot
+async function getCacheKey(): Promise {
+ if (runtimeHelpers.memorySnapshotCacheKey) {
+ return runtimeHelpers.memorySnapshotCacheKey;
+ }
+ if (!runtimeHelpers.subtle) {
+ return null;
+ }
+ const inputs = Object.assign({}, runtimeHelpers.config) as any;
+ // above already has env variables, runtime options, etc
+
+ if (!inputs.assetsHash) {
+ // this is fallback for blazor which does not have assetsHash yet
+ inputs.assetsHash = [];
+ for (const asset of inputs.assets) {
+ if (!asset.hash) {
+ // if we don't have hash, we can't use the cache
+ return null;
+ }
+ inputs.assetsHash.push(asset.hash);
+ }
+ }
+ // otherwise config.assetsHash already has hashes for all the assets (DLLs, ICU, .wasms, etc).
+
+ // Now we remove assets collection from the hash.
+ delete inputs.assets;
+ // some things are calculated at runtime, so we need to add them to the hash
+ inputs.preferredIcuAsset = runtimeHelpers.preferredIcuAsset;
+ // timezone is part of env variables, so it is already in the hash
+
+ // some things are not relevant for memory snapshot
+ delete inputs.forwardConsoleLogsToWS;
+ delete inputs.diagnosticTracing;
+ delete inputs.appendElementOnExit;
+ delete inputs.logExitCode;
+ delete inputs.pthreadPoolSize;
+ delete inputs.asyncFlushOnExit;
+ delete inputs.assemblyRootFolder;
+ delete inputs.remoteSources;
+ delete inputs.ignorePdbLoadErrors;
+ delete inputs.maxParallelDownloads;
+ delete inputs.enableDownloadRetry;
+ delete inputs.exitAfterSnapshot;
+
+ inputs.GitHash = GitHash;
+ inputs.ProductVersion = ProductVersion;
+
+ const inputsJson = JSON.stringify(inputs);
+ const sha256Buffer = await runtimeHelpers.subtle.digest("SHA-256", new TextEncoder().encode(inputsJson));
+ const uint8ViewOfHash = new Uint8Array(sha256Buffer);
+ const hashAsString = Array.from(uint8ViewOfHash).map((b) => b.toString(16).padStart(2, "0")).join("");
+ runtimeHelpers.memorySnapshotCacheKey = `${memoryPrefix}-${hashAsString}`;
+ return runtimeHelpers.memorySnapshotCacheKey;
+}
diff --git a/src/mono/wasm/runtime/startup.ts b/src/mono/wasm/runtime/startup.ts
index 482c967bec26a..970a8408f3eea 100644
--- a/src/mono/wasm/runtime/startup.ts
+++ b/src/mono/wasm/runtime/startup.ts
@@ -4,11 +4,10 @@
import BuildConfiguration from "consts:configuration";
import MonoWasmThreads from "consts:monoWasmThreads";
import WasmEnableLegacyJsInterop from "consts:WasmEnableLegacyJsInterop";
-import { CharPtrNull, DotnetModule, RuntimeAPI, MonoConfig, MonoConfigInternal, DotnetModuleInternal } from "./types";
+import { CharPtrNull, DotnetModule, RuntimeAPI, MonoConfig, MonoConfigInternal, DotnetModuleInternal, mono_assert } from "./types";
import { ENVIRONMENT_IS_NODE, ENVIRONMENT_IS_SHELL, INTERNAL, Module, runtimeHelpers } from "./imports";
import cwraps, { init_c_exports } from "./cwraps";
import { mono_wasm_raise_debug_event, mono_wasm_runtime_ready } from "./debug";
-import { get_preferred_icu_asset, mono_wasm_globalization_init } from "./icu";
import { toBase64StringImpl } from "./base64";
import { mono_wasm_init_aot_profiler, mono_wasm_init_browser_profiler } from "./profiler";
import { mono_on_abort, mono_exit } from "./run";
@@ -27,16 +26,19 @@ import { mono_wasm_init_diagnostics } from "./diagnostics";
import { preAllocatePThreadWorkerPool, instantiateWasmPThreadWorkerPool } from "./pthreads/browser";
import { export_linker } from "./exports-linker";
import { endMeasure, MeasuredBlock, startMeasure } from "./profiler";
+import { getMemorySnapshot, storeMemorySnapshot, getMemorySnapshotSize } from "./snapshot";
// legacy
import { init_legacy_exports } from "./net6-legacy/corebindings";
import { cwraps_binding_api, cwraps_mono_api } from "./net6-legacy/exports-legacy";
import { BINDING, MONO } from "./net6-legacy/imports";
+import { init_globalization } from "./icu";
let config: MonoConfigInternal = undefined as any;
let configLoaded = false;
export const dotnetReady = createPromiseController();
export const afterConfigLoaded = createPromiseController();
+export const memorySnapshotSkippedOrDone = createPromiseController();
export const afterInstantiateWasm = createPromiseController();
export const beforePreInit = createPromiseController();
export const afterPreInit = createPromiseController();
@@ -106,6 +108,9 @@ function instantiateWasm(
const mark = startMeasure();
if (userInstantiateWasm) {
+ init_globalization();
+ // user wasm instantiation doesn't support memory snapshots
+ memorySnapshotSkippedOrDone.promise_control.resolve();
const exports = userInstantiateWasm(imports, (instance: WebAssembly.Instance, module: WebAssembly.Module | undefined) => {
endMeasure(mark, MeasuredBlock.instantiateWasm);
afterInstantiateWasm.promise_control.resolve();
@@ -154,9 +159,10 @@ function preInit(userPreInit: (() => void)[]) {
// It will block emscripten `userOnRuntimeInitialized` by pending addRunDependency("mono_pre_init")
(async () => {
try {
+ // - init the rest of the polyfills
+ // - download Module.config from configSrc
await mono_wasm_pre_init_essential_async();
- // - download Module.config from configSrc
// - start download assets like DLLs
await mono_wasm_pre_init_full();
@@ -232,10 +238,30 @@ async function onRuntimeInitializedAsync(userOnRuntimeInitialized: () => void) {
try {
await wait_for_all_assets();
+ // Diagnostics early are not supported with memory snapshot. See below how we enable them later.
+ // Please disable startupMemoryCache in order to be able to diagnose or pause runtime startup.
+ if (MonoWasmThreads && !config.startupMemoryCache) {
+ await mono_wasm_init_diagnostics();
+ }
+
// load runtime and apply environment settings (if necessary)
- await mono_wasm_before_user_runtime_initialized();
+ await mono_wasm_before_memory_snapshot();
+
+ if (config.exitAfterSnapshot) {
+ const reason = runtimeHelpers.ExitStatus
+ ? new runtimeHelpers.ExitStatus(0)
+ : new Error("Snapshot taken, exiting because exitAfterSnapshot was set.");
+ reason.silent = true;
+
+ abort_startup(reason, false);
+ return;
+ }
if (MonoWasmThreads) {
+ if (config.startupMemoryCache) {
+ // we could enable diagnostics after the snapshot is taken
+ await mono_wasm_init_diagnostics();
+ }
await instantiateWasmPThreadWorkerPool();
}
@@ -295,6 +321,7 @@ async function postRunAsync(userpostRun: (() => void)[]) {
export function abort_startup(reason: any, should_exit: boolean): void {
if (runtimeHelpers.diagnosticTracing) console.trace("MONO_WASM: abort_startup");
dotnetReady.promise_control.reject(reason);
+ memorySnapshotSkippedOrDone.promise_control.reject(reason);
afterInstantiateWasm.promise_control.reject(reason);
beforePreInit.promise_control.reject(reason);
afterPreInit.promise_control.reject(reason);
@@ -302,7 +329,7 @@ export function abort_startup(reason: any, should_exit: boolean): void {
beforeOnRuntimeInitialized.promise_control.reject(reason);
afterOnRuntimeInitialized.promise_control.reject(reason);
afterPostRun.promise_control.reject(reason);
- if (should_exit) {
+ if (should_exit && (typeof reason !== "object" || reason.silent !== true)) {
mono_exit(1, reason);
}
}
@@ -392,8 +419,11 @@ async function mono_wasm_after_user_runtime_initialized(): Promise {
function _print_error(message: string, err: any): void {
+ if (typeof err === "object" && err.silent) {
+ return;
+ }
Module.err(`${message}: ${JSON.stringify(err)}`);
- if (err.stack) {
+ if (typeof err === "object" && err.stack) {
Module.err("MONO_WASM: Stacktrace: \n");
Module.err(err.stack);
}
@@ -443,11 +473,27 @@ async function instantiate_wasm_module(
): Promise {
// this is called so early that even Module exports like addRunDependency don't exist yet
try {
+ let memorySize: number | undefined = undefined;
await mono_wasm_load_config(Module.configSrc);
if (runtimeHelpers.diagnosticTracing) console.debug("MONO_WASM: instantiate_wasm_module");
const assetToLoad = resolve_asset_path("dotnetwasm");
// FIXME: this would not apply re-try (on connection reset during download) for dotnet.wasm because we could not download the buffer before we pass it to instantiate_wasm_asset
- await start_asset_download(assetToLoad);
+ const wasmDownloadPromise = start_asset_download(assetToLoad);
+
+ // this is right time as we have free CPU time to do this
+ init_globalization();
+
+ if (config.startupMemoryCache) {
+ memorySize = await getMemorySnapshotSize();
+ runtimeHelpers.loadedMemorySnapshot = !!memorySize;
+ runtimeHelpers.storeMemorySnapshotPending = !runtimeHelpers.loadedMemorySnapshot;
+ }
+ if (!runtimeHelpers.loadedMemorySnapshot) {
+ // we should start downloading DLLs etc as they are not in the snapshot
+ memorySnapshotSkippedOrDone.promise_control.resolve();
+ }
+
+ await wasmDownloadPromise;
await beforePreInit.promise;
Module.addRunDependency("instantiate_wasm_module");
@@ -458,6 +504,21 @@ async function instantiate_wasm_module(
assetToLoad.buffer = null as any; // GC
if (runtimeHelpers.diagnosticTracing) console.debug("MONO_WASM: instantiate_wasm_module done");
+
+ if (runtimeHelpers.loadedMemorySnapshot) {
+ try {
+ const wasmMemory = (Module.asm?.memory || Module.wasmMemory)!;
+
+ // .grow() takes a delta compared to the previous size
+ wasmMemory.grow((memorySize! - wasmMemory.buffer.byteLength + 65535) >>> 16);
+ runtimeHelpers.updateMemoryViews();
+ } catch (err) {
+ console.warn("MONO_WASM: failed to resize memory for the snapshot", err);
+ runtimeHelpers.loadedMemorySnapshot = false;
+ }
+ // now we know if the loading of memory succeeded or not, we can start loading the rest of the assets
+ memorySnapshotSkippedOrDone.promise_control.resolve();
+ }
afterInstantiateWasm.promise_control.resolve();
} catch (err) {
_print_error("MONO_WASM: instantiate_wasm_module() failed", err);
@@ -467,8 +528,18 @@ async function instantiate_wasm_module(
Module.removeRunDependency("instantiate_wasm_module");
}
-async function mono_wasm_before_user_runtime_initialized() {
- mono_wasm_globalization_init();
+async function mono_wasm_before_memory_snapshot() {
+ const mark = startMeasure();
+ if (runtimeHelpers.loadedMemorySnapshot) {
+ // get the bytes after we re-sized the memory, so that we don't have too much memory in use at the same time
+ const memoryBytes = await getMemorySnapshot();
+ mono_assert(memoryBytes!.byteLength === Module.HEAP8.byteLength, "MONO_WASM: Loaded memory is not the expected size");
+ Module.HEAP8.set(new Int8Array(memoryBytes!), 0);
+ if (runtimeHelpers.diagnosticTracing) console.info("MONO_WASM: Loaded WASM linear memory from browser cache");
+
+ // all things below are loaded from the snapshot
+ return;
+ }
for (const k in config.environmentVariables) {
const v = config.environmentVariables![k];
@@ -477,7 +548,10 @@ async function mono_wasm_before_user_runtime_initialized() {
else
throw new Error(`Expected environment variable '${k}' to be a string but it was ${typeof v}: '${v}'`);
}
-
+ if (config.startupMemoryCache) {
+ // disable the trampoline for now, we will re-enable it after we stored the snapshot
+ cwraps.mono_jiterp_update_jit_call_dispatcher(0);
+ }
if (config.runtimeOptions)
mono_wasm_set_runtime_options(config.runtimeOptions);
@@ -487,13 +561,17 @@ async function mono_wasm_before_user_runtime_initialized() {
if (config.browserProfilerOptions)
mono_wasm_init_browser_profiler(config.browserProfilerOptions);
- // init diagnostics after environment variables are set
- if (MonoWasmThreads) {
- await mono_wasm_init_diagnostics();
- }
-
mono_wasm_load_runtime("unused", config.debugLevel);
+ // we didn't have snapshot yet and the feature is enabled. Take snapshot now.
+ if (config.startupMemoryCache) {
+ // this would install the mono_jiterp_do_jit_call_indirect
+ cwraps.mono_jiterp_update_jit_call_dispatcher(-1);
+ await storeMemorySnapshot(Module.HEAP8.buffer);
+ runtimeHelpers.storeMemorySnapshotPending = false;
+ }
+
+ endMeasure(mark, MeasuredBlock.memorySnapshot);
}
export function mono_wasm_load_runtime(unused?: string, debugLevel?: number): void {
@@ -604,6 +682,7 @@ function normalizeConfig() {
config.assets = config.assets || [];
config.runtimeOptions = config.runtimeOptions || [];
config.globalizationMode = config.globalizationMode || "auto";
+
if (config.debugLevel === undefined && BuildConfiguration === "Debug") {
config.debugLevel = -1;
}
@@ -611,21 +690,17 @@ function normalizeConfig() {
config.diagnosticTracing = true;
}
runtimeHelpers.diagnosticTracing = !!config.diagnosticTracing;
+ runtimeHelpers.waitForDebugger = config.waitForDebugger;
+ config.startupMemoryCache = config.startupMemoryCache == undefined ? true : !!config.startupMemoryCache;
+ if (config.startupMemoryCache && runtimeHelpers.waitForDebugger) {
+ if (runtimeHelpers.diagnosticTracing) console.info("MONO_WASM: Disabling startupMemoryCache because waitForDebugger is set");
+ config.startupMemoryCache = false;
+ }
runtimeHelpers.enablePerfMeasure = !!config.browserProfilerOptions
&& globalThis.performance
&& typeof globalThis.performance.measure === "function";
- runtimeHelpers.preferredIcuAsset = get_preferred_icu_asset();
- if (runtimeHelpers.timezone === undefined && config.environmentVariables["TZ"] === undefined) {
- try {
- runtimeHelpers.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || null;
- if (runtimeHelpers.timezone) config.environmentVariables["TZ"] = runtimeHelpers.timezone;
- } catch {
- console.info("MONO_WASM: failed to detect timezone, will fallback to UTC");
- }
- }
- runtimeHelpers.waitForDebugger = config.waitForDebugger;
}
export function mono_wasm_asm_loaded(assembly_name: CharPtr, assembly_ptr: number, assembly_len: number, pdb_ptr: number, pdb_len: number): void {
diff --git a/src/mono/wasm/runtime/types.ts b/src/mono/wasm/runtime/types.ts
index 19a13ea6a7c2a..cd8b63dbe9ff0 100644
--- a/src/mono/wasm/runtime/types.ts
+++ b/src/mono/wasm/runtime/types.ts
@@ -119,6 +119,10 @@ export type MonoConfig = {
* initial number of workers to add to the emscripten pthread pool
*/
pthreadPoolSize?: number,
+ /**
+ * If true, the snapshot of runtime's memory will be stored in the browser and used for faster startup next time. Default is true.
+ */
+ startupMemoryCache?: boolean,
/**
* hash of assets
*/
@@ -134,6 +138,7 @@ export type MonoConfigInternal = MonoConfig & {
logExitCode?: boolean
forwardConsoleLogsToWS?: boolean,
asyncFlushOnExit?: boolean
+ exitAfterSnapshot?: number,
};
export type RunArguments = {
@@ -228,8 +233,12 @@ export type RuntimeHelpers = {
locateFile: (path: string, prefix?: string) => string,
javaScriptExports: JavaScriptExports,
loadedFiles: string[],
+ loadedMemorySnapshot: boolean,
+ storeMemorySnapshotPending: boolean,
+ memorySnapshotCacheKey: string,
+ subtle: SubtleCrypto | null,
preferredIcuAsset: string | null,
- timezone: string | null,
+ invariantMode: boolean,
updateMemoryViews: () => void
}
diff --git a/src/mono/wasm/runtime/types/emscripten.ts b/src/mono/wasm/runtime/types/emscripten.ts
index 0c3fca990c97d..cc8ac6462125e 100644
--- a/src/mono/wasm/runtime/types/emscripten.ts
+++ b/src/mono/wasm/runtime/types/emscripten.ts
@@ -71,6 +71,8 @@ export declare interface EmscriptenModuleInternal {
mainScriptUrlOrBlob?: string;
wasmModule: WebAssembly.Instance | null;
ready: Promise;
+ asm: { memory?: WebAssembly.Memory };
+ wasmMemory?: WebAssembly.Memory;
getWasmTableEntry(index: number): any;
removeRunDependency(id: string): void;
addRunDependency(id: string): void;
diff --git a/src/mono/wasm/test-main.js b/src/mono/wasm/test-main.js
index 979c8a2d90ae0..73437d7a5351d 100644
--- a/src/mono/wasm/test-main.js
+++ b/src/mono/wasm/test-main.js
@@ -66,8 +66,7 @@ async function getArgs() {
let runArgsJson;
// ToDo: runArgs should be read for all kinds of hosts, but
// fetch is added to node>=18 and current Windows's emcc node<18
- if (is_browser)
- {
+ if (is_browser) {
const response = await globalThis.fetch('./runArgs.json');
if (response.ok) {
runArgsJson = initRunArgs(await response.json());
@@ -243,53 +242,97 @@ const App = {
};
globalThis.App = App; // Necessary as System.Runtime.InteropServices.JavaScript.Tests.MarshalTests (among others) call the App.call_test_method directly
+function configureRuntime(dotnet, runArgs, INTERNAL) {
+ dotnet
+ .withVirtualWorkingDirectory(runArgs.workingDirectory)
+ .withEnvironmentVariables(runArgs.environmentVariables)
+ .withDiagnosticTracing(runArgs.diagnosticTracing)
+ .withDiagnosticTracing(true)
+ .withExitOnUnhandledError()
+ .withExitCodeLogging()
+ .withElementOnExit();
+
+ if (is_node) {
+ dotnet
+ .withEnvironmentVariable("NodeJSPlatform", process.platform)
+ .withAsyncFlushOnExit();
+
+ const modulesToLoad = runArgs.environmentVariables["NPM_MODULES"];
+ if (modulesToLoad) {
+ dotnet.withModuleConfig({
+ onConfigLoaded: (config) => {
+ loadNodeModules(config, INTERNAL.require, modulesToLoad)
+ }
+ })
+ }
+ }
+ if (is_browser) {
+ dotnet.withEnvironmentVariable("IsWebSocketSupported", "true");
+ }
+ if (runArgs.runtimeArgs.length > 0) {
+ dotnet.withRuntimeOptions(runArgs.runtimeArgs);
+ }
+ if (runArgs.debugging) {
+ dotnet.withDebugging(-1);
+ dotnet.withWaitingForDebugger(-1);
+ }
+ if (runArgs.forwardConsole) {
+ dotnet.withConsoleForwarding();
+ }
+}
+
+async function dry_run(runArgs) {
+ try {
+ console.log("Silently starting separate runtime instance as another ES6 module to populate caches...");
+ // this separate instance of the ES6 module, in which we just populate the caches
+ const { dotnet, exit, INTERNAL } = await loadDotnet('./dotnet.js?dry_run=true');
+ mono_exit = exit;
+ configureRuntime(dotnet, runArgs, INTERNAL);
+ // silent minimal startup
+ await dotnet.withConfig({
+ forwardConsoleLogsToWS: false,
+ diagnosticTracing: false,
+ appendElementOnExit: false,
+ logExitCode: false,
+ pthreadPoolSize: 0,
+ // this just means to not continue startup after the snapshot is taken.
+ // If there was previously a matching snapshot, it will be used.
+ exitAfterSnapshot: true
+ }).create();
+ } catch (err) {
+ if (err && err.status !== 0) {
+ return false;
+ }
+ }
+ console.log("Separate runtime instance finished loading.");
+ return true;
+}
+
async function run() {
try {
+ const runArgs = await getArgs();
+ console.log("Application arguments: " + runArgs.applicationArguments.join(' '));
+
+ if (is_browser) {
+ const dryOk = await dry_run(runArgs);
+ if (!dryOk) {
+ mono_exit(1, "Failed during dry run");
+ return;
+ }
+ }
+
+ // this is subsequent run with the actual tests. It will use whatever was cached in the previous run.
+ // This way, we are testing that the cached version works.
const { dotnet, exit, INTERNAL } = await loadDotnet('./dotnet.js');
mono_exit = exit;
- const runArgs = await getArgs();
if (runArgs.applicationArguments.length == 0) {
mono_exit(1, "Missing required --run argument");
return;
}
- console.log("Application arguments: " + runArgs.applicationArguments.join(' '));
- dotnet
- .withVirtualWorkingDirectory(runArgs.workingDirectory)
- .withEnvironmentVariables(runArgs.environmentVariables)
- .withDiagnosticTracing(runArgs.diagnosticTracing)
- .withExitOnUnhandledError()
- .withExitCodeLogging()
- .withElementOnExit();
-
- if (is_node) {
- dotnet
- .withEnvironmentVariable("NodeJSPlatform", process.platform)
- .withAsyncFlushOnExit();
-
- const modulesToLoad = runArgs.environmentVariables["NPM_MODULES"];
- if (modulesToLoad) {
- dotnet.withModuleConfig({
- onConfigLoaded: (config) => {
- loadNodeModules(config, INTERNAL.require, modulesToLoad)
- }
- })
- }
- }
- if (is_browser) {
- dotnet.withEnvironmentVariable("IsWebSocketSupported", "true");
- }
- if (runArgs.runtimeArgs.length > 0) {
- dotnet.withRuntimeOptions(runArgs.runtimeArgs);
- }
- if (runArgs.debugging) {
- dotnet.withDebugging(-1);
- dotnet.withWaitingForDebugger(-1);
- }
- if (runArgs.forwardConsole) {
- dotnet.withConsoleForwarding();
- }
+ configureRuntime(dotnet, runArgs, INTERNAL);
+
App.runtime = await dotnet.create();
App.runArgs = runArgs
diff --git a/src/tasks/WasmAppBuilder/WasmAppBuilder.cs b/src/tasks/WasmAppBuilder/WasmAppBuilder.cs
index 40a74d7f57975..c74b5816a26bf 100644
--- a/src/tasks/WasmAppBuilder/WasmAppBuilder.cs
+++ b/src/tasks/WasmAppBuilder/WasmAppBuilder.cs
@@ -334,7 +334,7 @@ protected override bool ExecuteInternal()
{
throw new LogAsErrorException($"PThreadPoolSize must be -1, 0 or positive, but got {PThreadPoolSize}");
}
- else
+ else if (PThreadPoolSize > -1)
{
config.Extra["pthreadPoolSize"] = PThreadPoolSize;
}