use ai_app::ui::fixture::{PHONE_FRAME_MS, PHONE_SCALE, phone_size}; use iris::harness::Harness; use iris::prelude::{GlyphPrimitive, Primitive, RectPrimitive}; use iris::widget::Scrollable; use ui_profile::stats::pct_u64; const PASSES: usize = 8; const VELOCITY: f32 = 12_000.0; const PASS_CAP_MS: u64 = 4_000; #[derive(Default)] struct Tally { name: &'static str, stride: usize, prev: Vec, changed: Vec, uploaded: Vec, whole: Vec, calls: Vec, } impl Tally { fn new(name: &'static str, stride: usize) -> Self { Self { name, stride, ..Default::default() } } fn frame(&mut self, bytes: &[u8], ranges: Vec>) { let n = self.prev.len().min(bytes.len()) / self.stride; let mut changed = (0..n) .filter(|i| { let r = i * self.stride..(i + 1) * self.stride; self.prev[r.clone()] != bytes[r] }) .count(); changed += bytes.len() / self.stride - n; self.changed.push((changed * self.stride) as u64); self.uploaded .push(ranges.iter().map(|r| (r.len() * self.stride) as u64).sum()); self.calls.push(ranges.len() as u64); self.whole.push(bytes.len() as u64); self.prev.clear(); self.prev.extend_from_slice(bytes); } fn report(&mut self) { let sum = |v: &[u64]| v.iter().sum::(); let (changed, uploaded, whole) = (sum(&self.changed), sum(&self.uploaded), sum(&self.whole)); println!( " {:<10} whole {:>7.1} MB | uploaded {:>7.1} MB ({:>5.1}%) | floor {:>7.1} MB ({:>5.1}%)", self.name, whole as f64 / 1e6, uploaded as f64 / 1e6, 100.0 * uploaded as f64 / whole.max(1) as f64, changed as f64 / 1e6, 100.0 * changed as f64 / whole.max(1) as f64, ); println!( " {:<10} uploaded per frame p50 {:>7} p90 {:>7} p99 {:>7} max {:>7} B | \ writes p50 {} p90 {} max {}", "", pct_u64(&mut self.uploaded.clone(), 0.5), pct_u64(&mut self.uploaded.clone(), 0.9), pct_u64(&mut self.uploaded.clone(), 0.99), pct_u64(&mut self.uploaded.clone(), 1.0), pct_u64(&mut self.calls.clone(), 0.5), pct_u64(&mut self.calls.clone(), 0.9), pct_u64(&mut self.calls.clone(), 1.0), ); } } struct Arenas { instances: Tally, rects: Tally, glyphs: Tally, } impl Arenas { fn new() -> Self { Self { instances: Tally::new("instances", 0), rects: Tally::new("rects", size_of::()), glyphs: Tally::new("glyphs", size_of::()), } } fn frame(&mut self, h: &mut Harness) { let count = h.render.primitives.instances().len(); let (entries, dirty) = h.render.primitives.instances_for_upload(); let bytes: Vec = bytemuck::cast_slice(entries).to_vec(); self.instances.stride = bytes.len().checked_div(count).unwrap_or(48); let ranges = dirty.ranges(count, 1024 / self.instances.stride); dirty.clear(); self.instances.frame(&bytes, ranges); let data = h.render.primitives.data_mut(); let (entries, dirty) = RectPrimitive::vec(data).for_upload(); let bytes: Vec = bytemuck::cast_slice(entries).to_vec(); let ranges = dirty.ranges(entries.len(), 1024 / size_of::()); dirty.clear(); self.rects.frame(&bytes, ranges); let (entries, dirty) = GlyphPrimitive::vec(data).for_upload(); let bytes: Vec = bytemuck::cast_slice(entries).to_vec(); let ranges = dirty.ranges(entries.len(), 1024 / size_of::()); dirty.clear(); self.glyphs.frame(&bytes, ranges); } fn report(&mut self, h: &Harness) { println!( " arena {} slots for {} live primitives", h.render.primitives.instances().len(), h.render.primitives.live_count(), ); self.instances.report(); self.rects.report(); self.glyphs.report(); } } #[test] #[ignore] fn what_a_fling_uploads() { 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); let mut arenas = Arenas::new(); arenas.frame(&mut h); let mut t = PHONE_FRAME_MS; for pass in 0..PASSES { let velocity = if pass % 2 == 0 { VELOCITY } else { -VELOCITY }; (opened.screen.list)(&mut h.rsc).fling(velocity); let end = t + PASS_CAP_MS; while t <= end { t += PHONE_FRAME_MS; h.frame(t); arenas.frame(&mut h); if !(opened.screen.list)(&mut h.rsc).is_scrolling() { break; } } t += 200; } println!("\na fling, {PASSES} passes:"); arenas.report(&h); } #[test] #[ignore] fn what_a_streamed_reply_uploads() { 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); let mut arenas = Arenas::new(); arenas.frame(&mut h); let mut items = opened.items; let mut t = PHONE_FRAME_MS; for event in opened.stream_tail.iter() { let old = items.clone(); items = ai_app::client::transcript_fold::fold_event(&items, event); opened.screen.apply(&mut h.rsc, &old, &items); t += PHONE_FRAME_MS; h.frame(t); arenas.frame(&mut h); } println!("\na reply streaming in, one event per frame:"); arenas.report(&h); }