Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
c2a0d1e
Init Design doc
jtschuster Oct 8, 2026
8949b1d
Add implementation phases
jtschuster Oct 13, 2026
b3d3fc8
Some changes related to tiering
jtschuster Oct 14, 2026
08b17a1
Update document to include tiering information and clarifications.
jtschuster Oct 14, 2026
40cf02a
Use inline method activation and delay direct calls in auxiliary modu…
jtschuster Oct 16, 2026
e4c7a55
MethodDefEntryPoints per assembly, clarify activation checks
jtschuster Oct 16, 2026
d98f4ed
Add more diagrams and minor wording changes
jtschuster Oct 19, 2026
7ede5e2
Update partitioned R2R modules design for the delayed activation prot…
jtschuster Oct 27, 2026
75e2047
[wasm][R2R] Delay activation of R2R methods in auxiliary modules (sim…
jtschuster Oct 27, 2026
df37fda
[wasm][R2R] Simulate delayed Auxiliary EntryPoints modules with an era
jtschuster Oct 27, 2026
984183f
TEMPORARY: fix crossgen2 build break in NewObjectFixupSignature
jtschuster Oct 27, 2026
ac365f6
TEMPORARY: skip module-less composite ReadyToRunInfo in GetEntryPoint
jtschuster Oct 27, 2026
b19742f
Revert "TEMPORARY: fix crossgen2 build break in NewObjectFixupSignature"
jtschuster Oct 27, 2026
1d0ddf7
Merge remote-tracking branch 'upstream/main' into wasm-r2r-delayed-ac…
jtschuster Oct 27, 2026
62d7975
Implement partitioned Browser-WASM ReadyToRun auxiliary modules
jtschuster Oct 27, 2026
f0984cc
Preserve direct managed helpers across auxiliary Wasm modules
jtschuster Oct 27, 2026
4cdaaf6
Document final auxiliary Wasm size and performance measurements
jtschuster Oct 28, 2026
53380e2
Select partitioned WASM hot methods from MIBC profiles
jtschuster Oct 28, 2026
e013321
Merge dotnet/runtime main into partitioned WASM R2R feature
jtschuster Oct 28, 2026
769fb16
Group cold-exclusive WebAssembly R2R imports before layout
jtschuster Oct 29, 2026
1f95cb5
Separate auxiliary worker preparation from resource loading
jtschuster Oct 29, 2026
1cd46b8
Merge pull request #107 from jtschuster/wasm-r2r-import-layout
jtschuster Oct 29, 2026
66b9e6f
Merge upstream main and adapt partitioned Wasm GC maps
jtschuster Oct 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions docs/design/features/partitioned-wasm-r2r-modules-mibc-sizes.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
variant,role,raw,gzip,brotli
heuristic,primary,11473928,1775725,1138160
heuristic,map,2099936,909297,716273
heuristic,body,43239255,6455104,3365735
heuristic,all,56813119,9140126,5220168
sparse,primary,6765354,729050,387406
sparse,map,2188618,979681,768394
sparse,body,48078684,7345638,3914392
sparse,all,57032656,9054369,5070192
empty,primary,6764118,728715,388491
empty,map,2188630,979496,768322
empty,body,48079617,7345659,3913065
empty,all,57032365,9053870,5069878
union,primary,6766483,729415,388545
union,map,2188602,979495,769139
union,body,48077643,7345520,3914205
union,all,57032728,9054430,5071889
repeated,primary,6765354,729042,388073
repeated,map,2188618,979681,768394
repeated,body,48078684,7345640,3913053
repeated,all,57032656,9054363,5069520
merged-union,primary,6766483,729392,389344
merged-union,map,2188602,979495,769139
merged-union,body,48077643,7345520,3915052
merged-union,all,57032728,9054407,5073535
55 changes: 55 additions & 0 deletions docs/design/features/partitioned-wasm-r2r-modules-sizes.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
side,app,role,files,raw_bytes,gzip_bytes,brotli_bytes
baseline,empty-browser,primary,1,3626082,1042189,722337
baseline,empty-browser,entrypoints-map,0,0,0,0
baseline,empty-browser,body,0,0,0,0
baseline,empty-blazor,primary,1,14748120,3659495,2190179
baseline,empty-blazor,entrypoints-map,0,0,0,0
baseline,empty-blazor,body,0,0,0,0
baseline,blazing-pizza,primary,1,25231901,5327995,3100237
baseline,blazing-pizza,entrypoints-map,0,0,0,0
baseline,blazing-pizza,body,0,0,0,0
baseline,micro-benchmarks,primary,1,3912482,1127476,775941
baseline,micro-benchmarks,entrypoints-map,0,0,0,0
baseline,micro-benchmarks,body,0,0,0,0
baseline,havit-bootstrap,primary,1,42669126,8164595,4731287
baseline,havit-bootstrap,entrypoints-map,0,0,0,0
baseline,havit-bootstrap,body,0,0,0,0
baseline,mud-blazor,primary,1,136209465,23167417,12019321
baseline,mud-blazor,entrypoints-map,0,0,0,0
baseline,mud-blazor,body,0,0,0,0
baseline,igniteui-light,primary,1,29373727,5891038,3426600
baseline,igniteui-light,entrypoints-map,0,0,0,0
baseline,igniteui-light,body,0,0,0,0
baseline,blazor-perf,primary,1,15420384,3780025,2266750
baseline,blazor-perf,entrypoints-map,0,0,0,0
baseline,blazor-perf,body,0,0,0,0
baseline,avalonia-bench,primary,1,15190773,3722407,2377841
baseline,avalonia-bench,entrypoints-map,0,0,0,0
baseline,avalonia-bench,body,0,0,0,0
feature,empty-browser,primary,1,1591530,379683,271875
feature,empty-browser,entrypoints-map,1,111661,58585,48451
feature,empty-browser,body,1,2897189,708921,477380
feature,empty-blazor,primary,1,4607065,933941,641456
feature,empty-blazor,entrypoints-map,1,605836,283176,227702
feature,empty-blazor,body,1,13609113,2839154,1508192
feature,blazing-pizza,primary,1,7131888,1258291,830275
feature,blazing-pizza,entrypoints-map,1,1257548,564851,445728
feature,blazing-pizza,body,1,25590289,4142627,2126338
feature,micro-benchmarks,primary,1,1669058,397843,283578
feature,micro-benchmarks,entrypoints-map,1,121194,63397,52604
feature,micro-benchmarks,body,1,3159760,777089,521213
feature,havit-bootstrap,primary,1,11473928,1775730,1138165
feature,havit-bootstrap,entrypoints-map,1,2099936,909297,716273
feature,havit-bootstrap,body,1,43239255,6455088,3366232
feature,mud-blazor,primary,1,27514543,3250352,1911039
feature,mud-blazor,entrypoints-map,1,8101906,3087422,2335534
feature,mud-blazor,body,1,145879819,19527694,8980534
feature,igniteui-light,primary,1,8235409,1373042,893843
feature,igniteui-light,entrypoints-map,1,1479638,666186,518666
feature,igniteui-light,body,1,29714718,4540681,2375772
feature,blazor-perf,primary,1,4743053,954992,651958
feature,blazor-perf,entrypoints-map,1,635863,296544,239116
feature,blazor-perf,body,1,14276433,2934765,1565229
feature,avalonia-bench,primary,1,5966799,1227308,847497
feature,avalonia-bench,entrypoints-map,1,615587,295663,237782
feature,avalonia-bench,body,1,12931853,2600819,1524572
555 changes: 555 additions & 0 deletions docs/design/features/partitioned-wasm-r2r-modules.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions eng/native.wasm.targets
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@
<EmccExportedFunction Include="___stack_pointer" />
<EmccExportedFunction Include="___coreclr_wasm_rtlrestorecontext_tag" />
<EmccExportedFunction Include="___async_continuation" />
<EmccExportedFunction Include="_dotnet_wasm_notify_auxiliary_entrypoints_activated" Condition="'$(RuntimeFlavor)' == 'CoreCLR'" />
</ItemGroup>

<!-- Emscripten's default INCOMING_MODULE_JS_API, kept explicit so a Module property we don't declare aborts a debug build early. -->
Expand Down
2 changes: 2 additions & 0 deletions eng/testing/tests.browser.targets
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,8 @@
<!-- A similar logic is in ComputeWasmBuildAssets and ComputeWasmPublishAssets -->
<WasmNativeAsset Include="$(MicrosoftNetCoreAppRuntimePackRidNativeDir)dotnet.js" />
<WasmNativeAsset Include="$(MicrosoftNetCoreAppRuntimePackRidNativeDir)dotnet.runtime.js" />
<WasmNativeAsset Include="$(MicrosoftNetCoreAppRuntimePackRidNativeDir)dotnet.auxiliary.worker.js"
Condition="Exists('$(MicrosoftNetCoreAppRuntimePackRidNativeDir)dotnet.auxiliary.worker.js')" />
<WasmNativeAsset Include="$(MicrosoftNetCoreAppRuntimePackRidNativeDir)dotnet.diagnostics.js" Condition="'$(EnableDiagnostics)' == 'true'" />
<WasmNativeAsset Include="$(MicrosoftNetCoreAppRuntimePackRidNativeDir)dotnet.native.wasm" Condition="'$(_HasDotnetWasm)' != 'true'" />
<WasmNativeAsset Include="$(MicrosoftNetCoreAppRuntimePackRidNativeDir)dotnet.native.js" Condition="'$(_HasDotnetNativeJs)' != 'true'" />
Expand Down
3 changes: 3 additions & 0 deletions src/coreclr/clrdefinitions.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,9 @@ if (CLR_CMAKE_TARGET_WIN32)
endif (CLR_CMAKE_TARGET_WIN32)

add_compile_definitions($<${FEATURE_INTERPRETER}:FEATURE_INTERPRETER>)
if (FEATURE_AUXILIARY_R2R_MODULES)
add_compile_definitions(FEATURE_AUXILIARY_R2R_MODULES)
endif()
if (FEATURE_DYNAMIC_CODE_COMPILED)
add_compile_definitions(FEATURE_DYNAMIC_CODE_COMPILED)
endif()
Expand Down
13 changes: 13 additions & 0 deletions src/coreclr/clrfeatures.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,19 @@ if(NOT DEFINED FEATURE_INTERPRETER)
endif()
endif()

# Auxiliary R2R modules currently require single-threaded Browser-WASM and interpreter fallback.
if(NOT DEFINED FEATURE_AUXILIARY_R2R_MODULES)
set(FEATURE_AUXILIARY_R2R_MODULES 0)
if(CLR_CMAKE_TARGET_BROWSER AND CLR_CMAKE_TARGET_ARCH_WASM AND FEATURE_INTERPRETER AND NOT FEATURE_MULTITHREADING)
set(FEATURE_AUXILIARY_R2R_MODULES 1)
endif()
endif()

if(FEATURE_AUXILIARY_R2R_MODULES AND
(NOT CLR_CMAKE_TARGET_BROWSER OR NOT CLR_CMAKE_TARGET_ARCH_WASM OR NOT FEATURE_INTERPRETER OR FEATURE_MULTITHREADING))
message(FATAL_ERROR "FEATURE_AUXILIARY_R2R_MODULES requires single-threaded Browser-WASM with FEATURE_INTERPRETER.")
endif()

if(NOT DEFINED FEATURE_STANDALONE_GC)
set(FEATURE_STANDALONE_GC 1)
endif(NOT DEFINED FEATURE_STANDALONE_GC)
Expand Down
148 changes: 146 additions & 2 deletions src/coreclr/hosts/corerun/wasm/libCorerun.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,145 @@ function libCoreRunFactory() {
}
const mergeCoreRun = {
$CORERUN: {
auxiliaryModuleLoads: [],
waitForAuxiliaryModules: async () => {
await Promise.resolve();
await Promise.all(CORERUN.auxiliaryModuleLoads);
},
installAuxiliaryModuleGates: () => {
if (ENV["DOTNET_WasmAuxiliaryManualActivation"] !== "1") return;
const releases = {};
CORERUN.auxiliaryModuleGates = {};
CORERUN.auxiliaryModuleCompletions = { ENTRYPOINTS: [], MODULES: [] };
const requested = new Set();
let preceding = Promise.resolve();
for (const stage of ["ENTRYPOINTS", "MODULES"]) {
CORERUN.auxiliaryModuleGates[stage] = new Promise(resolve => releases[stage] = resolve);
}
const observeLine = message => {
for (const stage of ["ENTRYPOINTS", "MODULES"]) {
if (message.trim() === "AUXILIARY_R2R_ACTIVATE_" + stage && !requested.has(stage)) {
requested.add(stage);
const previous = preceding;
preceding = Promise.all(CORERUN.auxiliaryModuleCompletions[stage]);
// Console.WriteLine may still have native frames on the stack.
setTimeout(() => releases[stage](previous), 0);
}
}
};
const originalOut = out;
out = message => {
originalOut(message);
observeLine(message);
};
const installWriteObserver = () => {
const write = FS.write;
const decoder = new TextDecoder();
let pending = "";
FS.write = (stream, buffer, offset, length, ...args) => {
const written = write.call(FS, stream, buffer, offset, length, ...args);
if (stream.fd === 1) {
const lines = (pending + decoder.decode(buffer.subarray(offset, offset + written), { stream: true })).split("\n");
pending = lines.pop();
lines.forEach(observeLine);
}
return written;
};
};
// NODERAWFS bypasses out, and replaces FS.write after library postsets run.
const preRun = Module["preRun"];
Module["preRun"] = typeof preRun === "function"
? [preRun, installWriteObserver]
: [...preRun || [], installWriteObserver];
},
queueAuxiliaryModules: (instance, imports, payloadPtr, payloadSize, tableSize, wasmPath) => {
if (instance.exports.getAuxiliaryModuleMetadata === undefined) return;
if (typeof instance.exports.getAuxiliaryModuleMetadata !== "function") throw new Error("Invalid getAuxiliaryModuleMetadata export");
const address = instance.exports.getAuxiliaryModuleMetadata() >>> 0;
if (address === 0) return;
const end = payloadPtr + payloadSize;
const heap = new Uint8Array(wasmMemory.buffer);
if (end > heap.length || address < payloadPtr || address % 4 !== 0 || address + 16 > end) {
throw new Error("Auxiliary R2R module metadata is outside the primary payload");
}
const view = new DataView(wasmMemory.buffer);
const count = view.getUint32(address + 8, true);
if (count > (end - address - 16) / 16) throw new Error("Truncated auxiliary R2R module metadata");
const modules = [];
const names = new Set();
const addModule = (filenameRva, statusAddress, entrypoints) => {
const start = payloadPtr + filenameRva;
if (view.getUint32(statusAddress, true) > 1 || start >= end) throw new Error("Invalid auxiliary R2R module metadata");
let filenameEnd = start;
while (filenameEnd < end && heap[filenameEnd] !== 0) filenameEnd++;
if (filenameEnd === end) throw new Error("Unterminated auxiliary R2R module filename");
const name = new TextDecoder("utf-8", { fatal: true }).decode(heap.subarray(start, filenameEnd));
if (!/^[^./\\][^/\\?#:]*\.wasm$/.test(name) || names.has(name)) throw new Error(`Invalid auxiliary R2R module filename '${name}'`);
names.add(name);
modules.push({ name, statusAddress, entrypoints });
};
addModule(view.getUint32(address + 12, true), address, true);
let nextTableIndex = view.getUint32(address + 4, true);
if (nextTableIndex > tableSize) throw new Error("Invalid primary R2R module table range");
for (let i = 0; i < count; i++) {
const entry = address + 16 + i * 16;
const tableBase = view.getUint32(entry + 4, true);
const methodCount = view.getUint32(entry + 8, true);
if (tableBase !== nextTableIndex || tableBase + methodCount > tableSize) {
throw new Error("Invalid auxiliary R2R module table range");
}
nextTableIndex += methodCount;
addModule(view.getUint32(entry, true), entry + 12, false);
}
// Native registration finishes after the synchronous probe returns. Never activate in the probe.
const directory = wasmPath.substring(0, wasmPath.lastIndexOf("/") + 1);
const operations = modules.map(module => Promise.resolve().then(async () => {
try {
if (new DataView(wasmMemory.buffer).getUint32(module.statusAddress, true) === 1) return;
if (CORERUN.auxiliaryModuleGates) {
await CORERUN.auxiliaryModuleGates[module.entrypoints ? "ENTRYPOINTS" : "MODULES"];
}
const delayName = module.entrypoints
? "DOTNET_WasmAuxiliaryEntryPointsDelayMilliseconds"
: "DOTNET_WasmAuxiliaryModuleDelayMilliseconds";
const delay = Number(ENV[delayName] || 0);
if (!Number.isInteger(delay) || delay < 0 || delay > 2147483647) {
throw new Error(`Invalid ${delayName}: ${ENV[delayName]}`);
}
if (delay !== 0) await new Promise(resolve => setTimeout(resolve, delay));
const bytes = FS.readFile(directory + module.name);
const { instance } = await WebAssembly.instantiate(bytes, imports);
if (typeof instance.exports.ActivateModule !== "function") throw new Error("Missing ActivateModule export");
if (new DataView(wasmMemory.buffer).getUint32(module.statusAddress, true) === 0) {
instance.exports.ActivateModule();
if (new DataView(wasmMemory.buffer).getUint32(module.statusAddress, true) !== 1) {
throw new Error("ActivateModule did not publish its activation status");
}
if (module.entrypoints) {
if (typeof wasmExports.dotnet_wasm_notify_auxiliary_entrypoints_activated !== "function") {
throw new Error("Runtime does not support auxiliary R2R modules");
}
wasmExports.dotnet_wasm_notify_auxiliary_entrypoints_activated();
}
}
} catch (reason) {
const error = new Error(`Failed to load auxiliary R2R module '${module.name}': ${reason instanceof Error ? reason.message : String(reason)}`);
err(error.message);
return error;
}
}));
if (CORERUN.auxiliaryModuleCompletions) {
for (let i = 0; i < modules.length; i++) {
CORERUN.auxiliaryModuleCompletions[modules[i].entrypoints ? "ENTRYPOINTS" : "MODULES"].push(operations[i]);
}
}
const completion = Promise.all(operations).then(errors => {
const error = errors.find(error => error !== undefined);
if (error) throw error;
});
CORERUN.auxiliaryModuleLoads.push(completion);
void completion.catch(() => {});
},
selfInitialize: () => {
const browserVirtualAppBase = "/";// keep in sync other places that define browserVirtualAppBase
FS.createPath("/", browserVirtualAppBase, true, true);
Expand All @@ -32,6 +171,8 @@ function libCoreRunFactory() {
}

ENV["DOTNET_SYSTEM_GLOBALIZATION_INVARIANT"] = "true";
Module["waitForAuxiliaryModules"] = CORERUN.waitForAuxiliaryModules;
CORERUN.installAuxiliaryModuleGates();

if (ENVIRONMENT_IS_NODE) {
const original_proc_exit = _proc_exit;
Expand Down Expand Up @@ -226,6 +367,7 @@ function libCoreRunFactory() {

var payloadPtr = 0;
var wasmInstance;
var imports;
const sp = stackSave();
try {
const ptrPtr = stackAlloc(4);
Expand All @@ -242,7 +384,7 @@ function libCoreRunFactory() {
throw new Error("__async_continuation was not preserved by the linker or optimizer");
}
payloadPtr = HEAPU32[ptrPtr >>> 2 >>> 0];
wasmInstance = new WebAssembly.Instance(wasmModule, {
imports = {
webcil: {
memory: wasmMemory,
__stack_pointer: wasmExports.__stack_pointer,
Expand All @@ -253,7 +395,8 @@ function libCoreRunFactory() {
// Runtime-async continuation return value, shared with the runtime module.
__async_continuation: wasmExports.__async_continuation
}
});
};
wasmInstance = new WebAssembly.Instance(wasmModule, imports);
} catch (e) {
const errorMessage = e instanceof Error ? e.message : String(e);
console.error("Failed to construct WebAssembly instance for Webcil image:", { wasmPath, errorMessage });
Expand Down Expand Up @@ -281,6 +424,7 @@ function libCoreRunFactory() {
HEAPU32[outDataStartPtr >>> 2 >>> 0] = payloadPtr;
HEAPU32[outSize >>> 2 >>> 0] = payloadSize;
HEAPU32[(outSize + 4) >>> 2 >>> 0] = 0;
CORERUN.queueAuxiliaryModules(wasmInstance, imports, payloadPtr, payloadSize, tableSize, wasmPath);
return true;
}
};
Expand Down
5 changes: 5 additions & 0 deletions src/coreclr/inc/clrconfigvalues.h
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,11 @@ RETAIL_CONFIG_STRING_INFO(EXTERNAL_Interpreter, W("Interpreter"), "Enables Inter
RETAIL_CONFIG_DWORD_INFO(EXTERNAL_InterpMode, W("InterpMode"), 0, "Enables Interpreter for a subset of all methods depending on the specified mode.")
#endif // FEATURE_INTERPRETER

#ifdef FEATURE_AUXILIARY_R2R_MODULES
RETAIL_CONFIG_DWORD_INFO(EXTERNAL_R2RDelayedActivationCallCount, W("R2RDelayedActivationCallCount"), 0, "Activate auxiliary R2R code after this many pending interpreter entries across a composite image. Zero preserves eager activation.")
RETAIL_CONFIG_DWORD_INFO(EXTERNAL_R2RDelayedEntryPointsActivationCallCount, W("R2RDelayedEntryPointsActivationCallCount"), 0, "Activate auxiliary R2R EntryPoints modules after this many calls to methods awaiting them. Defaults to R2RDelayedActivationCallCount when not specified.")
#endif // FEATURE_AUXILIARY_R2R_MODULES

RETAIL_CONFIG_DWORD_INFO(EXTERNAL_JitHostMaxSlabCache, W("JitHostMaxSlabCache"), 0x1000000, "Sets jit host max slab cache size, 16MB default")

RETAIL_CONFIG_DWORD_INFO(EXTERNAL_JitOptimizeType, W("JitOptimizeType"), 0 /* OPT_DEFAULT */, "")
Expand Down
Loading
Loading