//! A profiling run rather than a test: what a fling frame costs on the //! CPU, at layer 1 (docs/RUST.md's "Three test layers") -- the real //! transcript screen over the real bench fixture, with no window, no //! compositor and no GPU, on a clock this file owns. It exists so "the //! fling stutters" can be attributed rather than guessed at, and it is //! kept between investigations rather than rewritten each time (Iris, //! 2026-09-09: "please keep the profiling rig around for future use"). //! //! cargo test --release --test fling_profile -- --ignored --nocapture //! //! `#[ignore]`d because it asserts nothing -- it prints a distribution, //! so `run-tests.sh` neither runs it nor can fail on it. **Release, or //! the numbers mean nothing**: layout is dominated by text shaping, which //! is an order of magnitude slower unoptimised. //! //! What it cannot answer: anything about the GPU, the present queue, or //! the phone's own clock. It measures the CPU half of a frame, which is //! where `cpu_p50` in a phone bench report comes from. use ai_app::ui::fixture::{PHONE_FRAME_MS, PHONE_SCALE, phone_size}; use iris::harness::{Harness, TouchScript}; use iris::prelude::*; use std::time::{Duration, Instant}; /// Passes over the same content, alternating direction. More than two /// because the question the rig was built for is whether a frame's cost /// is first-time work (which the first pass pays and the rest do not) or /// work repeated every time a row comes back on screen. const PASSES: usize = 8; /// The velocity `bench_client.rs`'s fling phase uses, so a number here /// and a number in a phone report describe the same gesture. const VELOCITY: f32 = 12_000.0; /// A fling settles on the spline's own schedule (~2s at this velocity); /// this only stops a pass that somehow never settles from running away. const PASS_CAP_MS: u64 = 4_000; fn pct(sorted: &[Duration], p: f64) -> f64 { let i = ((sorted.len() as f64 - 1.0) * p).round() as usize; sorted[i].as_secs_f64() * 1000.0 } fn summarise(name: &str, samples: &[Duration]) { let mut v = samples.to_vec(); v.sort(); let over_budget = v .iter() .filter(|d| **d > Duration::from_micros(8_333)) .count(); println!( " {name:16} n={:<5} p50 {:.2}ms p90 {:.2}ms p99 {:.2}ms worst {:.2}ms \ over-8.3ms {over_budget}", v.len(), pct(&v, 0.5), pct(&v, 0.9), pct(&v, 0.99), pct(&v, 1.0), ); } #[test] #[ignore] fn what_a_fling_frame_costs() { let mut h = Harness::new(phone_size(), PHONE_SCALE); let opened = ai_app::ui::fixture::open(&mut h.rsc, &mut h.state).expect("the fixture folds"); h.frame(0); h.frame(PHONE_FRAME_MS); // The recorded flick first, so the velocity a real finger produces is // in the log beside the scripted passes below. let flick = TouchScript::parse(include_str!("../touch/flick-120hz.touch")).unwrap(); h.replay(&flick); println!( "recorded flick released at {:?}px/s; scripted passes run at {VELOCITY}px/s", (opened.screen.list)(&mut h.rsc).fling_velocity(), ); let mut t = flick.end_ms(); let mut all_frames = Vec::new(); let mut all_layouts = Vec::new(); for pass in 0..PASSES { // Away from the newest end on the even passes and back on the // odd ones, the same out-and-back the bench's fling phase drives. let velocity = if pass % 2 == 0 { VELOCITY } else { -VELOCITY }; let list = (opened.screen.list)(&mut h.rsc); list.fling(velocity); let id = opened.screen.list.id(); h.rsc.ui.animate(id); let mut frames = Vec::new(); let mut layouts = Vec::new(); let end = t + PASS_CAP_MS; while t <= end { let at = Instant::now(); h.frame(t); frames.push(at.elapsed()); layouts.push(h.render.last_layout_duration()); t += PHONE_FRAME_MS; if !(opened.screen.list)(&mut h.rsc).is_scrolling() { break; } } let laid_out = layouts .iter() .filter(|d| **d > Duration::from_micros(20)) .count(); println!( "pass {pass} ({}): {} frames, {laid_out} did layout", if velocity > 0.0 { "out " } else { "back" }, frames.len(), ); summarise("frame", &frames); all_frames.extend(frames); all_layouts.extend(layouts); // A moment at rest between passes, as a finger would leave. t += 200; } println!("\nall {PASSES} passes, primitives={}", h.render.active_primitive_count()); summarise("frame", &all_frames); summarise("layout", &all_layouts); }