fcache: a probe cache entry holds the whole address of code below 64 GiB

An entry kept the address's low 32 bits and the index above them, and
every probe compared the whole target with those bits. In a 64-bit
memory, where Wine puts the images near the top of the 16 GiB, no call
into them hit: each went to gex.lookup and the function table's lock
(Hades gameplay: ftab lookups about 15% of the render thread's samples,
113M lookups in 740 s on one thread). And an entry written for an
address above 4 GiB was taken for the address 4 GiB below it, which has
the same slot and the same low bits: tests/browser-mem64-fcache.sh,
functions at 0x20000000 and 0x120000000 called in turn from the function
JIT's code, summed 199999001 where 100100000 is right.

An entry is now the address's low 36 bits (gex_abi::FCACHE_TAG_BITS)
and a 28-bit index above them: gex_abi::fcache_pack/fcache_probe, the
kernel's writes and clears, cgen's C and wasm probes, the function
JIT's two, gex-split's stubs, and the two providers' C probes. An
address or index an entry cannot hold leaves the slot to miss.
UNIT_ABI_VERSION goes to 2: every unit's inline probe reads the new
form, so every unit is rebuilt.

?fcachestats=1 (GEX_FCACHE_STATS=1 under run-node): each Worker's probe
misses at most every 10 s, how many above 4 GiB, and the addresses that
missed most. Off by default, and nothing is counted then.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_40f26e8988a74ea098d8bc80ff68287c
This commit is contained in:
velzieandClaude Opus 5.5 committed 2026-10-22 16:58:43 -04:00
1 parent 085fdc23fe
commit 3bb8eaff18
15 files changed
+215 -32

No files matched your search

