page: Muck runs in Chromium on the .NET provider (B3 first run)

- guest code a stateful provider called (its frames below the call-out) waits in place and
  stages threads instead of trapping to the run loop, which had abandoned the provider's
  frames (the guest then returned into the provider's code page: SIGSEGV);
- tier-1's syscall import starts staged threads in such a process;
- a provider's guest call-outs nest up to 400 deep (they cannot leave);
- gex-fjit: in the browser a function of more than 1500 blocks is laid out flat (the
  structured emission's recursion overflowed a Worker's stack on UnityPlayer);
- diagnostics at a wild branch: open call-outs, the last provider guest calls and lookups.

Muck in the page on edger (no function JIT, interpreter): first frame 79 s after the page
opened, then 11.5 presents/s from 300 s to 700 s, when a WebGPU validation error in Mesa's
gex_wgpu (a bind group layout) stops drawing.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_d2453757b8a84fdbb4ee7c9e093141df
This commit is contained in:
velzieandClaude Opus 5.5 committed 2026-10-08 02:36:20 -04:00
1 parent c19c8e01cd
commit aa4b35e125
5 files changed
+118 -7

No files matched your search

+11 -2
View File
@@ -137,6 +137,9 @@ fn flags() -> RegSet {
/// Emit `f` (memory form, verified, `loops` = `outer_loops(f)` when
/// outlining). `None` when a loop function's CFG is irreducible.
/// Blocks above which a function is laid out flat in the browser (see `emit`).
const FLAT_ABOVE: usize = 1500;
pub fn emit(m: &ModuleCx<'_>, f: &Function, loops: &[Loop], stats: &mut Stats) -> Option<Emitted> {
let n = f.blocks.len();
// The body's graph: the blocks, then one handover node per outlined
@@ -151,7 +154,13 @@ pub fn emit(m: &ModuleCx<'_>, f: &Function, loops: &[Loop], stats: &mut Stats) -
succs.push(lp.exits.iter().copied().collect());
}
let graph = Graph { succs: succs.clone(), entry: 0 };
let mut layout = match cfg::make_reducible(graph) {
// In the browser the structured layout's emission recurses about as deep
// as the function's structure, on a Worker's stack (about 1 MB): a function
// of thousands of blocks (UnityPlayer has them) overflowed it
// ("Maximum call stack size exceeded" in `tree`/`within`/`node`). Such a
// function is laid out flat there.
let too_deep = cfg!(target_family = "wasm") && succs.len() > FLAT_ABOVE;
let mut layout = match (!too_deep).then(|| cfg::make_reducible(graph)).flatten() {
Some(fx) => {
if !fx.dispatch.is_empty() {
stats.irreducible += 1;
@@ -163,7 +172,7 @@ pub fn emit(m: &ModuleCx<'_>, f: &Function, loops: &[Loop], stats: &mut Stats) -
// branch setting the arm (tier-1's region form).
stats.dropped += 1;
if std::env::var_os("GEX_FJIT_STATS").is_some() {
eprintln!("[fjit] {:#x}{}: irreducible, laid out flat", f.entry, f.name.as_deref().map(|n| format!(" ({n})")).unwrap_or_default());
eprintln!("[fjit] {:#x}{}: irreducible or too big, laid out flat", f.entry, f.name.as_deref().map(|n| format!(" ({n})")).unwrap_or_default());
}
let graph = Graph { succs, entry: 0 };
let mut arm = vec![u32::MAX; graph.succs.len()];
+16 -2
View File
@@ -1340,7 +1340,7 @@ export class Gex {
opts.syscallGuard(ctx >>> 0, pc, this.#memory);
return e.gex_syscall(ctx, pc);
}
: e.gex_syscall,
: this.#guestSyscall(e),
dispatch_miss: e.gex_dispatch_miss,
illegal: e.gex_illegal,
breakpoint: e.gex_breakpoint,
@@ -1564,7 +1564,7 @@ export class Gex {
const e = this.#shim.exports;
this.#imports = {
memory: this.#memory,
syscall: e.gex_syscall,
syscall: this.#guestSyscall(e),
dispatch_miss: e.gex_dispatch_miss,
illegal: e.gex_illegal,
breakpoint: e.gex_breakpoint,
@@ -3176,6 +3176,20 @@ export class Gex {
* install, a thread to start -- is served here and returns, so the native
* frames below it survive, as natively. Anything else (the process or
* thread ending, a callback's return) goes on up. */
/** Tier-1's `gex.syscall`: the shim's export itself, except in a process with a
* stateful provider, where guest code a provider called (its frames below) stages a
* thread or a child rather than trapping, and the host starts it here. */
#guestSyscall(e) {
if (!Gex.wasmDlls) return e.gex_syscall;
return (ctx, pc) => {
e.gex_syscall(ctx, pc);
if (e.gex_wasmprog_staged()) {
this.#drainFork().catch((err) => console.error(`[gex] starting a process: ${err.message ?? err}`));
this.#drainThread().catch((err) => console.error(`[gex] starting a thread: ${err.message ?? err}`));
}
};
}
/** A wasm64 provider's `gex.syscall(ctx, pc)` (cgen/libprov.rs): marked, so the shim
* serves it without leaving the provider's frames (a blocking call waits in place, a
* thread is staged); the host then starts what it staged, and the call returns. */
+70 -1
View File
@@ -1183,6 +1183,15 @@ pub extern "C" fn gex_ftab_lookup(_ctx: i32, addr: i64) -> i32 {
let hit = hit.or_else(|| crate::fjit::translate(addr));
let idx = hit.unwrap_or(gex_abi::ftab::FALLBACK);
gex_kernel::ftab::fcache_write(mem, addr, idx);
if libprov::any_registered() {
LOOKUP_RING.with(|r| {
let mut r = r.borrow_mut();
if r.len() >= 16 {
r.remove(0);
}
r.push((addr, idx));
});
}
idx as i32
}
@@ -1253,7 +1262,28 @@ pub extern "C" fn gex_ftab_fallback(ctx: i32, _depth: i32, _need: i32, ret: i64)
if ret as u64 == gex_abi::ftab::NO_RETURN || !CALLOUT_ON.load(Ordering::Relaxed) {
return gex_abi::ftab::RET_LEFT;
}
if CALLOUTS.with(|c| c.borrow().len()) >= CALLOUT_MAX_DEPTH {
if libprov::provider_return(ret as u64) {
let sp = gm.read_u64(c + gex_abi::ctx::x(sp_index()) as u64).unwrap_or(0);
PROV_RING.with(|r| {
let mut r = r.borrow_mut();
if r.len() >= 16 {
r.remove(0);
}
r.push((t, ret as u64, sp));
});
}
if PROV_TRACE.load(Ordering::Relaxed) && libprov::provider_return(ret as u64) {
crate::warn_line(&format!("[gex] provider guest call to {t:#x} (call-outs open {})\n", CALLOUTS.with(|c| c.borrow().len())));
}
// A stateful provider's guest call (its return address in the provider's
// code page) cannot be left: its frames would be lost. Those nest as deep
// as the program's calls between guest and provider do (Unity: icall ->
// runtime_invoke -> icall ...).
let limit = if libprov::provider_return(ret as u64) { CALLOUT_MAX_DEPTH_PROVIDER } else { CALLOUT_MAX_DEPTH };
if CALLOUTS.with(|c| c.borrow().len()) >= limit {
if limit == CALLOUT_MAX_DEPTH_PROVIDER {
crate::warn_line(&format!("gex: a provider's guest calls nest deeper than {limit}\n"));
}
return gex_abi::ftab::RET_LEFT;
}
// The stack once the guest has returned: the caller's, past the return
@@ -1273,6 +1303,10 @@ pub extern "C" fn gex_ftab_fallback(ctx: i32, _depth: i32, _need: i32, ret: i64)
CALLOUTS.with(|c| c.borrow_mut().truncate(depth));
if r == gex_abi::ftab::RET_LEFT {
CALLOUT_LEFT.fetch_add(1, Ordering::Relaxed);
if libprov::provider_return(ret as u64) {
let pc = GuestMem::new(crate::guest_bytes()).read_u64(c + gex_abi::ctx::PC as u64).unwrap_or(0);
crate::warn_line(&format!("[gex] a provider's guest call-out to {t:#x} was left (the guest is at {pc:#x})\n"));
}
}
r
}
@@ -1298,11 +1332,17 @@ static CALLOUT_ON: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool
#[no_mangle]
pub extern "C" fn gex_set_callout(on: i32) {
CALLOUT_ON.store(on != 0, Ordering::Relaxed);
PROV_TRACE.store(on == 2, Ordering::Relaxed);
}
/// Call-outs nested deeper than this leave instead: each holds a shim frame
/// and a JS one.
const CALLOUT_MAX_DEPTH: usize = 32;
/// `gex_set_callout(2)`: say every stateful provider's guest call.
static PROV_TRACE: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
/// The same for a stateful provider's guest calls, which cannot leave.
const CALLOUT_MAX_DEPTH_PROVIDER: usize = 400;
/// How far above a call-out's `sp_want` its return may come back (what a
/// stdcall callee pops, or a function frame resumed inside), and past which
@@ -1379,6 +1419,35 @@ pub(crate) fn returned(pc: u64, ctx: u64) -> bool {
}
}
thread_local! {
/// The last function-table lookups of this Worker (address, index), in a provider's process.
static LOOKUP_RING: std::cell::RefCell<Vec<(u64, u32)>> = const { std::cell::RefCell::new(Vec::new()) };
/// The last stateful-provider guest calls that reached the fallback: (target, return, sp).
static PROV_RING: std::cell::RefCell<Vec<(u64, u64, u64)>> = const { std::cell::RefCell::new(Vec::new()) };
}
/// Whether a stateful provider's guest call is open on this Worker: the guest
/// code running now has the provider's wasm frames below it, which a trap to
/// JS's run loop would abandon.
pub(crate) fn provider_callout_open() -> bool {
CALLOUTS.with(|c| c.borrow().iter().any(|&(r, _, _)| libprov::provider_return(r)))
}
/// [`LOOKUP_RING`], for a diagnostic.
pub(crate) fn lookups_recent() -> Vec<(u64, u32)> {
LOOKUP_RING.with(|r| r.borrow().clone())
}
/// [`PROV_RING`], for a diagnostic.
pub(crate) fn provider_calls_recent() -> Vec<(u64, u64, u64)> {
PROV_RING.with(|r| r.borrow().clone())
}
/// The call-outs this Worker has open, for a diagnostic: (return, sp wanted, callback depth).
pub(crate) fn callouts_open() -> Vec<(u64, u64, u32)> {
CALLOUTS.with(|c| c.borrow().clone())
}
/// A call-out at depth `depth` (and every deeper one) was left by an
/// exception (JS's loop unwound through its frames): its entries go.
#[no_mangle]
+7
View File
@@ -305,6 +305,13 @@ pub extern "C" fn gex_cgen_pmod_libprov(number: u32) -> i32 {
lock(&LIBS).iter().any(|l| l.number == Some(number)) as i32
}
/// Whether `addr` is one of the return addresses a provider's guest calls push
/// (its code page, `[entry + 0x100, entry + 0x1000)`).
pub(crate) fn provider_return(addr: u64) -> bool {
let at = THREAD_ENTRY.load(Ordering::Relaxed);
at != 0 && addr >= at + 0x100 && addr < at + REVERSE_FIRST
}
/// `gex.provider_thread_entry()` / `provider_code_page()`.
#[no_mangle]
pub extern "C" fn gex_libprov_thread_entry() -> u64 {
+14 -2
View File
@@ -6797,9 +6797,10 @@ thread_local! {
}
/// Whether the syscall being served cannot leave its wasm frames: a wasm32
/// program's, or a stateful provider's.
/// program's, a stateful provider's, or guest code a provider called (its
/// frames are below the call-out).
fn frames_stay(h: &Host) -> bool {
h.kernel.wasm32 || IN_PROVIDER.with(|c| c.get())
h.kernel.wasm32 || IN_PROVIDER.with(|c| c.get()) || cgen::provider_callout_open()
}
/// A thread or a fork a wasm32 program's syscall staged without trapping
@@ -10965,6 +10966,17 @@ pub extern "C" fn gex_dispatch_miss(_ctx: i32, pc: i64) -> i32 {
stack.join(", "),
LAST_SYSCALL.load(std::sync::atomic::Ordering::Relaxed),
));
// A return a call-out should have caught: the call-outs open, and
// the callback depth this Worker is at.
let open = cgen::callouts_open();
if !open.is_empty() || cgen::libprov::provider_return(pc) {
warn_line(&format!(
"[gex] call-outs open: {:x?} (return, sp wanted, callback depth); sp now {sp:#x}; last provider guest calls (target, return, sp): {:x?}\n",
open,
cgen::provider_calls_recent()
));
warn_line(&format!("[gex] last lookups (address, index): {:x?}\n", cgen::lookups_recent()));
}
set_exit(h, 128 + 11);
});
trap();