diff --git a/iris/android-app/Cargo.lock b/iris/android-app/Cargo.lock index 9e5a6c7..6c4a19c 100644 --- a/iris/android-app/Cargo.lock +++ b/iris/android-app/Cargo.lock @@ -1774,6 +1774,7 @@ dependencies = [ "serde_json", "tabs-ui", "tokio", + "transcript-fixture", "transcript-ui", ] @@ -3864,6 +3865,17 @@ dependencies = [ "once_cell", ] +[[package]] +name = "transcript-fixture" +version = "0.1.0" +dependencies = [ + "client-core", + "event-model", + "iris", + "serde_json", + "transcript-ui", +] + [[package]] name = "transcript-ui" version = "0.1.0" diff --git a/iris/android-app/Cargo.toml b/iris/android-app/Cargo.toml index d0551bd..11f1664 100644 --- a/iris/android-app/Cargo.toml +++ b/iris/android-app/Cargo.toml @@ -29,6 +29,10 @@ log = "0.4.28" # which Cargo's `unused_dependencies` lint (on by default) correctly flags. tabs-ui = { path = "../tabs-ui", optional = true } transcript-ui = { path = "../transcript-ui", optional = true } +# P0's bench build only: the fixture and the folded screen both bench +# clients open, shared with the headless harness and the desktop window +# (docs/RUST.md's "Three test layers"). +transcript-fixture = { path = "../transcript-fixture", optional = true } client-core = { path = "../../client-core", optional = true } event-model = { path = "../../event-model", optional = true } serde_json = { version = "1", features = ["float_roundtrip"], optional = true } @@ -65,7 +69,7 @@ force-gles = ["iris/force-gles"] # `event-model` -- `lib.rs`'s `ActiveClient` selection gives this feature # priority over `transcript-screen`'s own `TranscriptClient` when both are # listed, which is how this crate's build command names both explicitly. -bench = ["transcript-screen", "dep:libc", "dep:tokio"] +bench = ["transcript-screen", "dep:transcript-fixture", "dep:libc", "dep:tokio"] [profile.release] panic = "abort" diff --git a/iris/android-app/src/bench_client.rs b/iris/android-app/src/bench_client.rs index 212b2c7..9544a2d 100644 --- a/iris/android-app/src/bench_client.rs +++ b/iris/android-app/src/bench_client.rs @@ -6,14 +6,11 @@ //! //! **Reuses `transcript_client.rs`'s shape** (folded items, the same //! `TranscriptScreen::apply` incremental update on every event) with the -//! network half replaced by the checked-in fixture, embedded with -//! `include_str!` -- `app/bench-fixture/assets/transcript.jsonl`, -//! 1,915,760 bytes, generated by `app/bench-fixture/generate.py` and never -//! a real transcript (that file's own README). The first 3,200 lines are -//! the opening backlog, folded once through -//! `client_core::transcript_fold::fold_page` exactly as a real -//! `/transcript` page would be (then a full `transcript_ui::build_tree`, -//! same as any first load); the remaining ~400 are the streaming tail, +//! network half replaced by the checked-in fixture. Reading that fixture +//! and folding it into a screen is **`transcript-fixture`'s** job, not +//! this file's -- the same crate the headless harness and the +//! phone-shaped desktop window open, so all three measure one screen +//! (AGENTS.md's sharing rule; moved out of here 2026-09-07). The tail is //! replayed one at a time through `fold_event` -- the same fold path a //! live SSE reply arrives on -- by the "Run benchmark" control below. //! Streaming through `apply` rather than a full rebuild per event is what @@ -22,7 +19,7 @@ use crate::bench_jni::PlatformHandle; use android_view::jni::{JavaVM, objects::GlobalRef}; -use client_core::transcript_fold::{TranscriptItem, fold_event, fold_page, group_tool_runs}; +use client_core::transcript_fold::{TranscriptItem, fold_event}; use event_model::SeqEvent; use iris::android::{AndroidAppState, AndroidRsc, AndroidUiState, HasAndroidUiState}; use iris::prelude::*; @@ -30,13 +27,6 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; -/// bench-fixture/README.md: the first `BACKLOG_COUNT` non-blank lines are -/// the opening window; the rest are the streaming tail. Kept in sync with -/// `BenchFixture.kt`'s identical constant by hand -- both read the same -/// checked-in file, so a mismatch would only mean the two apps' bench -/// builds open a different split of it, not a wrong-vs-right answer. -const BACKLOG_COUNT: usize = 3200; - /// RUST.md's "Benchmark v2" spec, written once so both apps' bench clients /// implement the identical four phases -- see that box before changing any /// constant here, since a mismatch would make the two reports stop @@ -90,8 +80,6 @@ const KEYBOARD_WAIT_MS: u64 = 1_000; /// when a later step in the same phase needs to read state back. const ANIM_STEP_MS: u64 = 16; -const FIXTURE_JSONL: &str = include_str!("../../../app/bench-fixture/assets/transcript.jsonl"); - /// How much of the screen a *filled* benchmark report may take before it /// scrolls instead of growing -- roughly a third of a phone screen, the /// share the pane used to reserve unconditionally. An empty report takes @@ -158,31 +146,6 @@ impl HasAndroidUiState for BenchClient { } } -/// Parses the fixture once: `serde_json::Value`s for the backlog -/// (`fold_page` takes a page of raw wire JSON, same as a real -/// `/transcript` response) and folded `SeqEvent`s for the tail (`fold_event` -/// takes one live wire event at a time, same as a real SSE frame). -fn parse_fixture() -> (Vec, Vec) { - let lines: Vec<&str> = FIXTURE_JSONL - .lines() - .filter(|line| !line.trim().is_empty()) - .collect(); - let mut backlog = Vec::with_capacity(BACKLOG_COUNT.min(lines.len())); - let mut stream_tail = Vec::new(); - for (i, line) in lines.iter().enumerate() { - let value: serde_json::Value = - serde_json::from_str(line).expect("bench fixture is generated JSON, always valid"); - if i < BACKLOG_COUNT { - backlog.push(value); - } else { - let event: SeqEvent = serde_json::from_value(value) - .expect("bench fixture event matches event-model's SeqEvent"); - stream_tail.push(event); - } - } - (backlog, stream_tail) -} - fn placeholder(rsc: &mut Rsc, message: &str) -> StrongWidget { wtext(message.to_string()) .color(Color::WHITE) @@ -322,12 +285,12 @@ impl AndroidAppState for BenchClient { last_top_pad: 0.0, }; - let (backlog, stream_tail) = parse_fixture(); - client.stream_tail = stream_tail; - match fold_page(&backlog) { - Ok(items) => { - client.items = items; - client.rebuild_transcript(rsc); + match transcript_fixture::build_screen(rsc) { + Ok((opened, tree)) => { + client.items = opened.items; + client.stream_tail = opened.stream_tail; + (client.content)(rsc).set(tree); + client.screen = Some(opened.screen); } Err(message) => { client.show_message(rsc, &format!("Couldn't fold the bench fixture: {message}")) @@ -546,8 +509,7 @@ impl BenchClient { } fn rebuild_transcript(&mut self, rsc: &mut Rsc) { - let rows = group_tool_runs(&self.items); - let (screen, tree) = transcript_ui::build_tree(rsc, rows); + let (screen, tree) = transcript_ui::build_tree(rsc, transcript_fixture::rows(&self.items)); (self.content)(rsc).set(tree); self.screen = Some(screen); } diff --git a/iris/transcript-fixture/src/lib.rs b/iris/transcript-fixture/src/lib.rs index 4ba65d3..3da670a 100644 --- a/iris/transcript-fixture/src/lib.rs +++ b/iris/transcript-fixture/src/lib.rs @@ -100,22 +100,49 @@ pub fn rows(items: &[TranscriptItem]) -> Vec { group_tool_runs(items) } -/// Build the transcript screen over the fixture's opening page and make -/// it the root -- what every layer of the rig opens. Returns the screen -/// and the items behind it, so a caller can go on streaming the tail -/// through `fold_event`/`TranscriptScreen::apply` as the Android bench -/// does. -pub fn open( - rsc: &mut Rsc, - ui_state: &mut impl HasRoot, -) -> Result<(transcript_ui::TranscriptScreen, Vec), String> +/// Everything a caller needs to run the fixture as an app screen would: +/// the screen, the folded items behind it, and the events not yet +/// streamed. The tree itself comes back separately from +/// [`build_screen`], since whoever takes it owns it. +pub struct Opened { + pub screen: transcript_ui::TranscriptScreen, + pub items: Vec, + /// The tail, for a caller that goes on replaying it one event at a + /// time through `fold_event`/`TranscriptScreen::apply` -- the + /// streaming phase of either app's benchmark. + pub stream_tail: Vec, +} + +/// Build the transcript screen over the fixture's opening page, without +/// claiming the window's root -- `transcript_ui::build_tree`'s own split, +/// for a caller (the Android bench) that puts the screen inside a shell +/// of its own. +pub fn build_screen(rsc: &mut Rsc) -> Result<(Opened, StrongWidget), String> where Rsc::State: FocusHost + OpenUrl, { let fixture = Fixture::parse(); let items = fixture.backlog_items()?; - let screen = transcript_ui::build(rsc, ui_state, rows(&items)); - Ok((screen, items)) + let (screen, tree) = transcript_ui::build_tree(rsc, rows(&items)); + Ok(( + Opened { + screen, + items, + stream_tail: fixture.stream_tail, + }, + tree, + )) +} + +/// [`build_screen`] with the screen as the window's root -- what the +/// headless harness and the desktop window open. +pub fn open(rsc: &mut Rsc, ui_state: &mut impl HasRoot) -> Result +where + Rsc::State: FocusHost + OpenUrl, +{ + let (opened, tree) = build_screen(rsc)?; + ui_state.set_root(tree); + Ok(opened) } #[cfg(test)] diff --git a/iris/transcript-fixture/tests/phone_screen.rs b/iris/transcript-fixture/tests/phone_screen.rs index 206a45a..c94f8e2 100644 --- a/iris/transcript-fixture/tests/phone_screen.rs +++ b/iris/transcript-fixture/tests/phone_screen.rs @@ -17,11 +17,10 @@ use transcript_fixture::{PHONE_FRAME_MS, PHONE_SCALE, phone_size}; /// anchor, which is what every assertion about scroll position reads. fn opened() -> (Harness, transcript_ui::TranscriptScreen) { let mut h = Harness::new(phone_size(), PHONE_SCALE); - let (screen, _items) = - transcript_fixture::open(&mut h.rsc, &mut h.state).expect("the fixture folds"); + let opened = transcript_fixture::open(&mut h.rsc, &mut h.state).expect("the fixture folds"); h.frame(0); h.frame(PHONE_FRAME_MS); - (h, screen) + (h, opened.screen) } fn script(name: &str, text: &str) -> TouchScript {