diff --git a/crates/xenia-app/src/main.rs b/crates/xenia-app/src/main.rs index 07ec4d8..8b53a3d 100644 --- a/crates/xenia-app/src/main.rs +++ b/crates/xenia-app/src/main.rs @@ -4396,6 +4396,8 @@ fn dump_thread_diagnostic( } // JIT differential harness: checked/skipped/mismatch tally at clean exit. xenia_cpu::recompiler::report_diff_summary(); + // JIT opcode-frequency histogram (XENIA_JIT_HIST) — drives M1 coverage. + xenia_cpu::recompiler::report_histogram(); // STEP-10 diagnostic (observe-only, env-gated `XENIA_DUMP_SLOTS=1`). // Prints each scheduler slot's full runqueue with the fields needed to diff --git a/crates/xenia-cpu/src/recompiler.rs b/crates/xenia-cpu/src/recompiler.rs index 8191767..8b45075 100644 --- a/crates/xenia-cpu/src/recompiler.rs +++ b/crates/xenia-cpu/src/recompiler.rs @@ -43,6 +43,7 @@ use std::sync::atomic::{AtomicU64, AtomicU8, Ordering}; use crate::block_cache::DecodedBlock; use crate::context::PpcContext; use crate::interpreter::{execute, step_block, StepResult}; +use crate::opcode::PpcOpcode; use xenia_memory::MemoryAccess; /// Cached env gate. 0 = uninitialised, 1 = on, 2 = off. @@ -74,6 +75,58 @@ pub fn diff_enabled() -> bool { cached_flag(&F, "XENIA_JIT_DIFF") } +// ---- opcode histogram (XENIA_JIT_HIST) — data-drives M1 coverage --------- + +#[inline] +fn hist_enabled() -> bool { + static F: AtomicU8 = AtomicU8::new(0); + cached_flag(&F, "XENIA_JIT_HIST") +} + +thread_local! { + static HIST: RefCell> = RefCell::new(HashMap::new()); + static HIST_TICK: Cell = const { Cell::new(0) }; +} + +/// Sample 1-in-16 instructions — relative opcode frequencies are unbiased at +/// this rate and the HashMap cost stops dominating the run. +#[inline] +fn hist_tally(op: PpcOpcode) { + let t = HIST_TICK.with(|c| { + let n = c.get().wrapping_add(1); + c.set(n); + n + }); + if t & 0xF == 0 { + HIST.with(|h| *h.borrow_mut().entry(op).or_insert(0) += 1); + } +} + +/// Print the opcode-frequency histogram (top 40 + cumulative %), so M1 coverage +/// targets the opcodes that actually dominate boot+movie execution. +pub fn report_histogram() { + if !hist_enabled() { + return; + } + let mut v: Vec<(PpcOpcode, u64)> = + HIST.with(|h| h.borrow().iter().map(|(&k, &c)| (k, c)).collect()); + v.sort_by(|a, b| b.1.cmp(&a.1)); + let total: u64 = v.iter().map(|(_, c)| c).sum(); + eprintln!("=== JIT opcode histogram (total={total}, {} distinct) ===", v.len()); + let mut cum = 0u64; + for (i, (op, c)) in v.iter().take(40).enumerate() { + cum += c; + eprintln!( + " {:>2}. {:<14?} {:>12} {:>5.1}% cum {:>5.1}%", + i + 1, + op, + c, + 100.0 * *c as f64 / total as f64, + 100.0 * cum as f64 / total as f64 + ); + } +} + /// Execute one decoded block. **M0: interpreter fallback for every opcode.** /// /// Byte-for-byte the same loop as [`crate::interpreter::step_block`]: bump @@ -84,9 +137,13 @@ pub fn run_block( mem: &dyn MemoryAccess, block: &DecodedBlock, ) -> StepResult { + let hist = hist_enabled(); let mut result = StepResult::Continue; for instr in &block.instrs { let expected_next = instr.addr.wrapping_add(4); + if hist { + hist_tally(instr.opcode); + } // M0 fallback: identical to the interpreter. Future stages dispatch // lowered ops here and only fall back for uncompiled opcodes. result = execute(ctx, mem, instr);