fjit (page): seed each mapped object's function starts; cheaper fetch checks

The page's function JIT knew only the starts calls name: recovery followed
a tail jump into the function it reached and copied it into the caller.
libasound's exported wrappers each carried a copy of the function behind
them; HK's menu had 20-26 MiB of libasound wasm (hp13, hb53). Before a
translation, every mapped object the JIT has no starts for gets them, as
natively ahead of time (Jit::add_image: symbols, .eh_frame, init arrays,
relocations); retired code drops its objects (and Jit::retire its starts),
so a library mapped again is seeded again. add_image returns how many
starts it added (logged with ?fjitlibs=byobj=1).

Offline (scratch/unity/fjitrepro, libasound + libc, each of libasound's
1,698 exports translated in turn): 57.2 MiB of wasm in 16.4 s without the
starts, 9.7 MiB in 7.1 s with them (one function per module: 49.7 MiB in
14.1 s against 3.2 MiB in 1.7 s). Seeding both objects took 17 ms.

HK in the page, full fjit, no unit (edger; hb53 = 64efd78e without this,
hb54 with the seeding alone):
- first frame 121 s (hb53 178 s), 100 presents 171 s (225 s);
- 39.1 presents/s at 392 s (35.7);
- libasound 3.7 MiB of wasm by 211 s (hb53 26.0 MiB), libpulse 0.55
  MiB (4.0), libudev 1.0 MiB (3.2); UnityPlayer about the same
  (72.6 against 74.6 MiB).

`executable`, asked of every byte fetched, took three locks a byte for the
providers' addresses and the replaced ranges (5.9 s of hp14's 80 s
profile of the main Worker, 2.3 s in elfprov::provided alone): both are
now taken once per translation and the range checks kept by page.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_1896c5b302a54696aaa92c5f4047f20b
This commit is contained in:
velzieandClaude Opus 5.5 committed 2026-10-09 06:29:53 -04:00
1 parent 64efd78e4e
commit 1bf5ba09d0
4 files changed
+83 -7

No files matched your search

