iris: FrameReport CPU/GPU split, force-gles backend switch, iris-scroll.sh rig
Splits each frame sample at queue.submit into redraw-to-submit (iris's own CPU work) and submit-to-after-present (driver/GPU wait), so RUST.md's I5 "where does iris's frame time go" question can be answered with a number per half instead of a single total. Adds a force-gles Cargo feature that switches the Android wgpu::Instance from Backends::PRIMARY to Backends::GL at compile time (no runtime env-var path exists into an already-launched Android process on this machine), for isolating SwiftShader-Vulkan vs. GLES/virgl as the software-mode gap's cause. app/iris-scroll.sh extracts transcript-bench.sh's exact 24-swipe/6-cycle gesture loop for iris's own demo app, which transcript-bench.sh cannot drive directly since it opens a session through the Compose app's own UI. Verification (this pass, on a disk-pressure-limited host running low on space): cargo fmt --all clean, no diff. cargo clippy --workspace --all-targets: no warnings from this diff (pre-existing future-incompat notices from wgpu/winit/naga only). cargo test --workspace and cargo ndk for iris-android-app --features transcript-screen were verified clean by the previous pass on this identical diff (fmt/clippy/test/ndk all clean, per that pass's own report); not re-run here because the host's disk was 93% full and a concurrent ai-server rebuild (stable toolchain moved to 1.98.1, rebuilding aws-lc-sys from scratch) had driven I/O pressure to ~60%, so a repeat cargo test --workspace sat 50+ minutes doing no useful work and was stopped rather than left to make the disk situation worse. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
0e4629361b
commit
e2a1fadbec
6 files changed
+166
-7
No files matched your search
@@ -35,6 +35,12 @@ const RING_CAPACITY: usize = 4096;
|
||||
/// ever called from a button tap, not once per frame.
|
||||
pub struct FrameReport {
|
||||
ring: Box<[Duration; RING_CAPACITY]>,
|
||||
/// The `submit_to_present` half of each sample in `ring`, same index,
|
||||
/// same lifetime -- kept as a second ring rather than a ring of pairs so
|
||||
/// the existing `ring`/percentile code above is untouched (RUST.md's I5
|
||||
/// "Where iris's frame time goes" CPU/GPU split, added 2026-09-05).
|
||||
/// `ring[i] - submit_ring[i]` is that frame's `redraw_to_submit` half.
|
||||
submit_ring: Box<[Duration; RING_CAPACITY]>,
|
||||
/// How many of `ring`'s slots hold a real sample -- saturates at
|
||||
/// `RING_CAPACITY`, unlike `total_frames` below which keeps counting.
|
||||
len: usize,
|
||||
@@ -56,6 +62,20 @@ pub struct FrameStats {
|
||||
pub p90: Duration,
|
||||
pub p99: Duration,
|
||||
pub worst: Duration,
|
||||
/// Median of `redraw_to_submit` -- iris's own CPU work (layout, text,
|
||||
/// primitive building) up to and including building the `queue.submit`
|
||||
/// call, per frame. RUST.md's I5 "Where iris's frame time goes" split,
|
||||
/// added 2026-09-05 to answer "CPU or GPU?" with a number rather than a
|
||||
/// guess.
|
||||
pub cpu_p50: Duration,
|
||||
/// Median of `submit_to_present` -- the `queue.submit` call itself plus
|
||||
/// `present()`, i.e. wherever the driver/GPU/compositor wait actually
|
||||
/// happens. Same caveat as the type's own doc: `present()` is not
|
||||
/// fenced against the GPU actually finishing, so this is "how long the
|
||||
/// CPU was blocked handing the frame off", not the frame's true GPU
|
||||
/// time -- still enough to separate "iris is slow building the frame"
|
||||
/// from "iris is slow handing it to the driver".
|
||||
pub gpu_wait_p50: Duration,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for FrameStats {
|
||||
@@ -71,6 +91,13 @@ impl std::fmt::Display for FrameStats {
|
||||
self.p90.as_secs_f64() * 1000.0,
|
||||
self.p99.as_secs_f64() * 1000.0,
|
||||
self.worst.as_secs_f64() * 1000.0,
|
||||
)?;
|
||||
write!(
|
||||
f,
|
||||
" cpu_p50={:.1}ms gpu_wait_p50={:.1}ms (redraw-start-to-submit vs. \
|
||||
submit-to-after-present)",
|
||||
self.cpu_p50.as_secs_f64() * 1000.0,
|
||||
self.gpu_wait_p50.as_secs_f64() * 1000.0,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -79,6 +106,7 @@ impl FrameReport {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
ring: Box::new([Duration::ZERO; RING_CAPACITY]),
|
||||
submit_ring: Box::new([Duration::ZERO; RING_CAPACITY]),
|
||||
len: 0,
|
||||
pos: 0,
|
||||
total_frames: 0,
|
||||
@@ -86,13 +114,27 @@ impl FrameReport {
|
||||
}
|
||||
}
|
||||
|
||||
/// Record one frame's elapsed wall time. O(1), no allocation.
|
||||
/// Record one frame's elapsed wall time, with no CPU/GPU split (the
|
||||
/// `submit_to_present` half is recorded as zero, so `cpu_p50` reads as
|
||||
/// the whole frame and `gpu_wait_p50` as nothing -- honest for a caller
|
||||
/// that never measured the split, rather than fabricating one). O(1),
|
||||
/// no allocation.
|
||||
pub fn record(&mut self, elapsed: Duration) {
|
||||
self.ring[self.pos] = elapsed;
|
||||
self.record_split(elapsed, Duration::ZERO);
|
||||
}
|
||||
|
||||
/// Record one frame's elapsed wall time, split at `queue.submit`:
|
||||
/// `submit_to_present` is the `queue.submit()` call plus `present()`;
|
||||
/// `total - submit_to_present` is everything before it (layout, text,
|
||||
/// primitive building). RUST.md's I5 "Where iris's frame time goes"
|
||||
/// CPU/GPU split, added 2026-09-05. O(1), no allocation.
|
||||
pub fn record_split(&mut self, total: Duration, submit_to_present: Duration) {
|
||||
self.ring[self.pos] = total;
|
||||
self.submit_ring[self.pos] = submit_to_present;
|
||||
self.pos = (self.pos + 1) % RING_CAPACITY;
|
||||
self.len = (self.len + 1).min(RING_CAPACITY);
|
||||
self.total_frames += 1;
|
||||
if elapsed > JANK_THRESHOLD {
|
||||
if total > JANK_THRESHOLD {
|
||||
self.janky_frames += 1;
|
||||
}
|
||||
}
|
||||
@@ -118,6 +160,21 @@ impl FrameReport {
|
||||
let mut samples: Vec<Duration> = self.ring[..self.len].to_vec();
|
||||
samples.sort_unstable();
|
||||
let pct = |p: usize| samples[(samples.len() * p / 100).min(samples.len() - 1)];
|
||||
|
||||
// Separate arrays rather than subtracting the two medians above:
|
||||
// medians do not distribute over subtraction, and each needs its
|
||||
// own sort.
|
||||
let submit_samples: Vec<Duration> = self.submit_ring[..self.len].to_vec();
|
||||
let cpu_samples: Vec<Duration> = self.ring[..self.len]
|
||||
.iter()
|
||||
.zip(self.submit_ring[..self.len].iter())
|
||||
.map(|(&total, &submit_to_present)| total.saturating_sub(submit_to_present))
|
||||
.collect();
|
||||
let median = |mut v: Vec<Duration>| {
|
||||
v.sort_unstable();
|
||||
v[v.len() / 2]
|
||||
};
|
||||
|
||||
Some(FrameStats {
|
||||
total_frames: self.total_frames,
|
||||
janky_percent: 100.0 * self.janky_frames as f64 / self.total_frames as f64,
|
||||
@@ -125,6 +182,8 @@ impl FrameReport {
|
||||
p90: pct(90),
|
||||
p99: pct(99),
|
||||
worst: *samples.last().expect("len > 0 checked above"),
|
||||
cpu_p50: median(cpu_samples),
|
||||
gpu_wait_p50: median(submit_samples),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -196,6 +255,33 @@ mod tests {
|
||||
assert_eq!(r.report().unwrap().janky_percent, 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn record_without_a_split_reports_the_whole_frame_as_cpu() {
|
||||
// A caller that never measured the split (plain `record`) should
|
||||
// not fabricate a GPU-wait number -- it reads as zero, and the CPU
|
||||
// half reads as the whole frame.
|
||||
let mut r = FrameReport::new();
|
||||
r.record(Duration::from_millis(20));
|
||||
let stats = r.report().unwrap();
|
||||
assert_eq!(stats.cpu_p50, Duration::from_millis(20));
|
||||
assert_eq!(stats.gpu_wait_p50, Duration::ZERO);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn record_split_reports_each_halfs_own_median() {
|
||||
let mut r = FrameReport::new();
|
||||
// Three frames: total is always 30ms, but the CPU/GPU-wait split
|
||||
// moves, so the two medians must be independent of each other and
|
||||
// of `total`'s own median.
|
||||
r.record_split(Duration::from_millis(30), Duration::from_millis(5));
|
||||
r.record_split(Duration::from_millis(30), Duration::from_millis(10));
|
||||
r.record_split(Duration::from_millis(30), Duration::from_millis(20));
|
||||
let stats = r.report().unwrap();
|
||||
assert_eq!(stats.p50, Duration::from_millis(30));
|
||||
assert_eq!(stats.gpu_wait_p50, Duration::from_millis(10));
|
||||
assert_eq!(stats.cpu_p50, Duration::from_millis(20));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ring_wraps_without_growing_past_capacity() {
|
||||
let mut r = FrameReport::new();
|
||||
|
||||
Reference in new issue
Block a user