integ: merge merge/gexcc-c-1011 467beaac

# Conflicts:
#	shims/browser/src/lib.rs
This commit is contained in:
velzie committed 2026-10-11 03:17:51 -04:00
commit 1050edc7c1
13 files changed
+513 -50

No files matched your search

+53 -2
View File
@@ -135,8 +135,29 @@ fn flags() -> RegSet {
RegSet::of(cf::FLAG_REGS)
}
/// V8's limit on one wasm function's body (`kV8MaxWasmFunctionSize`): a
/// module holding a bigger one does not compile at all. amd64 python3.13's
/// eval loop came to 8.3 MB as one function, and the browser crashed on it.
pub const V8_MAX_FUNCTION_BYTES: usize = 7_654_321;
/// The largest body [`emit`] keeps: V8's limit less room for what the
/// splice and the merger may add to a body's immediates.
/// `GEX_FJIT_MAX_BODY=<bytes>` lowers it natively, to exercise the fallback.
pub fn max_body_bytes() -> usize {
let cap = V8_MAX_FUNCTION_BYTES - (64 << 10);
std::env::var("GEX_FJIT_MAX_BODY").ok().and_then(|v| v.parse().ok()).map_or(cap, |v: usize| v.min(cap))
}
/// Whether an emitted function and its outlined loops all fit
/// ([`max_body_bytes`]).
pub fn fits(body: usize, loops: &[usize], max: usize) -> bool {
body <= max && loops.iter().all(|&l| l <= max)
}
/// Emit `f` (memory form, verified, `loops` = `outer_loops(f)` when
/// outlining). `None` when a loop function's CFG is irreducible.
/// outlining). `None` when a loop function's CFG is irreducible, or when
/// the function or one of its loops is over V8's size limit: the caller
/// leaves it to tier-1, block by block.
/// Blocks above which a function is laid out flat in the browser (see `emit`).
const FLAT_ABOVE: usize = 1500;
@@ -329,7 +350,20 @@ pub fn emit(m: &ModuleCx<'_>, f: &Function, loops: &[Loop], stats: &mut Stats) -
e.top(0);
e.b.f().unreachable();
let body = e.b.finish();
e.stats.code_bytes += body.len() + lfns.iter().map(|l| l.len()).sum::<usize>();
let loop_lens: Vec<usize> = lfns.iter().map(|l| l.len()).collect();
if !fits(body.len(), &loop_lens, max_body_bytes()) {
if std::env::var_os("GEX_FJIT_STATS").is_some() {
eprintln!(
"[fjit] {:#x}{}: {} bytes (loops {:?}), over V8's function size limit; left to tier-1",
f.entry,
f.name.as_deref().map(|n| format!(" ({n})")).unwrap_or_default(),
body.len(),
loop_lens
);
}
return None;
}
e.stats.code_bytes += body.len() + loop_lens.iter().sum::<usize>();
Some(Emitted { body, loops: lfns })
}
@@ -1639,3 +1673,20 @@ impl E<'_, '_> {
}
}
}
#[cfg(test)]
mod size_tests {
use super::*;
#[test]
fn a_function_over_v8s_size_limit_is_left_to_tier1() {
let max = V8_MAX_FUNCTION_BYTES - (64 << 10);
assert!(fits(1000, &[], max));
assert!(fits(max, &[max], max));
// amd64 python3.13's eval loop: 8.3 MB as one function.
assert!(!fits(8_300_000, &[], max));
// A small body whose outlined loop is too big goes too.
assert!(!fits(1000, &[10, V8_MAX_FUNCTION_BYTES], max));
assert!(max_body_bytes() <= max);
}
}
+65 -7
View File
@@ -1296,12 +1296,12 @@ impl<'a> Lifter<'a> {
double,
a,
}),
// Wasm's `nearest` breaks ties to even. FRINTA breaks
// them away from zero, and FRINTX/FRINTI follow the FPCR
// mode, which gex does not track in fast mode — so all
// four agree except exactly on halfway values.
// FRINTA breaks ties away from zero; wasm's `nearest`
// breaks them to even, so it is built (`round_away`).
FpOp1::RoundAway => self.round_away(double, a),
// FRINTX/FRINTI follow the FPCR mode, which gex does not
// track in fast mode: to nearest, ties to even, as FRINTN.
FpOp1::RoundNearest
| FpOp1::RoundAway
| FpOp1::RoundExact
| FpOp1::RoundCurrent => self.b.def(Expr::FpUn {
op: FpUnOp::RoundNearest,
@@ -3087,10 +3087,16 @@ impl<'a> Lifter<'a> {
VOp2::FrintP => Some(VLaneUnOp::Ceil),
VOp2::FrintM => Some(VLaneUnOp::Floor),
VOp2::FrintZ => Some(VLaneUnOp::Trunc),
// Ties-to-even stands in for every nearest variant (PLAN §5.7).
VOp2::FrintN | VOp2::FrintA | VOp2::FrintX | VOp2::FrintI => Some(VLaneUnOp::Nearest),
// Ties to even for FRINTN, and for FRINTX/FRINTI (the FPCR mode,
// not tracked; PLAN §5.7). FRINTA is built below.
VOp2::FrintN | VOp2::FrintX | VOp2::FrintI => Some(VLaneUnOp::Nearest),
_ => None,
};
if op == VOp2::FrintA && matches!(lane, VLane::F32 | VLane::F64) {
let r = self.round_away_lanes(lane, a);
self.commit_vector(vd, arr, r);
return Flow::Next;
}
if let Some(un) = un {
let r = self.b.def(Expr::VLaneUn { op: un, lane, a });
self.commit_vector(vd, arr, r);
@@ -4175,6 +4181,58 @@ impl<'a> Lifter<'a> {
(lt, eq, not_lt, unordered)
}
/// FRINTA: to nearest, ties away from zero. `t = trunc(a)`, and one
/// more step away from zero when `|a - t| >= 0.5`. `a - t` is exact (the
/// fraction of `a`), so no value near a half is misjudged, unlike
/// `trunc(a + copysign(0.5, a))`, which rounds 0.49999999999999994 up.
/// A NaN or an infinity compares false and stays itself; -0.3 stays -0.
fn round_away(&mut self, double: bool, a: Value) -> Value {
let (half, one, ty) = if double {
(self.b.const_i64(0.5f64.to_bits()), self.b.const_i64(1.0f64.to_bits()), Ty::I64)
} else {
(self.b.const_i32(0.5f32.to_bits()), self.b.const_i32(1.0f32.to_bits()), Ty::I32)
};
let t = self.b.def(Expr::FpUn { op: FpUnOp::Trunc, double, a });
let frac = self.fp_bin(FpBinOp::Sub, double, a, t);
let mag = self.b.def(Expr::FpUn { op: FpUnOp::Abs, double, a: frac });
let up = self.b.def(Expr::FpCmp { op: FpCmpOp::Ge, double, a: mag, b: half });
let step = self.fp_bin(FpBinOp::Copysign, double, one, a);
let away = self.fp_bin(FpBinOp::Add, double, t, step);
self.b.def(Expr::Select { cond: up, a: away, b: t, ty })
}
/// [`Self::round_away`] lane-wise, for the vector FRINTA.
fn round_away_lanes(&mut self, lane: VLane, a: Value) -> Value {
let splat = |bits: u64| -> Box<[u8; 16]> {
let mut v = [0u8; 16];
if lane == VLane::F64 {
for c in v.chunks_exact_mut(8) {
c.copy_from_slice(&bits.to_le_bytes());
}
} else {
for c in v.chunks_exact_mut(4) {
c.copy_from_slice(&(bits as u32).to_le_bytes());
}
}
Box::new(v)
};
let (half, one, sign) = if lane == VLane::F64 {
(0.5f64.to_bits(), 1.0f64.to_bits(), 1u64 << 63)
} else {
(0.5f32.to_bits() as u64, 1.0f32.to_bits() as u64, 1u64 << 31)
};
let half = self.b.def(Expr::VConst(splat(half)));
let one = self.b.def(Expr::VConst(splat(one)));
let sign = self.b.def(Expr::VConst(splat(sign)));
let t = self.b.def(Expr::VLaneUn { op: VLaneUnOp::Trunc, lane, a });
let frac = self.b.def(Expr::VLaneBin { op: VLaneBinOp::Sub, lane, a, b: t });
let mag = self.b.def(Expr::VLaneUn { op: VLaneUnOp::Abs, lane, a: frac });
let up = self.b.def(Expr::VLaneBin { op: VLaneBinOp::GeS, lane, a: mag, b: half });
let step = self.b.def(Expr::VBitSelect { mask: sign, a, b: one });
let away = self.b.def(Expr::VLaneBin { op: VLaneBinOp::Add, lane, a: t, b: step });
self.b.def(Expr::VBitSelect { mask: up, a: away, b: t })
}
fn fp_bin(&mut self, op: FpBinOp, double: bool, a: Value, b: Value) -> Value {
self.b.def(Expr::FpBin { op, double, a, b })
}
+70 -11
View File
@@ -34,6 +34,11 @@ fn batch_limit() -> usize {
#[derive(Default)]
pub struct Jit {
st: Mutex<State>,
/// Acquisitions of `st`, and how many found it held (`GEX_FJIT_STATS`):
/// translations from several threads (the browser's Workers, the
/// native runner's) share one translator.
locks: std::sync::atomic::AtomicU64,
contended: std::sync::atomic::AtomicU64,
}
/// What recovery learned of one process's code ([`Jit::image`] starts it over).
@@ -48,6 +53,10 @@ struct Learned {
noreturn: BTreeSet<u64>,
/// Entries some translation holds.
translated: HashSet<u64>,
/// Entries a translation in progress (on another thread) holds: left
/// out of other batches as the translated are, so two threads do not
/// emit the same callees.
in_flight: HashSet<u64>,
/// Read-only segments of the images the loader told us about: a jump
/// table read from one is as fixed as it is ahead of time. (Not
/// tracked past `mprotect`: data the guest makes writable later counts
@@ -63,6 +72,11 @@ struct State {
/// Processes [`Jit::forget`] dropped: a translation still in flight for
/// one must not bring its state back.
gone: HashSet<u64>,
/// Bumped by whatever invalidates what was learned ([`Jit::image`],
/// [`Jit::retire`], [`Jit::forget`]): a translation that saw another
/// value at its start was recovered from what is no longer so, and
/// marks nothing translated.
epoch: u64,
/// For `GEX_FJIT_STATS`: translations, functions, time in recovery and
/// emission, module bytes.
modules: u64,
@@ -73,9 +87,22 @@ struct State {
}
impl Jit {
fn lock(&self) -> std::sync::MutexGuard<'_, State> {
use std::sync::atomic::Ordering::Relaxed;
self.locks.fetch_add(1, Relaxed);
match self.st.try_lock() {
Ok(g) => g,
Err(std::sync::TryLockError::Poisoned(e)) => e.into_inner(),
Err(std::sync::TryLockError::WouldBlock) => {
self.contended.fetch_add(1, Relaxed);
self.st.lock().unwrap_or_else(|e| e.into_inner())
}
}
}
pub fn new(intrinsics: HashMap<u64, Intrinsic>) -> Jit {
let jit = Jit::default();
jit.st.lock().unwrap_or_else(|e| e.into_inner()).procs.entry(0).or_default().intrinsics = std::sync::Arc::new(intrinsics);
jit.lock().procs.entry(0).or_default().intrinsics = std::sync::Arc::new(intrinsics);
jit
}
@@ -83,7 +110,11 @@ impl Jit {
/// starts with what the loader knows of that image and nothing else --
/// a new process, or an `execve` over an old one.
pub fn image(&self, process: u64, intrinsics: HashMap<u64, Intrinsic>, image: &gex_ld::Image, elf: Option<&[u8]>) {
self.st.lock().unwrap_or_else(|e| e.into_inner()).procs.insert(process, Learned { intrinsics: std::sync::Arc::new(intrinsics), ..Learned::default() });
{
let mut st = self.lock();
st.epoch += 1;
st.procs.insert(process, Learned { intrinsics: std::sync::Arc::new(intrinsics), ..Learned::default() });
}
self.add_image_in(process, image, elf);
}
@@ -97,7 +128,8 @@ impl Jit {
/// `_xcb_in_read_block` before its `cmovle` as a tail call, flags dead,
/// and it read the X setup reply out of an empty queue.
pub fn retire(&self, process: u64, ranges: &[(u64, u64)]) {
let mut st = self.st.lock().unwrap_or_else(|e| e.into_inner());
let mut st = self.lock();
st.epoch += 1;
let Some(l) = st.procs.get_mut(&process) else { return };
for &(lo, hi) in ranges.iter().filter(|r| r.1 > r.0) {
let gone: Vec<u64> = l.starts.range(lo..hi).map(|(&a, _)| a).collect();
@@ -112,7 +144,8 @@ impl Jit {
/// Process `process` is gone.
pub fn forget(&self, process: u64) {
let mut st = self.st.lock().unwrap_or_else(|e| e.into_inner());
let mut st = self.lock();
st.epoch += 1;
st.procs.remove(&process);
st.gone.insert(process);
}
@@ -138,7 +171,7 @@ impl Jit {
fn add_image_in(&self, process: u64, image: &gex_ld::Image, elf: Option<&[u8]>) -> usize {
let code = Code::with_elf(image, elf);
let (starts, _) = func::seeds(&code, elf, false);
let mut guard = self.st.lock().unwrap_or_else(|e| e.into_inner());
let mut guard = self.lock();
let st = guard.procs.entry(process).or_default();
let mut n = 0;
for (a, info) in starts {
@@ -219,8 +252,13 @@ impl Jit {
if std::env::var("GEX_FJIT_STATS").as_deref() == Ok("2") {
eprintln!("[fjit] translate {addr:#x}: bytes {:02x?}", &mem[addr as usize..addr as usize + 16]);
}
let mut guard = self.st.lock().unwrap_or_else(|e| e.into_inner());
let State { procs, gone, modules, functions: n_functions, recover_us, emit_us, bytes } = &mut *guard;
// Under the lock: recovery (it reads and extends what was learned).
// Not under it: summaries and emission, most of the time -- other
// threads' translations go on meanwhile (in the browser, a Worker
// waiting here waits in `memory.atomic.wait`).
let mut guard = self.lock();
let epoch = guard.epoch;
let State { procs, gone, .. } = &mut *guard;
// A process already gone (a late request): what it learns goes nowhere.
let mut scratch = Learned::default();
let st = if gone.contains(&process) { &mut scratch } else { procs.entry(process).or_default() };
@@ -237,7 +275,7 @@ impl Jit {
if batch.len() >= limit {
break;
}
if batch.contains_key(&a) || (a != addr && (st.translated.contains(&a) || skip(a))) || !executable(a) {
if batch.contains_key(&a) || (a != addr && (st.translated.contains(&a) || st.in_flight.contains(&a) || skip(a))) || !executable(a) {
continue;
}
let Learned { starts, noreturn, .. } = &mut *st;
@@ -329,6 +367,8 @@ impl Jit {
st.noreturn.insert(a);
}
}
st.in_flight.extend(order.iter().copied());
drop(guard);
let sums = summary::analyze_functions(&batch).map;
let recovered = now_us().saturating_sub(started);
let functions: Vec<Function> = order.iter().filter_map(|a| batch.remove(a)).collect();
@@ -350,6 +390,7 @@ impl Jit {
gex_fjit::runtime(functions, &sums, class32, page_gen, &builtins, &pops)
}))
.map_err(|p| p.downcast_ref::<String>().cloned().or_else(|| p.downcast_ref::<&str>().map(|s| s.to_string())).unwrap_or_default());
let mut done = Vec::new();
let out = match emitted {
Err(why) => {
eprintln!("[fjit] {addr:#x}: the emitter panicked ({why}); left to the fallback");
@@ -359,7 +400,7 @@ impl Jit {
if std::env::var("GEX_FJIT_STATS").as_deref() == Ok("2") {
eprintln!("[fjit] {addr:#x}: {es:?}");
}
st.translated.extend(entries);
done = entries;
if let Ok(dir) = std::env::var("GEX_FJIT_KEEP") {
let _ = std::fs::create_dir_all(&dir);
let _ = std::fs::write(format!("{dir}/p{process}-{addr:x}.wasm"), &wasm);
@@ -377,20 +418,38 @@ impl Jit {
None
}
};
let mut guard = self.lock();
let same = guard.epoch == epoch;
let State { procs, modules, functions: n_functions, recover_us, emit_us, bytes, .. } = &mut *guard;
if let Some(st) = procs.get_mut(&process) {
for a in &order {
st.in_flight.remove(a);
}
if same {
st.translated.extend(done);
}
}
*modules += 1;
*n_functions += n as u64;
*recover_us += recovered;
*emit_us += now_us().saturating_sub(started).saturating_sub(recovered);
*bytes += out.as_ref().map_or(0, |w| w.len() as u64);
if std::env::var_os("GEX_FJIT_STATS").is_some() && modules.is_power_of_two() {
eprintln!("[fjit] {}", stats_line(*modules, *n_functions, *recover_us, *emit_us, *bytes));
eprintln!("[fjit] {}; {}", stats_line(*modules, *n_functions, *recover_us, *emit_us, *bytes), self.lock_line());
}
out
}
/// What the JIT has done, for `GEX_FJIT_STATS` and `GEX_NT_STATS`.
pub fn stats(&self) -> String {
self.st.lock().unwrap_or_else(|e| e.into_inner()).line()
let line = self.lock().line();
format!("{line}; {}", self.lock_line())
}
/// How often the translator's lock was found held.
fn lock_line(&self) -> String {
use std::sync::atomic::Ordering::Relaxed;
format!("lock {} of {} contended", self.contended.load(Relaxed), self.locks.load(Relaxed))
}
}
+54
View File
@@ -1541,3 +1541,57 @@ each time in one guest process tree under a wasm32 dash: zt check/checkgz,
zstt check/checkd/checkm, and sqlite3 on sq.sql and cb.sql. It passes when
19 checks say ok in both modes, all three libraries are replaced in the
provider run only, and the sqlite3 output matches.
## 8. A translation cache that survives a page reload (design, 2026-10-11)
### Why
In the page, a cold `node -e 0` takes 40.6 s and a warm one about 30 s. Most of the difference is 9.6 s spent translating 224k blocks (§6.6). Every reload starts with an empty module store, so it pays that 9.6 s again. Backlog item 3 (C) and T5.
### What is cached, and what is not
The cache saves the **global kernel's module store** and the slot numbering its modules were built against. It does not save compiled `WebAssembly.Module`s or any process's registry. Each saved record holds:
- every stored module: its wasm bytes (what `k_mod_store` received), its `slots`, and its `dirs` (`(page, base)` pairs);
- the claims: `by_page` (`(page, key) -> ids`), `by_object` (`obj -> ids`), and the `translated` object set;
- the slot map (`(page, key) -> base`, from `k_slot_put`) and the slot mark.
### What the cache keys on
- **A fingerprint for the whole cache.** It hashes:
- the shim and kernel wasm bytes the page loaded (arch, mem64 and shared variants are different files);
- the query switches that change emission (`fjit`, `dense`, `dual`, `fjitlibs`).
If the fingerprint differs, the whole cache is dropped. This also covers Rust's `DefaultHasher`, which the object and page identities use and whose output is not promised to stay the same across toolchains.
- **The content of each object.** Today an object's identity is SipHash of its file path, and a page's identity is SipHash of path plus offset (`object_identity` and `page_identity` in shims/browser/src/lib.rs). Neither depends on the file's bytes, so a library that apt replaced at the same path would keep its old translations.
The cache therefore stores, per object, its path and a hash of the file's bytes as they were when saved. To make that possible, the shim tells the kernel once per object which path each key it uses belongs to (a new `k_obj_name(key, path)`).
On restore, the kernel hashes the file now at that path in this page's filesystem. If the hash differs, or the file is gone, it drops that object's modules, claims and slot-map entries. Those slots are then left unused, which wastes them but is safe.
- **Placement.** Keys include absolute dispatch pages. A library loaded at a different address simply misses; nothing is relocated.
### Renumber on install
- The kernel store already holds modules before `dense::register` numbers them. `share_module` runs before `push_pending`, and `push_pending` numbers each process's copy from its own `NEXT` (shims/browser/src/dense.rs).
- So the saved bytes are unnumbered, and every process that installs a restored module renumbers it, exactly as it renumbers a module another process stored this session.
- This answers T1 for the cache. Within one session, a Worker's dense base already comes from its own numbering; fork adoption takes the parent's base on purpose. A stored compiled `Module` or registry entry would carry an earlier session's base, so neither is ever saved.
- Before saving, each module's dense section is checked for base 0, and zeroed with `gex_emit::dense::set_base` if not.
### Slot-map replay
- Classic tier-1 slot bases come from `k_slot_alloc`. They are baked into each module's element segments and into the `PAGE_GEN` values that function-JIT code checks. A saved module is valid only where `(page, key)` maps to the bases it was built with.
- Restore therefore runs in the kernel before pid 1 starts, which is before any `k_slot_alloc`:
- it replays every saved `(page, key) -> base` with `k_slot_put`;
- it raises the mark with `k_slot_floor` to the highest saved base plus that page's slots.
- Modules are re-stored in their saved order, so module ids, which are store positions, come back the same. The claims are then replayed.
- If the restored slots would leave less than half of `k_slot_left` free, nothing is restored. Slots are a budget for the whole page, and no slot is ever reclaimed.
### Where it lives
- **Storage:** one OPFS file per fingerprint, holding a header and the records, written from the page's main thread.
- **Saving:** a kernel export serialises the store (`k_store_save`). The host calls it when 30 s pass with no new module, and on `pagehide`. Writes are capped at 128 MiB of module bytes, against the store's own 256 MiB `MOD_BUDGET`.
- **Restoring:** `k_store_restore(bytes)` runs once at boot, after the filesystem is ready and before the first process.
- **Opt-in first:** behind `?xcache=1`. It becomes the default only after the measurement below.
### Who else uses the store
- W's wasm DLLs and the Mesa work use the same store and slot map. Their objects get keys, paths and content hashes the same way. Nothing changes for code that does not opt in.
- The one change visible in the shim is the new `k_obj_name` call next to the existing claims.
### Measured how
- On edger, in the page, measure cold `node -e 0` and `apt install ripgrep tcc libc6-dev`.
- Load twice in one browser profile. The second load runs with `?xcache=1` for "after", and without it for "before". This needs site.mjs to have a mode that reloads with a persistent profile.
- Report translation counts as well as times.
+67 -9
View File
@@ -1849,7 +1849,44 @@ export class Gex {
#processImage() {
const t0 = performance.now();
const e = this.#shim.exports;
const out = [{ at: 0, bytes: this.#bytes().slice(0, LOW_END) }];
// Only the runs of pages that are not all zero, scanned in place: a
// child's memory starts zeroed, so a zero page needs no bytes, and
// slicing it whole first (637 MiB of zeros for a node child) was most of
// the cost of a fork, paid again by the child scanning the copy for
// zeros (`writeSparse`). Each mapped region also goes as an entry of its
// own with no bytes (`end`, `prot`, and `zeros`, how much of it was
// left out); runs are `dense`, written without a scan.
const u8 = this.#bytes();
const words = new Int32Array(this.#memory.buffer, 0, u8.length >> 2);
const blank = (a, b) => {
for (let i = a >> 2; i < b >> 2; i++) if (words[i] !== 0) return false;
for (let i = b & ~3; i < b; i++) if (u8[i] !== 0) return false;
return true;
};
const out = [];
const runs = (start, end) => {
const PAGE = 4096;
let zeros = 0;
let i = start;
while (i < end) {
const pe = Math.min(i + PAGE, end);
if (blank(i, pe)) {
zeros += pe - i;
i = pe;
continue;
}
let run = pe;
while (run < end) {
const next = Math.min(run + PAGE, end);
if (blank(run, next)) break;
run = next;
}
out.push({ at: i, bytes: u8.slice(i, run), dense: true });
i = run;
}
return zeros;
};
out.push({ at: 0, bytes: new Uint8Array(0), zeros: runs(0, LOW_END) });
const scratch = e.gex_alloc(24) >>> 0;
const dv = new DataView(this.#memory.buffer);
const n = e.gex_vma_count();
@@ -1860,7 +1897,11 @@ export class Gex {
const prot = Number(dv.getBigUint64(scratch + 16, true));
// `prot` travels with the bytes: the child rebuilds its memory *map* from
// these, and a map is not recoverable from the memory it describes.
if (end > start) out.push({ at: start, end, prot, bytes: this.#bytes().slice(start, end) });
if (end > start) {
const r = { at: start, end, prot, bytes: new Uint8Array(0), zeros: 0 };
out.push(r);
r.zeros = runs(start, end);
}
}
e.gex_free?.(scratch, 24);
// A checksum per region, so the child can say whether what it runs on is
@@ -2162,7 +2203,7 @@ export class Gex {
step('copying the image');
// Grow to cover the parent's mapped regions before writing a byte, or the
// copy throws `RangeError: offset is out of bounds` above the initial size.
this.#growTo(child.image.reduce((n, r) => Math.max(n, r.at + r.bytes.byteLength), 0));
this.#growTo(child.image.reduce((n, r) => Math.max(n, r.end ?? 0, r.at + r.bytes.byteLength), 0));
const u8 = this.#bytes();
// `?forkskip=N`: leave any region larger than N MiB uncopied.
//
@@ -2187,6 +2228,12 @@ export class Gex {
skipped += n;
continue;
}
zeros += r.zeros ?? 0;
if (r.dense) {
u8.set(new Uint8Array(r.bytes), r.at);
copied += n;
continue;
}
const [wrote, blank] = writeSparse(u8, new Uint8Array(r.bytes), r.at);
copied += wrote;
zeros += blank;
@@ -2708,7 +2755,7 @@ export class Gex {
// `WebAssembly.Module` (same registry bytes -> same module) rather than
// paying `WebAssembly.compile` again. The bytes are still needed for the
// metadata and table-need computation the install does regardless.
const key = moduleContentKey(bytes);
const key = this.#registryKey(i, bytes);
// Or one the page's unit cache has (another process's).
const cached = (key ? this.#sharedModules.get(key) : undefined) ?? this.#unitGet(this.#unitKey(bytes)) ?? undefined;
let who = 'jit2049';
@@ -2779,7 +2826,7 @@ export class Gex {
// No metadata means nothing to key a later fill on -- install it eagerly,
// as the fork path does (`#adoptModule`), reusing a sibling's compile.
if (!meta || !meta.length) {
await this.#installModule(bytes, need, 64, null, 'lazy-nometa', this.#sharedModules.get(moduleContentKey(bytes)));
await this.#installModule(bytes, need, 64, null, 'lazy-nometa', this.#sharedModules.get(this.#registryKey(i, bytes)));
eager++;
continue;
}
@@ -2788,14 +2835,14 @@ export class Gex {
const got = e.gex_add_lazy_module(gp, meta.length) >>> 0;
e.gex_free?.(gp, meta.length);
if (!got) {
await this.#installModule(bytes, need, 64, meta, 'lazy-noneed', this.#sharedModules.get(moduleContentKey(bytes)));
await this.#installModule(bytes, need, 64, meta, 'lazy-noneed', this.#sharedModules.get(this.#registryKey(i, bytes)));
eager++;
continue;
}
// Keep a sibling's compiled Module when we have it, so the on-demand
// instantiate skips the compile too; otherwise the raw bytes. The push
// order matches `gex_add_lazy_module`'s ordinal (both count from 0 here).
this.#lazyInherited.push({ mod: this.#sharedModules.get(moduleContentKey(bytes)) ?? bytes, need, inst: null, meta });
this.#lazyInherited.push({ mod: this.#sharedModules.get(this.#registryKey(i, bytes)) ?? bytes, need, inst: null, meta });
lazy++;
}
if (globalThis.GEX_ATTACH_TRACE && (lazy || eager)) {
@@ -5199,6 +5246,17 @@ export class Gex {
#moduleStore = null;
/** Registry module `i`'s bytes (a copy), wherever they are kept; null if none. */
/** Registry module `i`'s content key: as the store's writer recorded it
* when the module is in the store, so a catching-up Worker does not hash
* bytes a sibling already hashed; computed from `bytes` otherwise. */
#registryKey(i, bytes) {
const e = this.#shim.exports;
if (e.gex_module_key && e.gex_module_ext && Number(e.gex_module_ext(i))) {
return `${bytes.length}:${e.gex_module_key(i, 0) >>> 0}:${e.gex_module_key(i, 1) >>> 0}`;
}
return moduleContentKey(bytes);
}
#registryBytes(i) {
const e = this.#shim.exports;
const len = e.gex_module_len(i) >>> 0;
@@ -5310,7 +5368,7 @@ export class Gex {
const i = this.#installed;
const bytes = this.#registryBytes(i);
if (!bytes) continue;
const key = moduleContentKey(bytes);
const key = this.#registryKey(i, bytes);
const cached = key ? this.#sharedModules.get(key) : undefined;
this.#installModule(bytes, need, 64, null, 'nt-catchup', cached, true);
if (cached) continue;
@@ -6049,7 +6107,7 @@ export class Gex {
if (i < 0) return;
const bytes = this.#registryBytes(i);
if (!bytes) return;
const key = moduleContentKey(bytes);
const key = this.#registryKey(i, bytes);
const cached = key ? this.#sharedModules.get(key) : undefined;
this.#installModule(bytes, 0, 64, null, `dense-${i}`, cached, true);
this.#denseCatchUps++;
+2
View File
@@ -763,6 +763,8 @@ async function handle(msg) {
// late-starting thread was stranded `runnable`, and the main thread hung
// joining it. A thread's own exit already woke its joiner's futex; a thread
// that faults is handled by the Worker's error path and never reaches here.
// `?fjit=`: what the function JIT did, and how often its lock was held.
if (msg.kind !== 'thread') ex.gex_fjit_report?.();
if (msg.kind !== 'thread') ex.gex_process_gone?.(134);
// `close` before reporting: it drops the tick channel, a live handle that
// would otherwise keep a Node Worker up.
+33 -3
View File
@@ -467,14 +467,34 @@ fn translate_root(addr: u64) -> Option<u32> {
};
let class32 = gex_abi::X86 && gex_xlate::arch::compat();
trace(&format!("translate {addr:#x}"));
let Some(wasm) = jit.translate_skipping(0, crate::guest_bytes(), addr, &executable, &served, &read_only, &volatile, &pg, class32) else {
// Declined (or not holding `addr`): the pages numbered for it get their
// directory entries anyway, as an install would give them. A page with a
// base and no entry strands tier-1's blocks there (they go into that
// base, which is not fresh, so their unit writes no entry for it) --
// natively a livelock once the emitter left a function over V8's size
// limit to tier-1 (gex-fjit `max_body_bytes`); here the miss path's
// reconcile covers the faulting page, not the others.
let declined = |fresh: Vec<(u64, u32)>| {
DECLINED.fetch_add(1, Ordering::Relaxed);
let mut gm = GuestMem::new(crate::guest_bytes());
for (p, b) in fresh {
let at = gex_abi::DISPATCH_DIR + p * 4;
if gm.read_u32(at).unwrap_or(0) == 0 {
let _ = gm.write_u32(at, b);
}
}
};
let Some(wasm) = jit.translate_skipping(0, crate::guest_bytes(), addr, &executable, &served, &read_only, &volatile, &pg, class32) else {
trace(&format!("declined {addr:#x}"));
declined(fresh.into_inner());
return None;
};
let Some(funcs) = gex_kernel::ftab::module_funcs(&wasm) else {
declined(fresh.into_inner());
return None;
};
let funcs = gex_kernel::ftab::module_funcs(&wasm)?;
if !funcs.contains(&addr) {
DECLINED.fetch_add(1, Ordering::Relaxed);
declined(fresh.into_inner());
return None;
}
note_object(addr, funcs.len(), wasm.len());
@@ -692,3 +712,13 @@ pub extern "C" fn gex_fjit_installed(n: u32) {
let dirs = lock(&MODULES).get(n as usize).map(|m| m.dirs.clone()).unwrap_or_default();
crate::cgen::write_dirs(dirs);
}
/// The translator's line (`gex_xlate::fjit::Jit::stats`: modules, time,
/// and how often its lock was found held), for the host to print as the
/// process ends.
#[no_mangle]
pub extern "C" fn gex_fjit_report() {
if let (true, Some(jit)) = (on(), JIT.get()) {
crate::warn_line(&format!("[gex] fjit: {} (translated {}, declined {})\n", jit.stats(), TRANSLATED.load(Ordering::Relaxed), DECLINED.load(Ordering::Relaxed)));
}
}
+63 -14
View File
@@ -2709,7 +2709,7 @@ struct Jit {
/// `registry` itself. An entry kept there holds no bytes in this module's
/// heap: the registry grew without bound in a long fjit run (980 MiB of
/// Muck's 1670 MiB heap at the 2 GiB arena's OOM).
registry_ext: Vec<Option<(u64, u32)>>,
registry_ext: Vec<Option<(u64, u32, u64)>>,
/// The bytes of the last modules kept in the JS-side store, still alive:
/// a sibling Worker that read the pending queue's front just before this
/// one popped it is copying them out (`gex_jit_pending_refresh`).
@@ -6787,7 +6787,7 @@ pub extern "C" fn gex_syscall(ctx: i32, pc: i64) {
if frames_stay(h) {
// A wasm32 program's frames cannot be left: the call
// returns, and `#wasmprogSyscall` starts the thread.
WASM32_STAGED.store(true, std::sync::atomic::Ordering::Relaxed);
WASM32_STAGED.with(|c| c.set(true));
return 0;
}
// The stack goes back so the host can see the record and
@@ -6914,12 +6914,11 @@ pub extern "C" fn gex_syscall(ctx: i32, pc: i64) {
// the call returns, and `#wasmprogSyscall` hands the
// child to the host.
let start = gm.read_u64(kstate.ctx_addr + ctx::PC as u64).unwrap_or(0);
do_fork(h, pid, pc as u64, child_stack, tls);
FORK[1].store(start, std::sync::atomic::Ordering::Relaxed);
WASM32_STAGED.store(true, std::sync::atomic::Ordering::Relaxed);
do_fork(h, pid, pc as u64, child_stack, tls, Some(start));
WASM32_STAGED.with(|c| c.set(true));
return 0;
}
do_fork(h, pid, pc as u64, child_stack, tls);
do_fork(h, pid, pc as u64, child_stack, tls, None);
// Give the stack back though the parent carries on: the child
// is a copy of this memory that must be taken now, when the
// stack comes back. Letting the parent run on first copied it
@@ -7171,13 +7170,20 @@ fn frames_stay(h: &Host) -> bool {
/// A thread or a fork a wasm32 program's syscall staged without trapping
/// (`gex_wasmprog_staged`).
static WASM32_STAGED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
///
/// Per Worker: the Worker whose syscall staged it is the one that must start
/// it (a fork's child is a copy of memory as the call left it; see
/// `gex_fork_pending`). Shared, a sibling thread's syscall took the flag and
/// the forking Worker never drained its child.
thread_local! {
static WASM32_STAGED: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
}
/// Whether a wasm32 program's last syscall staged a thread or a child for
/// the host (and forget it).
#[no_mangle]
pub extern "C" fn gex_wasmprog_staged() -> i32 {
WASM32_STAGED.swap(false, std::sync::atomic::Ordering::Relaxed) as i32
WASM32_STAGED.with(|c| c.replace(false)) as i32
}
/// A wasm32 program's blocking call (cgen/wasmprog.rs): its frames cannot be
@@ -7213,7 +7219,7 @@ fn wasm32_wait(pc: u64) -> i32 {
}
}
fn do_fork(h: &mut Host, pid: i32, pc: u64, child_stack: u64, tls: Option<u64>) {
fn do_fork(h: &mut Host, pid: i32, pc: u64, child_stack: u64, tls: Option<u64>, start: Option<u64>) {
let ctx_addr = h.threads[cur()].kstate.ctx_addr;
// The parent carries straight on, keeping its memory, threads and tables:
// the child is a separate Worker and will not use them.
@@ -7238,11 +7244,13 @@ fn do_fork(h: &mut Host, pid: i32, pc: u64, child_stack: u64, tls: Option<u64>)
.chain(h.kernel.argv.iter().map(String::as_str))
.collect::<Vec<_>>()
.join("\0");
FORK[1].store(pc + gex_abi::SYSCALL_INST_BYTES, std::sync::atomic::Ordering::Relaxed);
FORK[1].store(start.unwrap_or(pc + gex_abi::SYSCALL_INST_BYTES), std::sync::atomic::Ordering::Relaxed);
FORK[2].store(ctx_addr, std::sync::atomic::Ordering::Relaxed);
FORK[3].store(child_stack, std::sync::atomic::Ordering::Relaxed);
FORK[4].store(tls.unwrap_or(0), std::sync::atomic::Ordering::Relaxed);
FORK[5].store(h.kernel.mem.brk_current(), std::sync::atomic::Ordering::Relaxed);
// Only the forking thread's Worker may claim it (`gex_fork_pending`).
FORK_BY.store(cur() as u64 + 1, std::sync::atomic::Ordering::Relaxed);
// Last, because it is what the host tests: the rest must be readable by
// the time a non-zero pid appears.
FORK[0].store(pid as u64, std::sync::atomic::Ordering::Release);
@@ -7587,10 +7595,28 @@ pub extern "C" fn gex_adopt_windows_space(lo: u32) -> i32 {
static FORK: [std::sync::atomic::AtomicU64; 6] =
[const { std::sync::atomic::AtomicU64::new(0) }; 6];
/// The thread (index + 1) whose `fork` is in `FORK`.
static FORK_BY: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
/// The pid of a child needing a Worker, or zero -- and claiming it, so a `fork`
/// several Workers can see is not started twice (see `gex_thread_pending`).
///
/// Only on the Worker running the thread that forked: the child is a copy of
/// memory as `fork` left it, and only that Worker copies it before the parent
/// runs on (synchronously after a wasm32 program's syscall, before the run
/// loop resumes tier-1's). A sibling thread's Worker that drained it instead
/// copied memory while the parent ran on -- for a wasm32 program, after
/// `__gex_fork` had unmapped the record the child starts on, so the child
/// read its thread pointer from a zero page and trapped (the aarch64 browser
/// flake in tests/wasm32 fork: "vfork child exit 137" with a parked thread
/// in the parent, 6 in 750 vforks; tests/wasm32/vforkthreads).
#[no_mangle]
pub extern "C" fn gex_fork_pending() -> i32 {
if FORK[0].load(std::sync::atomic::Ordering::Acquire) == 0
|| FORK_BY.load(std::sync::atomic::Ordering::Relaxed) != cur() as u64 + 1
{
return 0;
}
let pid = FORK[0].swap(0, std::sync::atomic::Ordering::AcqRel) as i32;
if pid != 0 {
CLAIMED_FORK.with(|c| {
@@ -8442,6 +8468,9 @@ fn do_exec(h: &mut Host, path: &str, argv: &[String], envp: &[String]) -> u64 {
FILL_ASKED.0.borrow_mut().clear();
FILL_FAILED.0.borrow_mut().clear();
SERVED_PAGE.0.borrow_mut().clear();
// And the bases blocks were installed under: the page table starts over
// below, and a page of the new program has nothing installed yet.
LIVE_BASE.0.borrow_mut().clear();
// And the modules inherited from the parent: they cover the PARENT's pages.
// The new program's image sits at the same PIE base, so a page number the
// parent's image had is now another program's code; a fill answered from
@@ -8518,6 +8547,16 @@ fn do_exec(h: &mut Host, path: &str, argv: &[String], envp: &[String]) -> u64 {
));
}
drop(closure);
// And the store modules this Worker installed before the exec: they are
// still in its tables, but nothing points at them now -- the page table
// and directory start over above -- so counting them installed made the
// serve path refuse every one of them, and the new program lifted what
// it could have been served. A program exec'd from a fork of node (the
// child had taken libc and libnode whole before its exec) lifted 66,079
// blocks, against 4,188 for a fresh `node -e 0`. Served again, a module
// writes the same blocks into the same slots, and its compile is shared
// by content key.
h.jit.installed_mods.clear();
let ctx_addr = h.threads[cur()].kstate.ctx_addr;
let c = ctx_addr as usize;
@@ -13724,11 +13763,11 @@ pub extern "C" fn gex_jit_installed() {
// take the front of the shared queue between this Worker's read and
// this pop, so the note is checked against what was popped): keep
// only where it is.
if let Some((off, len, _)) = ext.filter(|&(_, len, key)| len as usize == done.wasm.len() && key == content_key(&done.wasm)) {
if let Some((off, len, key)) = ext.filter(|&(_, len, key)| len as usize == done.wasm.len() && key == content_key(&done.wasm)) {
EXTERNAL_BYTES.fetch_add(len as u64, std::sync::atomic::Ordering::Relaxed);
h.jit.registry_sums.push(Vec::new());
h.jit.registry.push(Vec::new());
h.jit.registry_ext.push(Some((off, len)));
h.jit.registry_ext.push(Some((off, len, key)));
h.jit.registry_dense.push(done.dense.as_ref().map_or((0, 0), |d| (d.base, d.entries.len() as u32)));
h.jit.store_grace.push_back(done.wasm);
if h.jit.store_grace.len() > STORE_GRACE {
@@ -14066,7 +14105,17 @@ pub extern "C" fn gex_jit_note_external(off: u64, len: u32, key_hi: u32, key_lo:
/// bytes are in this module's memory (`gex_module_ptr`).
#[no_mangle]
pub extern "C" fn gex_module_ext(i: u32) -> u64 {
with_host(|h| h.jit.registry_ext.get(i as usize).copied().flatten().map_or(0, |(off, _)| off + 1))
with_host(|h| h.jit.registry_ext.get(i as usize).copied().flatten().map_or(0, |(off, _, _)| off + 1))
}
/// Registry module `i`'s content key, high half (`which` 0) or low (1), when
/// it is in the JS-side store (whose writer gave it); 0 and 0 otherwise. So a
/// Worker catching up names the module without hashing its bytes.
#[no_mangle]
pub extern "C" fn gex_module_key(i: u32, which: u32) -> u32 {
with_host(|h| {
h.jit.registry_ext.get(i as usize).copied().flatten().map_or(0, |(_, _, k)| if which == 0 { (k >> 32) as u32 } else { k as u32 })
})
}
/// MiB of registry modules kept in the JS-side store (1) and in this heap (0).
@@ -14108,7 +14157,7 @@ pub extern "C" fn gex_jit_slots_total() -> u32 {
#[no_mangle]
pub extern "C" fn gex_module_len(i: u32) -> u32 {
with_host(|h| match h.jit.registry_ext.get(i as usize).copied().flatten() {
Some((_, len)) => len,
Some((_, len, _)) => len,
None => h.jit.registry.get(i as usize).map_or(0, |m| m.len() as u32),
})
}
+20 -3
View File
@@ -883,10 +883,27 @@ fn run_job(j: &Job) -> Result<bool> {
let t0 = std::time::Instant::now();
let t = tr.translate(&req);
stats::TRANSLATE_US.fetch_add(t0.elapsed().as_micros() as u64, Relaxed);
let Some(t) = t else { return Ok(false) };
let funcs = gex_kernel::ftab::module_funcs(&t.wasm).ok_or_else(|| anyhow!("a translation for {addr:#x} has no gex.funcs section"))?;
// Declined (or not holding `addr`): the pages `page_gen` numbered for it
// still get their directory entries, as a publication would give them.
// Otherwise the page has a base and no entry, tier-1's translation of
// the address (into that base: not a fresh page, so no entry of its
// own) is never reached, and the miss repeats for ever ("translated but
// missed dispatch ... retranslation livelock"), as when the emitter
// leaves a function over V8's size limit to tier-1.
let declined = |why: Option<String>| -> Result<bool> {
let numbered = pages.lock().unwrap_or_else(|e| e.into_inner()).clone();
publish_bridges(sh, &[], &numbered)?;
match why {
Some(w) => bail!("{w}"),
None => Ok(false),
}
};
let Some(t) = t else { return declined(None) };
let Some(funcs) = gex_kernel::ftab::module_funcs(&t.wasm) else {
return declined(Some(format!("a translation for {addr:#x} has no gex.funcs section")));
};
if !funcs.contains(&addr) {
bail!("the translation for {addr:#x} does not hold it");
return declined(Some(format!("the translation for {addr:#x} does not hold it")));
}
let t1 = std::time::Instant::now();
let module = compile_translation(j, &t.wasm).with_context(|| format!("compiling the translation for {addr:#x}"))?;
+1 -1
View File
@@ -53,7 +53,7 @@ awk -v out="$OUT" '
if (n0 < 3 || ci == "") { print "exec-reuse: FAIL: missing exit lines (" n0 " node -e 0, child " (ci == "" ? "none" : "seen") "), log " out; exit 1 }
printf "exec-reuse: A (warm fresh) installs %d nofill %d; C (exec child) %d nofill %d; B (fresh after) %d nofill %d\n", ai, an, ci, cn, bi, bn
bad = 0
if (ci > 2 * ai) { print "exec-reuse: FAIL: the exec'd child installs more than 2x a fresh start"; bad = 1 }
if (ci > 2 * ai) { print "exec-reuse: FAIL: the exec-ed child installs more than 2x a fresh start"; bad = 1 }
if (bi > 1.5 * ai) { print "exec-reuse: FAIL: a fresh start after it installs more than 1.5x"; bad = 1 }
if (bn > 3 * an + 500) { print "exec-reuse: FAIL: a fresh start after it misses unfilled more than 3x"; bad = 1 }
if (!bad) print "exec-reuse: PASS"
+49
View File
@@ -0,0 +1,49 @@
/* FRINTA rounds to nearest with ties away from zero; FRINTN ties to even.
* Scalar double and single, and the vector 2D and 4S forms, on halfway
* values, values a hair below a half, the edges of the integers, -0, the
* infinities and NaN. Printed as bit patterns so a sign of zero counts. */
#include <stdint.h>
#include <stdio.h>
#include <string.h>
static uint64_t bits_d(double d) { uint64_t u; memcpy(&u, &d, 8); return u; }
static uint32_t bits_s(float f) { uint32_t u; memcpy(&u, &f, 4); return u; }
static double frinta_d(double x) { double r; __asm__("frinta %d0, %d1" : "=w"(r) : "w"(x)); return r; }
static double frintn_d(double x) { double r; __asm__("frintn %d0, %d1" : "=w"(r) : "w"(x)); return r; }
static float frinta_s(float x) { float r; __asm__("frinta %s0, %s1" : "=w"(r) : "w"(x)); return r; }
typedef double v2d __attribute__((vector_size(16)));
typedef float v4s __attribute__((vector_size(16)));
int main(void) {
static const double d[] = {
0.5, -0.5, 1.5, -1.5, 2.5, -2.5, 3.5, 0.49999999999999994, -0.49999999999999994,
0.3, -0.3, -0.0, 0.0, 4503599627370495.5, -4503599627370495.5, 4503599627370497.0,
1e300, -1e300, 1.0 / 0.0, -1.0 / 0.0, 0.0 / 0.0, 2.4999999999999996, 1e-310, -1e-310,
};
for (unsigned i = 0; i < sizeof d / sizeof d[0]; i++)
printf("d %016llx a %016llx n %016llx\n", (unsigned long long)bits_d(d[i]),
(unsigned long long)bits_d(frinta_d(d[i])), (unsigned long long)bits_d(frintn_d(d[i])));
static const float s[] = {
0.5f, -0.5f, 1.5f, -2.5f, 0.49999997f, -0.49999997f, 8388607.5f, -8388607.5f,
-0.0f, -0.3f, 1.0f / 0.0f, 0.0f / 0.0f, 1e-40f,
};
for (unsigned i = 0; i < sizeof s / sizeof s[0]; i++)
printf("s %08x a %08x\n", bits_s(s[i]), bits_s(frinta_s(s[i])));
for (unsigned i = 0; i + 1 < sizeof d / sizeof d[0]; i += 2) {
v2d v = {d[i], d[i + 1]}, r;
__asm__("frinta %0.2d, %1.2d" : "=w"(r) : "w"(v));
printf("2d %016llx %016llx\n", (unsigned long long)bits_d(r[0]), (unsigned long long)bits_d(r[1]));
}
for (unsigned i = 0; i + 3 < sizeof s / sizeof s[0]; i += 4) {
v4s v = {s[i], s[i + 1], s[i + 2], s[i + 3]}, r;
__asm__("frinta %0.4s, %1.4s" : "=w"(r) : "w"(v));
printf("4s %08x %08x %08x %08x\n", bits_s(r[0]), bits_s(r[1]), bits_s(r[2]), bits_s(r[3]));
float lo[2] = {s[i], s[i + 1]};
__attribute__((vector_size(8))) float v2 = {lo[0], lo[1]}, r2;
__asm__("frinta %0.2s, %1.2s" : "=w"(r2) : "w"(v2));
printf("2s %08x %08x\n", bits_s(r2[0]), bits_s(r2[1]));
}
return 0;
}
+2
View File
@@ -534,6 +534,7 @@ echo "building guest programs..."
"$CC" -static -no-pie -O2 -o "$WORK/brk-zero" "$HERE/programs/brk-zero.c"
"$CC" -static -O2 -o "$WORK/selfmod" "$HERE/programs/selfmod.c"
"$CC" -static -O2 -o "$WORK/fma" "$HERE/programs/fma.c"
"$CC" -static -O2 -o "$WORK/frinta" "$HERE/programs/frinta.c"
"$CC" -static -no-pie -O2 -o "$WORK/init" "$HERE/programs/init.c"
"$CC" -static -no-pie -O2 -pthread -o "$WORK/forkthread" "$HERE/programs/forkthread.c"
# C++ pulls in the scalar AdvSIMD group, and its exception unwinder walks
@@ -921,6 +922,7 @@ compare "brk-zero" "$WORK/brk-zero"
# value is compared as a bit pattern, over ordinary magnitudes, random bit
# patterns, the exponent band's edges and zeros.
compare "fma" "$WORK/fma"
compare "frinta" "$WORK/frinta"
compare "signal-kill" "$WORK/signal-kill"
compare "signal-alarm" "$WORK/signal-alarm"
compare "signal-sigchld" "$WORK/signal-sigchld"
+34
View File
@@ -0,0 +1,34 @@
/* vfork, many times, with a sibling thread parked in the parent: the child
* is a copy of memory as the forking thread left it at the call, never as
* it is later. In the browser runtime any Worker of the process could claim
* a staged fork, so the parked thread's Worker sometimes copied memory after
* the parent had run on and unmapped the record the child starts on; the
* child trapped reading its thread pointer (exit 137, about 1 in 125). */
#include <pthread.h>
#include <stdio.h>
#include <sys/wait.h>
#include <unistd.h>
static void *park(void *a)
{
for (;;) pause();
return a;
}
int main(void)
{
enum { N = 300 };
int bad = 0;
pthread_t t;
pthread_create(&t, 0, park, 0);
for (int i = 0; i < N; i++) {
pid_t pid = vfork();
if (pid == 0) _exit(9);
int st;
if (waitpid(pid, &st, 0) != pid) { printf("waitpid failed at %d\n", i); return 1; }
int code = WIFEXITED(st) ? WEXITSTATUS(st) : 128 + WTERMSIG(st);
if (code != 9 && bad++ < 5) printf("vfork %d: child exit %d\n", i, code);
}
printf("%d vforks, %d bad\n", N, bad);
return bad != 0;
}