+7 -3
View File
@@ -130,18 +130,21 @@ impl Jit {
/// starts (symbols, `.eh_frame`, init arrays, relocations; `elf` is the
/// file, for `.eh_frame` and ELF32 symbols), with their extents, which
/// bound functions and computed-goto tables as they do ahead of time.
pub fn add_image(&self, image: &gex_ld::Image, elf: Option<&[u8]>) {
self.add_image_in(0, image, elf);
/// Returns how many function starts it knows of the image.
pub fn add_image(&self, image: &gex_ld::Image, elf: Option<&[u8]>) -> usize {
self.add_image_in(0, image, elf)
}
fn add_image_in(&self, process: u64, image: &gex_ld::Image, elf: Option<&[u8]>) {
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 st = guard.procs.entry(process).or_default();
let mut n = 0;
for (a, info) in starts {
if info.strong() {
st.starts.insert(a, info);
n += 1;
}
}
st.readonly.extend(
@@ -151,6 +154,7 @@ impl Jit {
.filter(|s| !s.writable)
.map(|s| (s.vaddr, s.vaddr + s.bytes.len() as u64)),
);
n
}
/// A module for the function at `addr` (see the module documentation),
+8
View File
@@ -49,6 +49,14 @@ pub extern "C" fn gex_elfprov_off() {
OFF.store(true, Ordering::Relaxed);
}
/// Every address `provided` holds, at once (the function JIT asks of each
/// byte it fetches; three locks a byte cost HK's start-up 2.3 s, hp14).
pub(crate) fn provided_set() -> std::collections::HashSet<u64> {
let mut s: std::collections::HashSet<u64> = lock(&BOUND).iter().flat_map(|b| b.addrs.iter().copied()).collect();
s.extend(super::libprov::bound_addrs());
s
}
/// Whether an ELF provider holds `addr` (the function JIT leaves it alone).
pub(crate) fn provided(addr: u64) -> bool {
lock(&BOUND).iter().any(|b| b.addrs.contains(&addr)) || super::libprov::bound(addr)
+10
View File
@@ -76,6 +76,16 @@ pub(crate) fn replaced(addr: u64) -> bool {
lock(&REPLACED).iter().any(|&(lo, hi)| addr >= lo && addr < hi)
}
/// The addresses replaced libraries' providers are bound at.
pub(crate) fn bound_addrs() -> Vec<u64> {
lock(&LIBS).iter().flat_map(|l| l.pairs.iter().map(|p| p.0)).collect()
}
/// The ranges of replaced libraries' own text.
pub(crate) fn replaced_ranges() -> Vec<(u64, u64)> {
lock(&REPLACED).clone()
}
/// Whether `addr` is one a replaced library's provider is bound at (its
/// pairs are sorted by address).
pub(crate) fn bound(addr: u64) -> bool {
+58 -4
View File
@@ -338,6 +338,53 @@ pub(crate) fn on_retire(ranges: &[(u64, u64)]) {
if let Some(jit) = JIT.get() {
jit.retire(0, ranges);
}
lock(&SEEDED).retain(|&(_, lo, hi)| !ranges.iter().any(|&(s, e)| s < hi && lo < e));
}
/// The objects whose function starts the JIT has (`seed_mapped`): path and
/// the address range of their `PT_LOAD`s, dropped when code there retires.
static SEEDED: Mutex<Vec<(String, u64, u64)>> = Mutex::new(Vec::new());
/// Give the JIT the function starts of every mapped object it lacks (its
/// symbols, `.eh_frame`, init arrays and relocations, `Jit::add_image`),
/// as natively ahead of time. Without them recovery knows only the starts
/// calls name, and follows a tail jump into the function it reaches: each
/// of libasound's exported wrappers carried a copy of the function it
/// jumps to. Offline, translating each of libasound's 1,698 exports in
/// turn made 57.2 MiB of wasm in 16.4 s without its starts and 9.7 MiB in
/// 7.1 s with them (one function per module: 49.7 MiB and 14.1 s against
/// 3.2 MiB and 1.7 s); HK's menu had 20 MiB of libasound wasm (hp13).
fn seed_mapped(jit: &gex_xlate::fjit::Jit) {
let maps: Vec<(u64, String, u64)> = crate::with_host(|h| {
use gex_kernel::mem::prot;
h.kernel.mem.vmas().iter().filter(|v| v.prot & prot::EXEC != 0).filter_map(|v| v.code.clone().map(|(p, o)| (v.start, p, o))).collect()
});
for (start, path, off) in maps {
if lock(&SEEDED).iter().any(|(p, lo, hi)| *p == path && start >= *lo && start < *hi) {
continue;
}
let bytes = crate::with_host(|h| h.kernel.read_file(&path)).unwrap_or_default();
let lds = loads(&bytes);
// The vma's file offset gives its vaddr in the file, hence the bias.
let Some(bias) = lds.iter().find(|&&(o, _, n)| off >= o && off < o + n.max(1)).map(|&(o, v, _)| start.wrapping_sub(v + (off - o))) else {
lock(&SEEDED).push((path, start, start + 1));
continue;
};
let lo = lds.iter().map(|l| l.1).min().unwrap_or(0).wrapping_add(bias) & !0xfff;
let hi = lds.iter().map(|l| l.1 + l.2).max().unwrap_or(0).wrapping_add(bias);
let n = match gex_ld::elf::load(&bytes, bias) {
Ok(mut image) => {
gex_ld::elf::apply_own_relative(&mut image);
jit.add_image(&image, Some(&bytes))
}
Err(_) => 0,
};
if BY_OBJECT_ON.load(Ordering::Relaxed) {
let name = path.rsplit('/').next().unwrap_or(&path);
crate::warn_line(&format!("[fjit] starts of {name} at {bias:#x}: {n}\n"));
}
lock(&SEEDED).push((path, lo, hi.max(lo + 1)));
}
}
/// Translate the function at `addr` and publish it (`gex.lookup`'s miss):
@@ -359,6 +406,7 @@ fn translate_root(addr: u64) -> Option<u32> {
return None;
}
let jit = JIT.get()?;
seed_mapped(jit);
let (exec, ro, wx): (Vec<(u64, u64)>, Vec<(u64, u64)>, Vec<(u64, u64)>) = crate::with_host(|h| {
use gex_kernel::mem::prot;
let v = h.kernel.mem.vmas();
@@ -392,11 +440,17 @@ fn translate_root(addr: u64) -> Option<u32> {
// unit (hb52).
let served_memo = std::cell::RefCell::new(std::collections::HashMap::<u64, bool>::new());
let served = |a: u64| *served_memo.borrow_mut().entry(a).or_insert_with(|| crate::cgen::unit_serves(a) || crate::cgen::jit_serves(a));
// Asked of every byte fetched: the providers' addresses and replaced
// ranges taken once, and the range checks kept by 4 KiB page (exec,
// replaced and object ranges are pages; `?fjitlibs=` ranges are not).
let provided = crate::cgen::elfprov::provided_set();
let replaced = crate::cgen::libprov::replaced_ranges();
let page_memo = std::cell::RefCell::new(std::collections::HashMap::<u64, bool>::new());
let ranged = rules && lock(&LIBS).iter().any(|r| r.range.is_some());
let executable = |a: u64| {
exec.iter().any(|&(s, e)| a >= s && a < e)
&& !crate::cgen::elfprov::provided(a)
&& !crate::cgen::libprov::replaced(a)
&& (!rules || lib_wanted(a))
let page_ok = |a: u64| exec.iter().any(|&(s, e)| a >= s && a < e) && !replaced.iter().any(|&(lo, hi)| a >= lo && a < hi) && (!rules || lib_wanted(a));
let ok = if ranged { page_ok(a) } else { *page_memo.borrow_mut().entry(a >> 12).or_insert_with(|| page_ok(a)) };
ok && !provided.contains(&a)
};
let read_only = |a: u64, n: u64| ro.iter().any(|&(s, e)| a >= s && a.saturating_add(n) <= e);
// Writable-and-executable code (a JIT's) stays tier-1, as natively.