+32 -8
View File
@@ -613,11 +613,13 @@ pub const BODY_LIMIT: u64 = BODY_LIMITS;
/// The function table's probe cache (docs/merge-plan.md §9.8): guest address
/// → [`ftab`] index, direct-mapped, shared by every thread of the process.
/// Entry `fcache_slot(a)` is one `u64`: the address's low 32 bits in the low
/// half (guest code is below 4 GiB), the index in the high half. Generated
/// code probes it inline on an indirect transfer; a miss (or a stale entry)
/// is index 0, the resolve stub, which asks the host (`gex.lookup`) and
/// writes the entry. All zero at start: address 0 maps to the resolve stub.
/// Entry `fcache_slot(a)` is one `u64`: the address's low
/// [`FCACHE_TAG_BITS`] bits (all of it, for code below 64 GiB: a memory64
/// process's images sit near the top of its 16 GiB) and the index above
/// them. Generated code probes it inline on an indirect transfer; a miss (or
/// a stale entry) is index 0, the resolve stub, which asks the host
/// (`gex.lookup`) and writes the entry. All zero at start: address 0 maps to
/// the resolve stub.
pub const FCACHE: u64 = PATCHED_PAGES - FCACHE_BYTES;
/// `log2` of the probe cache's entries.
pub const FCACHE_BITS: u32 = 15;
@@ -640,10 +642,32 @@ pub const fn fcache_entry(a: u64) -> u64 {
FCACHE + 8 * fcache_slot(a)
}
/// A probe cache entry for `a` at index `idx`.
/// The address bits a probe cache entry holds, below its index: the whole
/// address of code below 64 GiB. A target at or past that never hits (the
/// entry cannot name it), and a probe compares the whole target, so an
/// entry never stands for another address with the same low bits.
pub const FCACHE_TAG_BITS: u32 = 36;
/// The address part of a probe cache entry.
pub const FCACHE_TAG_MASK: u64 = (1 << FCACHE_TAG_BITS) - 1;
/// One past the largest index an entry holds (its other 28 bits).
pub const FCACHE_IDX_LIMIT: u64 = 1 << (64 - FCACHE_TAG_BITS);
/// A probe cache entry for `a` at index `idx`; `None` for an address or
/// index an entry cannot hold (the slot is then left to miss).
#[inline]
pub const fn fcache_pack(a: u64, idx: u32) -> u64 {
(a & 0xffff_ffff) | ((idx as u64) << 32)
pub const fn fcache_pack(a: u64, idx: u32) -> Option<u64> {
if a > FCACHE_TAG_MASK || idx as u64 >= FCACHE_IDX_LIMIT {
return None;
}
Some(a | ((idx as u64) << FCACHE_TAG_BITS))
}
/// The index probe cache entry `e` holds for target `t`, or the resolve stub
/// ([`ftab::RESOLVE`]) when it holds another address: what every emitter's
/// inline probe computes.
#[inline]
pub const fn fcache_probe(e: u64, t: u64) -> u32 {
if e & FCACHE_TAG_MASK == t { (e >> FCACHE_TAG_BITS) as u32 } else { ftab::RESOLVE }
}
/// Where the thread contexts must end: the runtime's fixed words
+5 -3
View File
@@ -44,6 +44,7 @@ pub fn header() -> String {
let _ = writeln!(w, "#define GEX_FCACHE 0x{:x}u", gex_abi::FCACHE);
let _ = writeln!(w, "#define GEX_FCACHE_BITS {}u", gex_abi::FCACHE_BITS);
let _ = writeln!(w, "#define GEX_FCACHE_MUL 0x{:x}u", gex_abi::FCACHE_MUL);
let _ = writeln!(w, "#define GEX_FCACHE_TAG_BITS {}u", gex_abi::FCACHE_TAG_BITS);
let _ = writeln!(w, "#define GEX_FTAB_RESOLVE {}u", gex_abi::ftab::RESOLVE);
let _ = writeln!(w);
@@ -386,8 +387,9 @@ GEX_INLINE uint64_t gex_fma64(int ctx, uint64_t ab, uint64_t bb_, uint64_t cb, u
/* The function table (docs/merge-plan.md §9.8.1). A unit is linked
position-independent, so its function pointers are indices into the
process's table, and a call through one is `call_indirect` on it. The
index for a guest address comes from the probe cache: the entry's low half
is the address, its high half the index; a miss is the resolve stub (0),
index for a guest address comes from the probe cache: the entry's low
GEX_FCACHE_TAG_BITS bits are the address, the bits above them the index;
a miss is the resolve stub (0),
which asks the host. Every call through the table stores its target in
ctx.LAST_TARGET first (the resolve stub, and any slot a thread has not
filled, read it). */
@@ -395,7 +397,7 @@ typedef int (*gex_body_t)(int ctx, int32_t depth, uint32_t need, uint64_t ret);
GEX_INLINE uint32_t gex_ftab_idx(uint64_t t) {
uint32_t slot = ((uint32_t)t * GEX_FCACHE_MUL) >> (32u - GEX_FCACHE_BITS);
uint64_t e = __atomic_load_n((gex_a64 *)GEX_P(GEX_FCACHE + 8u * slot), __ATOMIC_RELAXED);
return (e & 0xffffffffull) == t ? (uint32_t)(e >> 32) : GEX_FTAB_RESOLVE;
return (e & ((1ull << GEX_FCACHE_TAG_BITS) - 1u)) == t ? (uint32_t)(e >> GEX_FCACHE_TAG_BITS) : GEX_FTAB_RESOLVE;
}
#define GEX_FTAB(i) ((gex_body_t)(uintptr_t)(i))
+5 -3
View File
@@ -42,8 +42,10 @@ pub const UNIT_INPUTS: &str = env!("GEX_CGEN_UNIT_INPUTS");
/// What a unit embeds from outside gex-cgen and gex-abi (gex-emit's FMA
/// constants, in `gex_abi.h`), which the unit key does not hash: bump it
/// whenever [`unit_abi_fingerprint`] changes (its test says when).
pub const UNIT_ABI_VERSION: u32 = 1;
/// whenever [`unit_abi_fingerprint`] changes (its test says when). 2: the
/// probe cache's wide entries (`gex_abi::FCACHE_TAG_BITS`), which every
/// unit's inline probe reads.
pub const UNIT_ABI_VERSION: u32 = 2;
/// The identity of an image's recovery (`gex_key::recovery_key` with
/// [`RECOVERY_INPUTS`]): where the recovery is cached.
@@ -932,7 +934,7 @@ mod abi_tests {
fn unit_abi_version_pins_the_embedded_constants() {
assert_eq!(
(super::UNIT_ABI_VERSION, super::unit_abi_fingerprint().as_str()),
(1, "fma 134217729.0 523 1523")
(2, "fma 134217729.0 523 1523")
);
}
}
+4 -4
View File
@@ -105,14 +105,14 @@ pub fn emit_probe(f: &mut Function, t: u32, e: u32) {
crate::splice::widen_addr(f);
f.instruction(&I::I64Load(m));
f.instruction(&I::LocalTee(e));
// hit ? e >> 32 : 0, where hit compares the whole target with the low
// half (a target past 4 GiB never hits).
f.instruction(&I::I64Const(32));
// hit ? e >> TAG_BITS : 0, where hit compares the whole target with the
// entry's address bits (`gex_abi::fcache_probe`).
f.instruction(&I::I64Const(gex_abi::FCACHE_TAG_BITS as i64));
f.instruction(&I::I64ShrU);
f.instruction(&I::I32WrapI64);
f.instruction(&I::I32Const(ftab::RESOLVE as i32));
f.instruction(&I::LocalGet(e));
f.instruction(&I::I64Const(0xffff_ffff));
f.instruction(&I::I64Const(gex_abi::FCACHE_TAG_MASK as i64));
f.instruction(&I::I64And);
f.instruction(&I::LocalGet(t));
f.instruction(&I::I64Eq);
+4 -4
View File
@@ -621,12 +621,12 @@ fn probe_const(fb: &mut gex_emit::wasm::FuncBody, t: u64, e: u32) {
fb.addr_const(0);
fb.load(0x29, 3, gex_abi::fcache_entry(t) as u32); // i64.load
fb.local_tee(e);
fb.i64_const(32);
fb.i64_const(gex_abi::FCACHE_TAG_BITS as i64);
fb.byte(0x88); // i64.shr_u
fb.byte(0xa7); // i32.wrap_i64
fb.i32_const(gex_abi::ftab::RESOLVE as i32);
fb.local_get(e);
fb.i64_const(0xffff_ffff);
fb.i64_const(gex_abi::FCACHE_TAG_MASK as i64);
fb.byte(0x83); // i64.and
fb.i64_const(t as i64);
fb.byte(0x51); // i64.eq
@@ -1177,12 +1177,12 @@ impl E<'_, '_> {
fb.addr_i32();
fb.load(0x29, 3, gex_abi::FCACHE as u32); // i64.load
fb.local_tee(e);
fb.i64_const(32);
fb.i64_const(gex_abi::FCACHE_TAG_BITS as i64);
fb.byte(0x88); // i64.shr_u
fb.byte(0xa7); // i32.wrap_i64
fb.i32_const(gex_abi::ftab::RESOLVE as i32);
fb.local_get(e);
fb.i64_const(0xffff_ffff);
fb.i64_const(gex_abi::FCACHE_TAG_MASK as i64);
fb.byte(0x83); // i64.and
self.b.push_operand(o);
let fb = self.b.f();
+43 -6
View File
@@ -343,21 +343,27 @@ pub fn translator() -> Option<&'static dyn Translator> {
TRANSLATOR.get().map(|b| &**b)
}
/// Write `addr → idx` into the probe cache in guest memory `mem`.
/// Write `addr → idx` into the probe cache in guest memory `mem`. An
/// address or index no entry can hold (`gex_abi::fcache_pack`) clears the
/// slot instead, so it is left to miss.
pub fn fcache_write(mem: &mut [u8], addr: u64, idx: u32) {
let at = gex_abi::fcache_entry(addr) as usize;
let v = gex_abi::fcache_pack(addr, idx).unwrap_or(FCACHE_EMPTY);
if let Some(e) = mem.get_mut(at..at + 8) {
e.copy_from_slice(&gex_abi::fcache_pack(addr, idx).to_le_bytes());
e.copy_from_slice(&v.to_le_bytes());
}
}
/// An empty probe cache entry: address 0, the resolve stub.
const FCACHE_EMPTY: u64 = 0;
/// Empty the whole probe cache: every entry the resolve stub. For a memory
/// whose cache names another table's indices -- an `execve`'s new program,
/// or a fork's child, whose low region (and the cache in it) is a copy of
/// the parent's while its table is its own.
pub fn fcache_reset(mem: &mut [u8]) {
let (lo, hi) = (gex_abi::FCACHE as usize, (gex_abi::FCACHE + gex_abi::FCACHE_BYTES) as usize);
let empty = gex_abi::fcache_pack(0, RESOLVE).to_le_bytes();
let empty = FCACHE_EMPTY.to_le_bytes();
if let Some(c) = mem.get_mut(lo..hi) {
for e in c.chunks_exact_mut(8) {
e.copy_from_slice(&empty);
@@ -371,8 +377,8 @@ pub fn fcache_clear(mem: &mut [u8], addr: u64) {
let at = gex_abi::fcache_entry(addr) as usize;
if let Some(e) = mem.get_mut(at..at + 8) {
let v = u64::from_le_bytes(e.try_into().unwrap());
if v & 0xffff_ffff == addr & 0xffff_ffff {
e.copy_from_slice(&gex_abi::fcache_pack(0, RESOLVE).to_le_bytes());
if v & gex_abi::FCACHE_TAG_MASK == addr {
e.copy_from_slice(&FCACHE_EMPTY.to_le_bytes());
}
}
}
@@ -548,7 +554,7 @@ mod tests {
fcache_write(&mut mem, 0x40_1234, 99);
let at = gex_abi::fcache_entry(0x40_1234) as usize;
let v = u64::from_le_bytes(mem[at..at + 8].try_into().unwrap());
assert_eq!((v as u32, (v >> 32) as u32), (0x40_1234, 99));
assert_eq!(gex_abi::fcache_probe(v, 0x40_1234), 99);
// Another address sharing the slot does not clear it.
fcache_clear(&mut mem, 0x40_1235);
assert_eq!(u64::from_le_bytes(mem[at..at + 8].try_into().unwrap()), v);
@@ -556,6 +562,37 @@ mod tests {
assert_eq!(u64::from_le_bytes(mem[at..at + 8].try_into().unwrap()), 0);
}
/// A 64-bit memory's code above 4 GiB: an entry names its whole address,
/// so a function at t + 4 GiB (the same slot, the same low 32 bits) is
/// never taken for the one at t, and the one above 4 GiB hits.
#[test]
fn probe_cache_entries_above_4_gib() {
let mut mem = vec![0u8; (gex_abi::FCACHE + gex_abi::FCACHE_BYTES) as usize];
let (t, high) = (0x2000_0000u64, 0x1_2000_0000u64);
assert_eq!(gex_abi::fcache_entry(t), gex_abi::fcache_entry(high));
let at = gex_abi::fcache_entry(t) as usize;
let entry = |mem: &[u8]| u64::from_le_bytes(mem[at..at + 8].try_into().unwrap());
fcache_write(&mut mem, high, 7);
assert_eq!(gex_abi::fcache_probe(entry(&mem), high), 7);
assert_eq!(gex_abi::fcache_probe(entry(&mem), t), RESOLVE);
fcache_write(&mut mem, t, 8);
assert_eq!(gex_abi::fcache_probe(entry(&mem), t), 8);
assert_eq!(gex_abi::fcache_probe(entry(&mem), high), RESOLVE);
// A Wine image near the top of a 16 GiB memory.
fcache_write(&mut mem, 0x3_ffdd_1ce4, 9);
let at = gex_abi::fcache_entry(0x3_ffdd_1ce4) as usize;
assert_eq!(gex_abi::fcache_probe(u64::from_le_bytes(mem[at..at + 8].try_into().unwrap()), 0x3_ffdd_1ce4), 9);
// The other address clearing the slot leaves it; its own clears it.
fcache_clear(&mut mem, 0x3_ffdd_1ce4 + (1 << 32));
assert_ne!(u64::from_le_bytes(mem[at..at + 8].try_into().unwrap()), 0);
fcache_clear(&mut mem, 0x3_ffdd_1ce4);
assert_eq!(u64::from_le_bytes(mem[at..at + 8].try_into().unwrap()), 0);
// What no entry can hold misses rather than wraps.
fcache_write(&mut mem, 1 << 40, 3);
let at = gex_abi::fcache_entry(1 << 40) as usize;
assert_eq!(u64::from_le_bytes(mem[at..at + 8].try_into().unwrap()), 0);
}
#[test]
fn custom_sections_parse() {
// A module with only a `gex.funcs` section listing two addresses.
+2 -2
View File
@@ -426,12 +426,12 @@ fn build(u: &Unit, plan: &Plan) -> Result<Vec<u8>> {
s.instruction(&I::I32Const(probe as i32));
s.instruction(&I::I64Load(wasm_encoder::MemArg { offset: gex_abi::FCACHE, align: 3, memory_index: 0 }));
s.instruction(&I::LocalTee(4));
s.instruction(&I::I64Const(32));
s.instruction(&I::I64Const(gex_abi::FCACHE_TAG_BITS as i64));
s.instruction(&I::I64ShrU);
s.instruction(&I::I32WrapI64);
s.instruction(&I::I32Const(gex_abi::ftab::RESOLVE as i32));
s.instruction(&I::LocalGet(4));
s.instruction(&I::I64Const(0xffff_ffff));
s.instruction(&I::I64Const(gex_abi::FCACHE_TAG_MASK as i64));
s.instruction(&I::I64And);
s.instruction(&I::I64Const(addr as i64));
s.instruction(&I::I64Eq);
+2
View File
@@ -463,6 +463,8 @@ const host = new ProcessHost({
trapStack: process.env.GEX_TRAP_STACK ? 1 : undefined,
// GEX_DOOR_STATS: wineserver door call counts every 10 s.
doorStats: process.env.GEX_DOOR_STATS ? Number(process.env.GEX_DOOR_STATS) || 1 : undefined,
// GEX_FCACHE_STATS=1: the function probe cache's misses, every 10 s.
fcacheStats: process.env.GEX_FCACHE_STATS ? 1 : undefined,
// GEX_HEARTBEAT: say where the guest is every N safepoints and every 200
// dispatch misses, so a slow run can be told from a stuck one.
heartbeat: process.env.GEX_HEARTBEAT ? Number(process.env.GEX_HEARTBEAT) : 0,
+1
View File
@@ -1069,6 +1069,7 @@ async function handle(msg) {
if (msg.phases || msg.opts?.phases) gex.shim.exports.gex_set_phases?.(msg.phases || msg.opts.phases);
if (msg.inPlace === 0) gex.shim.exports.gex_set_in_place?.(0);
if (msg.excTrace) gex.shim.exports.gex_set_exc_trace?.(1);
if (msg.fcacheStats) gex.shim.exports.gex_set_fcache_stats?.(1);
if (msg.winsize) {
gex.shim.exports.gex_set_winsize?.(msg.winsize.rows, msg.winsize.cols);
}
+48
View File
@@ -1748,6 +1748,9 @@ pub extern "C" fn gex_ftab_size() -> u32 {
#[no_mangle]
pub extern "C" fn gex_ftab_lookup(_ctx: i32, addr: i64) -> i32 {
let addr = addr as u64;
if FCACHE_STATS.load(Ordering::Relaxed) {
fcache_stat(addr);
}
// Filler no function has (`STRAY_PC`): the fallback's.
if addr == STRAY_PC {
return gex_abi::ftab::FALLBACK as i32;
@@ -1804,6 +1807,51 @@ pub extern "C" fn gex_ftab_lookup(_ctx: i32, addr: i64) -> i32 {
idx as i32
}
/// `?fcachestats=1` (GEX_FCACHE_STATS=1): the probe cache's misses (every
/// `gex.lookup` is one), said by each Worker at most every 10 s, with how
/// many were above 4 GiB and the addresses that missed most. A debugging
/// opt-in: nothing is counted with it off.
static FCACHE_STATS: AtomicBool = AtomicBool::new(false);
#[no_mangle]
pub extern "C" fn gex_set_fcache_stats(on: i32) {
FCACHE_STATS.store(on != 0, Ordering::Relaxed);
}
thread_local! {
/// This Worker's misses since its last line: (since ns, above 4 GiB, by address).
static FCACHE_MISSES: std::cell::RefCell<(u64, u64, std::collections::HashMap<u64, u64>)> = Default::default();
}
fn fcache_stat(addr: u64) {
FCACHE_MISSES.with(|m| {
let mut m = m.borrow_mut();
let now = crate::now_ns();
if m.0 == 0 {
m.0 = now;
}
m.1 += (addr >> 32 != 0) as u64;
*m.2.entry(addr).or_insert(0) += 1;
let secs = now.saturating_sub(m.0) as f64 / 1e9;
if secs < 10.0 {
return;
}
let n: u64 = m.2.values().sum();
let mut top: Vec<(u64, u64)> = m.2.iter().map(|(a, c)| (*c, *a)).collect();
top.sort_unstable_by(|a, b| b.cmp(a));
let top: Vec<String> = top.iter().take(5).map(|(c, a)| format!("{a:#x} {c}")).collect();
crate::warn_line(&format!(
"[gex] fcache: pid {} {n} probe misses in {secs:.1} s ({:.0}/s), {} above 4 GiB, {} addresses; most: {}\n",
crate::gex_my_pid(),
n as f64 / secs,
m.1,
m.2.len(),
top.join(", ")
));
*m = (now, 0, Default::default());
});
}
/// The table's index 1: code no function body holds, under the `F_body`
/// contract (the call-out, docs/function-tier.md §7.3, as natively): tier-1
/// runs the target nested -- JavaScript's loop inside this call -- until the
+64
View File
@@ -0,0 +1,64 @@
#!/bin/sh
# The function table's probe cache in a 64-bit memory (gex_abi::FCACHE): two
# functions 4 GiB apart, at t and t + 4 GiB, share a slot (the slot hashes the
# low 32 bits) and their low 32 bits. Called in turn through pointers from
# the function JIT's code, each call must reach its own function: an entry
# that kept only the low 32 bits sent every call at t to the function at
# t + 4 GiB (and a 64-bit memory's images, near the top of its 16 GiB, never
# hit at all).
#
# tests/browser-mem64-fcache.sh
#
# Needs zig, node, and in web/public/gex the 64-bit memory's x86 shim and
# seed (GEX_ARCH=x86 GEX_SHARED=1 GEX_MEM64=1 shims/browser/build.sh).
set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
WORK="${TMPDIR:-/tmp}/gex-mem64-fcache-$$"
export GEX_ARCH=x86
P="$ROOT/web/public/gex"
for need in "$P/gex_browser.x86.m64.shared.wasm" "$P/seed.x86.m64.shared.wasm"; do
[ -e "$need" ] || { echo "skip: $need is not built" >&2; exit 0; }
done
command -v zig >/dev/null || { echo "skip: no zig to build the guest" >&2; exit 0; }
mkdir -p "$WORK"
[ -n "${KEEP:-}" ] || trap 'rm -rf "$WORK"' EXIT
cat > "$WORK/fc.c" <<'EOF2'
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
typedef int (*fn)(void);
static void *code_at(uint64_t a, int v) {
unsigned char *p = mmap((void *)a, 4096, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED_NOREPLACE, -1, 0);
if (p == MAP_FAILED || (uint64_t)p != a) return 0;
p[0] = 0xb8; memcpy(p + 1, &v, 4); p[5] = 0xc3;
mprotect(p, 4096, PROT_READ | PROT_EXEC);
return p;
}
__attribute__((noinline)) static long calls(fn *f, int n) {
long s = 0;
for (int i = 0; i < n; i++) s += f[i & 1]();
return s;
}
int main(void) {
setvbuf(stdout, 0, _IONBF, 0);
uint64_t t = 0x20000000ull;
fn f[2] = { (fn)code_at(t, 1), (fn)code_at(t + (1ull << 32), 1000) };
if (!f[0] || !f[1]) { printf("FAIL: no code at %#llx and %#llx\n", (unsigned long long)t, (unsigned long long)(t + (1ull << 32))); return 1; }
long want = 0, got = 0;
for (int r = 0; r < 200; r++) { got += calls(f, 1000); want += 500L * 1 + 500L * 1000; }
printf("%s: %ld calls through functions at %#llx and %#llx (4 GiB apart): sum %ld, want %ld\n",
got == want ? "ok" : "FAIL", 200L * 1000, (unsigned long long)t, (unsigned long long)(t + (1ull << 32)), got, want);
return got != want;
}
EOF2
zig cc -target x86_64-linux-musl -O2 -static -o "$WORK/fc" "$WORK/fc.c"
GEX_SHARED=1 GEX_MEM64=1 GEX_FJIT=1 timeout "${TMO:-180}" node "$ROOT/shims/browser/js/run-node.mjs" --file /g/fc="$WORK/fc" --argv0 /g/fc "$P/seed.x86.m64.shared.wasm" "$WORK/fc" > "$WORK/out" 2> "$WORK/err" || true
got=$(grep -E '^(ok|FAIL)' "$WORK/out" || true)
case $got in
ok:*) echo "$got"; exit 0 ;;
esac
echo "FAIL: the probe cache in a 64-bit memory: ${got:-no result}" >&2
tail -8 "$WORK/err" >&2
exit 1
+1 -1
View File
@@ -23,7 +23,7 @@ static uint32_t ftab_idx(uint64_t t)
}
uint64_t slot = (uint32_t)((uint32_t)t * 0x9e3779b1u) >> (32 - fcache_bits);
uint64_t e = __atomic_load_n((uint64_t *)(uintptr_t)(fcache + 8 * slot), __ATOMIC_RELAXED);
return (e & 0xffffffffull) == (t & 0xffffffffull) ? (uint32_t)(e >> 32) : 0;
return (e & ((1ull << 36) - 1)) == t ? (uint32_t)(e >> 36) : 0 /* gex_abi::FCACHE_TAG_BITS, 36 */;
}
static uint64_t ctx_v;
+1 -1
View File
@@ -106,7 +106,7 @@ static uint32_t ftab_idx(uint64_t t)
}
uint64_t slot = (uint32_t)((uint32_t)t * 0x9e3779b1u) >> (32 - fcache_bits);
uint64_t e = __atomic_load_n((uint64_t *)(uintptr_t)(fcache + 8 * slot), __ATOMIC_RELAXED);
return (e & 0xffffffffull) == (t & 0xffffffffull) ? (uint32_t)(e >> 32) : 0; /* RESOLVE */
return (e & ((1ull << 36) - 1)) == t ? (uint32_t)(e >> 36) : 0 /* gex_abi::FCACHE_TAG_BITS, 36 */; /* RESOLVE */
}
void gexp_call_guest(uint64_t fn, gexp_call *c)
+2
View File
@@ -964,6 +964,8 @@ export function setGlobals(get, b = {}) {
globalThis.GEX_ARCH = b.arch === 'aarch64' ? 'aarch64' : 'x86';
if (get('trapstack')) globalThis.GEX_TRAP_STACK = 1;
if (get('doorstats')) globalThis.GEX_DOOR_STATS = Number(get('doorstats')) || 1;
// fcachestats=1: the function probe cache's misses, every 10 s (cgen.rs `fcache_stat`).
if (get('fcachestats')) globalThis.GEX_FCACHE_STATS = 1;
if (get('exittrace')) globalThis.GEX_EXIT_TRACE = 1;
if (get('callouttrace')) globalThis.GEX_CALLOUT_TRACE = 1;
// straytrap=1: a return to native Wine's pad past open call-outs stays a breakpoint (cgen.rs STRAY_TRAP).
+1
View File
@@ -1081,6 +1081,7 @@ async function runProgramHere(
// `?doorstats=1`: wineserver door call counts every 10 s (docs/wineserver-kernel.md).
// (a number past 1: the line's period in ms)
doorStats: globalThis.GEX_DOOR_STATS || undefined,
fcacheStats: globalThis.GEX_FCACHE_STATS || undefined,
// `?timescale=N&fixedtime=S` (benchmarks only): the monotonic clocks
// N times real speed, the wall clock starting at Unix time S, as
// pewasm's PEWASM_TIME_SCALE and PEWASM_FIXED_TIME.