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}; use ui_profile::stats::summarise; const PASSES: usize = 8; const VELOCITY: f32 = 12_000.0; const PASS_CAP_MS: u64 = 4_000; #[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); let flick = TouchScript::parse(include_str!("../../../../app/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 { 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); t += 200; } println!( "\nall {PASSES} passes, primitives={}", h.render.active_primitive_count() ); summarise("frame", &all_frames); summarise("layout", &all_layouts); } #[test] #[ignore] fn what_a_streamed_event_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); let mut items = opened.items; println!( "backlog: {} items, then {} streamed events", items.len(), opened.stream_tail.len() ); let mut fold = Vec::new(); let mut apply = Vec::new(); let mut frame = Vec::new(); let mut frame_same_block = Vec::new(); let mut frame_new_block = Vec::new(); let mut t = PHONE_FRAME_MS; let block_count = |items: &[ai_app::client::transcript_fold::TranscriptItem]| { use ai_app::client::markdown_blocks::split_blocks; use ai_app::client::transcript_fold::TranscriptItem; match items.last() { Some(TranscriptItem::AssistantMsg { text, .. }) => split_blocks(text).len(), _ => 0, } }; for (n, event) in opened.stream_tail.iter().enumerate() { let blocks_before = block_count(&items); let old = items.clone(); let at = Instant::now(); let folded = ai_app::client::transcript_fold::fold_event(&items, event); fold.push(at.elapsed()); items = folded; let added_block = block_count(&items) > blocks_before; let at = Instant::now(); opened.screen.apply(&mut h.rsc, &old, &items); apply.push(at.elapsed()); t += PHONE_FRAME_MS; let at = Instant::now(); h.frame(t); let took = at.elapsed(); frame.push(took); if added_block { frame_new_block.push(took); } else { frame_same_block.push(took); } if n == 0 || n == opened.stream_tail.len() - 1 { println!( " event {n:>3} of {}: items={} fold {:?} apply {:?}", opened.stream_tail.len(), items.len(), fold[n], apply[n], ); } } summarise("fold", &fold); summarise("apply", &apply); summarise("frame", &frame); summarise("frame/same-block", &frame_same_block); summarise("frame/new-block", &frame_new_block); println!( " primitives on screen at the end: {}", h.render.active_primitive_count() ); } #[test] #[ignore] fn what_reshaping_a_growing_message_costs() { use iris::prelude::*; let mut h = Harness::new(phone_size(), PHONE_SCALE); const DELTA: &str = "the quick brown fox jumps over the lazy dog "; let attrs = TextAttrs::default(); let width = Some(phone_size().x); let mut buffer = iris::prelude::TextBuffer::new_empty(); let mut text = String::new(); let mut per_delta = Vec::new(); let mut total = Duration::ZERO; for n in 1..=400 { text.push_str(DELTA); buffer.set_text(text.clone()); let at = Instant::now(); buffer.shape(&mut h.rsc.ui.text, &attrs, width, PHONE_SCALE); let took = at.elapsed(); per_delta.push(took); total += took; if n % 100 == 0 { println!( " after {n:>3} deltas ({:>6} chars): this reshape {:?}", text.len(), took ); } } println!( " 400 deltas: {:?} of shaping in total, {:?} per delta at the end", total, per_delta.last().unwrap() ); summarise("reshape", &per_delta); println!(" at the sizes real replies reach:"); for chars in [147usize, 449, 836, 1580] { let mut sample = String::new(); while sample.len() < chars { sample.push_str(DELTA); } sample.truncate(chars); let mut buffer = iris::prelude::TextBuffer::new(sample); let at = Instant::now(); buffer.shape(&mut h.rsc.ui.text, &attrs, width, PHONE_SCALE); println!(" {chars:>5} chars: {:?}", at.elapsed()); } } #[test] #[ignore] fn where_a_streamed_deltas_cost_is() { use ai_app::client::markdown_blocks::{common_prefix, split_blocks}; const SENTENCE: &str = "The quick brown fox jumps over the lazy dog. "; let mut src = String::new(); let mut blocks = Vec::new(); let mut split = Vec::new(); let mut compare = Vec::new(); for n in 1..=400 { src.push_str(SENTENCE); if n % 8 == 0 { src.push_str("\n\n"); } let at = Instant::now(); let new_blocks = split_blocks(&src); split.push(at.elapsed()); let at = Instant::now(); let _ = common_prefix(&blocks, &new_blocks); compare.push(at.elapsed()); blocks = new_blocks; if n % 100 == 0 { println!( " after {n:>3} deltas ({:>6} chars, {} blocks): split {:?} compare {:?}", src.len(), blocks.len(), split[n - 1], compare[n - 1], ); } } summarise("split_blocks", &split); summarise("common_prefix", &compare); let total: Duration = split.iter().chain(compare.iter()).sum(); println!(" 400 deltas: {total:?} in block-splitting and comparison alone"); } #[test] #[ignore] fn what_the_fixture_streams() { use ai_app::client::markdown_blocks::split_blocks; use ai_app::client::transcript_fold::TranscriptItem; 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"); let backlog = opened.items.clone(); let mut items = opened.items; let before = items.len(); for event in &opened.stream_tail { items = ai_app::client::transcript_fold::fold_event(&items, event); } println!("{} items -> {}", before, items.len()); let biggest = backlog .iter() .filter_map(|item| match item { TranscriptItem::AssistantMsg { text, .. } => Some(text), _ => None, }) .map(|text| { let blocks = split_blocks(text); ( text.len(), blocks.len(), blocks.iter().map(|b| b.source.len()).max().unwrap_or(0), ) }) .max_by_key(|(_, _, longest)| *longest); if let Some((chars, blocks, longest)) = biggest { println!( " backlog's largest single block: {longest} chars (in a {chars}-char message of {blocks} blocks)" ); } for item in items.iter().rev().take(4) { let (kind, text) = match item { TranscriptItem::AssistantMsg { text, .. } => ("AssistantMsg", text.clone()), TranscriptItem::UserMsg { text, .. } => ("UserMsg", text.clone()), TranscriptItem::ToolRun { input, output, .. } => { ("ToolRun", format!("{input}{output}")) } _ => ("other", String::new()), }; let blocks = split_blocks(&text); println!( " tail {kind}: {} chars in {} blocks; longest block {} chars", text.len(), blocks.len(), blocks.iter().map(|b| b.source.len()).max().unwrap_or(0), ); } }