[iterate-4A] diagnostics: XENIA_PROFILE wall-time profiler + probe/tooling snapshot
Handoff snapshot of the env-gated diagnostic scaffolding used across the intro-video RE. Kept out of the milestone commits (645feb8..5573ac1) to keep those clean; committed here so nothing is lost on handoff. New — XENIA_PROFILE wall-time profiler (crates/xenia-gpu/src/prof.rs): Coarse buckets attributing playback wall time to interpreter (step_block), kernel HLE (call_export), block decode/cache (lookup_or_build), texture decode, host draw, and present; prints periodic snapshots (every 500M guest instr, or every 500 presents) + a clean-exit report. Hot path is gated on a cached is_on() (one relaxed load) so it is zero-cost when XENIA_PROFILE is unset. Call sites: main.rs run_superblock / parallel worker (step_block, lookup_or_build, call_export), texture_cache ensure_cached, render.rs present + dispatch_xenos_draws. First profile (movie playback, headless single-thread lockstep): effective ~35 MIPS; interpreter body ~40% @ ~95-102 MIPS; texture decode 0.3% (cache works); present ~0%; the rest is per-block dispatch + scheduler plumbing (~13 instr/block over 229M blocks). Overhead-bound, not interpreter-body bound; the levers are coarser execution units (superblock chaining) and ultimately a JIT. Pre-existing read-only probe knobs (were uncommitted; env-gated, observe-only): XENIA_RET_CAPTURE_PC/_REG/_MEM, LOG_RESUMES, LOG_WAITS, LOG_SIGNAL, FORCE_TID, STARVE_LIMIT, INCUMBENT_PICK, INSTR_PER_MS, DUMP_FRAME, DUMP_WGSL, BIND_LOG, CONST_LOG, DISPATCH_REC, AUDIT_PC_TRACE. Tooling: sylph-run.sh (movie oracle loop, 180s default timeout), zq.py (DuckDB disasm/xref helper). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,58 @@ use xenia_memory::MemoryAccess;
|
||||
/// `reserved_line = ea & !RESERVATION_MASK` in [context::PpcContext].
|
||||
pub const RESERVATION_MASK: u32 = 0x7F;
|
||||
|
||||
/// RE diagnostic (milestone-2 intro-video). `XENIA_RET_CAPTURE_PC=0x..`
|
||||
/// names a guest block-head PC at which to log r3/r4/r5 every time the
|
||||
/// block is entered. Unlike the block-/slot-level audit probes in
|
||||
/// `KernelState`, this fires in `step_block` at *every* block entry, so
|
||||
/// it reaches mid-function return points (e.g. the instruction right
|
||||
/// after a `bcctrl`) that the slot-visit probes miss. Read-only: only
|
||||
/// emits a `println!`, mutates no guest state, so a captured run is
|
||||
/// byte-identical to an unprobed one. 0 (unset env) → inert (one cached
|
||||
/// `OnceLock` load + a `!= 0` branch per block).
|
||||
fn ret_capture_pc() -> u32 {
|
||||
use std::sync::OnceLock;
|
||||
static RET_CAPTURE_PC: OnceLock<u32> = OnceLock::new();
|
||||
*RET_CAPTURE_PC.get_or_init(|| {
|
||||
std::env::var("XENIA_RET_CAPTURE_PC")
|
||||
.ok()
|
||||
.and_then(|s| {
|
||||
let t = s.trim();
|
||||
u32::from_str_radix(t.trim_start_matches("0x"), 16).ok()
|
||||
})
|
||||
.unwrap_or(0)
|
||||
})
|
||||
}
|
||||
|
||||
/// RE diagnostic companion to [`ret_capture_pc`]. When `XENIA_RET_CAPTURE_MEM=1`
|
||||
/// the RET-CAPTURE site also dumps the guest words at `r3+0x30 .. r3+0x48`
|
||||
/// (the decoder *track* ring header: `+0x34` write cursor, `+0x44` read
|
||||
/// cursor — fill = write − read). Read-only (`mem.read_u32` + `println!`).
|
||||
fn ret_capture_mem() -> bool {
|
||||
use std::sync::OnceLock;
|
||||
static RET_CAPTURE_MEM: OnceLock<bool> = OnceLock::new();
|
||||
*RET_CAPTURE_MEM.get_or_init(|| {
|
||||
std::env::var("XENIA_RET_CAPTURE_MEM")
|
||||
.map(|s| s.trim() == "1")
|
||||
.unwrap_or(false)
|
||||
})
|
||||
}
|
||||
|
||||
/// Which GPR holds the base pointer for the `XENIA_RET_CAPTURE_MEM` dump.
|
||||
/// Defaults to r3; set `XENIA_RET_CAPTURE_REG=21` to dump the pointee of r21
|
||||
/// (e.g. the ring element the pump pops). Read-only.
|
||||
fn ret_capture_reg() -> usize {
|
||||
use std::sync::OnceLock;
|
||||
static RET_CAPTURE_REG: OnceLock<usize> = OnceLock::new();
|
||||
*RET_CAPTURE_REG.get_or_init(|| {
|
||||
std::env::var("XENIA_RET_CAPTURE_REG")
|
||||
.ok()
|
||||
.and_then(|s| s.trim().parse::<usize>().ok())
|
||||
.filter(|&n| n < 32)
|
||||
.unwrap_or(3)
|
||||
})
|
||||
}
|
||||
|
||||
/// Result of executing a single instruction.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum StepResult {
|
||||
@@ -97,6 +149,48 @@ pub fn step_block(
|
||||
mem: &dyn MemoryAccess,
|
||||
block: &crate::block_cache::DecodedBlock,
|
||||
) -> StepResult {
|
||||
// RE diagnostic: log r3/r4/r5 at a configured block-head PC (e.g. the
|
||||
// return point after a `bcctrl`). Read-only; inert unless the env is set.
|
||||
let cap = ret_capture_pc();
|
||||
if cap != 0 && block.start_pc == cap {
|
||||
// KeTimeStampBundle tick_count (ms): [([0x820007D0]) + 0x10]. Read-only;
|
||||
// lets a capture correlate guest progress with the guest-visible clock.
|
||||
let bundle = mem.read_u32(0x820007D0);
|
||||
let tick_ms = if bundle != 0 { mem.read_u32(bundle.wrapping_add(0x10)) } else { 0 };
|
||||
println!("RET-CAPTURE-TICK pc={:#010x} tid={} tick_ms={} cycle={}", block.start_pc, ctx.thread_id, tick_ms, ctx.cycle_count);
|
||||
println!(
|
||||
"RET-CAPTURE pc={:#010x} tid={} r3={:#010x} r4={:#010x} r5={:#010x} r21={:#010x} lr={:#010x} cycle={}",
|
||||
block.start_pc,
|
||||
ctx.thread_id,
|
||||
ctx.gpr[3] as u32,
|
||||
ctx.gpr[4] as u32,
|
||||
ctx.gpr[5] as u32,
|
||||
ctx.gpr[21] as u32,
|
||||
ctx.lr as u32,
|
||||
ctx.cycle_count,
|
||||
);
|
||||
if ret_capture_mem() {
|
||||
let reg = ret_capture_reg();
|
||||
let base = ctx.gpr[reg] as u32;
|
||||
let w = |off: u32| mem.read_u32(base.wrapping_add(off));
|
||||
// Dump +0x00 (vtable ptr) .. +0x48 so callers can inspect either a
|
||||
// ring element (60-byte struct, vtable at +0) or the track header
|
||||
// (+0x34 write / +0x44 read cursors).
|
||||
println!(
|
||||
"RET-CAPTURE-MEM r{} base={:#010x} +00={:#010x} +04={:#010x} +08={:#010x} +0c={:#010x} +10={:#010x} +14={:#010x} +18={:#010x} +1c={:#010x} +20={:#010x} +24={:#010x} +28={:#010x} +2c={:#010x} +30={:#010x} +34={:#010x} +38={:#010x} +3c={:#010x} +40={:#010x} +44={:#010x} +48={:#010x}",
|
||||
reg, base,
|
||||
w(0x00), w(0x04), w(0x08), w(0x0c), w(0x10), w(0x14), w(0x18), w(0x1c),
|
||||
w(0x20), w(0x24), w(0x28), w(0x2c), w(0x30), w(0x34), w(0x38), w(0x3c),
|
||||
w(0x40), w(0x44), w(0x48),
|
||||
);
|
||||
// Extra slots: stack locals [r1+0x50]/[r1+0x54] when reg=1; engine
|
||||
// worker-resume gate +0xF0 (pending flag) / +0x110 (worker handle).
|
||||
println!(
|
||||
"RET-CAPTURE-MEM2 r{} base={:#010x} +50={:#010x} +54={:#010x} +f0={:#010x} +110={:#010x}",
|
||||
reg, base, w(0x50), w(0x54), w(0xF0), w(0x110),
|
||||
);
|
||||
}
|
||||
}
|
||||
let mut result = StepResult::Continue;
|
||||
for instr in &block.instrs {
|
||||
let expected_next = instr.addr.wrapping_add(4);
|
||||
|
||||
@@ -49,10 +49,27 @@ pub const QUANTUM_DEFAULT: u32 = 50_000;
|
||||
/// guarantees *bounded* forward progress, it does not invert priority.
|
||||
pub const STARVE_LIMIT: u32 = 4096;
|
||||
|
||||
/// Toggle for the `pick_runnable` incumbent-preference tiebreak (the fix).
|
||||
/// Default on. `XENIA_INCUMBENT_PICK=0` reverts to the old lowest-index
|
||||
/// tiebreak — a rollback knob to A/B the fix's effect on boot rendering vs
|
||||
/// the movie feeder-starvation cure. Inert unless explicitly set to 0.
|
||||
/// RE diagnostic: `XENIA_STARVE_LIMIT=<n>` overrides [`STARVE_LIMIT`] at
|
||||
/// runtime (cached). Lets a fairness test boost starved co-located threads
|
||||
/// (e.g. the feeder tid24 on hw=1) much sooner without a full monopoly —
|
||||
/// distinguishing "fair scheduling fixes the deadlock" from "needs more".
|
||||
/// Unset → the compiled 4096 default (inert).
|
||||
fn starve_limit() -> u32 {
|
||||
use std::sync::OnceLock;
|
||||
static SL: OnceLock<u32> = OnceLock::new();
|
||||
*SL.get_or_init(|| {
|
||||
std::env::var("XENIA_STARVE_LIMIT")
|
||||
.ok()
|
||||
.and_then(|s| s.trim().parse::<u32>().ok())
|
||||
.filter(|&n| n > 0)
|
||||
.unwrap_or(STARVE_LIMIT)
|
||||
})
|
||||
}
|
||||
|
||||
/// Diagnostic A/B toggle for the `pick_runnable` incumbent-preference
|
||||
/// tiebreak. Default on (the fix). `XENIA_INCUMBENT_PICK=0` reverts to the
|
||||
/// old lowest-index tiebreak, to measure the fix's effect on boot rendering
|
||||
/// vs the movie feeder-starvation cure. Inert unless explicitly set to 0.
|
||||
fn incumbent_pick() -> bool {
|
||||
use std::sync::OnceLock;
|
||||
static IP: OnceLock<bool> = OnceLock::new();
|
||||
@@ -200,6 +217,28 @@ pub enum HwState {
|
||||
ServicingIrq(BlockReason),
|
||||
}
|
||||
|
||||
/// RE diagnostic: gate for the `XENIA_LOG_WAITS=1` `PARK` logger in
|
||||
/// [`Scheduler::park_current`]. Cached; inert unless the env is set.
|
||||
fn log_waits_enabled() -> bool {
|
||||
use std::sync::OnceLock;
|
||||
static EN: OnceLock<bool> = OnceLock::new();
|
||||
*EN.get_or_init(|| std::env::var("XENIA_LOG_WAITS").is_ok())
|
||||
}
|
||||
|
||||
/// RE diagnostic: `XENIA_FORCE_TID=<n>` — guest tid that `pick_runnable`
|
||||
/// always prefers when Ready (un-starve test for the feeder deadlock).
|
||||
/// 0 / unset = inert. Cached.
|
||||
fn force_tid() -> u32 {
|
||||
use std::sync::OnceLock;
|
||||
static FT: OnceLock<u32> = OnceLock::new();
|
||||
*FT.get_or_init(|| {
|
||||
std::env::var("XENIA_FORCE_TID")
|
||||
.ok()
|
||||
.and_then(|s| s.trim().parse::<u32>().ok())
|
||||
.unwrap_or(0)
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum BlockReason {
|
||||
Suspended,
|
||||
@@ -274,6 +313,20 @@ impl HwSlot {
|
||||
/// `STARVE_LIMIT` visits). The boost is a pure function of the per-thread
|
||||
/// counters/priority/index, so picks stay deterministic.
|
||||
pub fn pick_runnable(&self) -> Option<usize> {
|
||||
// RE diagnostic (milestone-2 intro-video). `XENIA_FORCE_TID=<n>` makes
|
||||
// this slot always prefer the named guest tid whenever it is Ready on
|
||||
// this slot — fully un-starving it. Used to separate "tid is starved"
|
||||
// from "tid is genuinely stuck" for the feeder tid24 deadlock. Slots
|
||||
// not carrying that tid fall through to normal selection. Behaviour-
|
||||
// changing, so strictly env-gated (inert when unset).
|
||||
let force = force_tid();
|
||||
if force != 0 {
|
||||
if let Some((i, _)) = self.runqueue.iter().enumerate().find(|(_, t)| {
|
||||
t.tid == force && matches!(t.state, HwState::Ready | HwState::ServicingIrq(_))
|
||||
}) {
|
||||
return Some(i);
|
||||
}
|
||||
}
|
||||
// Tiebreak among equal-effective-priority Ready threads PREFERS THE
|
||||
// INCUMBENT (`running_idx`) over the lowest index. `decrement_quantum`
|
||||
// rotates `running_idx` to the next same-priority peer when the 50k
|
||||
@@ -307,7 +360,7 @@ impl HwSlot {
|
||||
/// visits is lifted to `i32::MAX` so it wins the next pick regardless of
|
||||
/// peer priority; otherwise its nominal priority is used unchanged.
|
||||
fn effective_priority(t: &GuestThread) -> i32 {
|
||||
if t.steps_starved >= STARVE_LIMIT {
|
||||
if t.steps_starved >= starve_limit() {
|
||||
i32::MAX
|
||||
} else {
|
||||
t.priority
|
||||
@@ -991,7 +1044,7 @@ impl Scheduler {
|
||||
if i == me {
|
||||
t.steps_starved = 0;
|
||||
} else if matches!(t.state, HwState::Ready | HwState::ServicingIrq(_)) {
|
||||
t.steps_starved = STARVE_LIMIT;
|
||||
t.steps_starved = starve_limit();
|
||||
promoted = true;
|
||||
}
|
||||
}
|
||||
@@ -1015,6 +1068,18 @@ impl Scheduler {
|
||||
self.timed_waits.push((d, r));
|
||||
self.timed_waits.sort_by_key(|&(d, _)| d);
|
||||
}
|
||||
// RE diagnostic (milestone-2 intro-video). `XENIA_LOG_WAITS=1` logs a
|
||||
// `PARK` line (tid, pc, cycle, block reason incl. waited handles) every
|
||||
// time a thread blocks — the symmetric counterpart of the resume log,
|
||||
// for pinning the producer↔consumer deadlock (what does the feeder
|
||||
// tid24 wait on after its one pass). Read-only diagnostic.
|
||||
if log_waits_enabled() {
|
||||
let t = self.thread_mut(r);
|
||||
println!(
|
||||
"PARK tid={} pc={:#010x} cycle={} reason={:?}",
|
||||
t.tid, t.ctx.pc, t.ctx.cycle_count, reason,
|
||||
);
|
||||
}
|
||||
self.thread_mut(r).state = HwState::Blocked(reason);
|
||||
self.recompute_slot_runnable(r.hw_id);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user