mirror of
https://github.com/HeyPuter/gex.git
synced 2026-11-13 20:12:43 -10:00
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:
1 parent
64efd78e4e
commit
1bf5ba09d0
4 files changed
+83
-7
No files matched your search
@@ -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),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user