iris-android-app: the bench's fixture half comes from transcript-fixture

The fixture bytes, the backlog/tail split and the fold into a screen
were `bench_client.rs`'s alone; they are `transcript-fixture`'s now, so
the Android bench, the headless harness and the phone-shaped desktop
window open one screen from one copy (AGENTS.md: nothing UI-shaped in a
platform crate). What stays here is the JNI half -- clipboard, battery,
IME, the report and the four phases.

Built with `cargo ndk -t arm64-v8a -P 29 build --features
"transcript-screen bench"`; the two warnings it prints (bench_jni's
unused overlay methods, the unused `tabs-ui` dependency under this
feature set) predate this change.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
irisandClaude Fable 5.1 committed 2026-09-07 12:27:04 -04:00
1 parent 333220196e
commit 6840edf61e
5 files changed
+70 -66

No files matched your search

+12
View File
@@ -1774,6 +1774,7 @@ dependencies = [
"serde_json", "serde_json",
"tabs-ui", "tabs-ui",
"tokio", "tokio",
"transcript-fixture",
"transcript-ui", "transcript-ui",
] ]
@@ -3864,6 +3865,17 @@ dependencies = [
"once_cell", "once_cell",
] ]
[[package]]
name = "transcript-fixture"
version = "0.1.0"
dependencies = [
"client-core",
"event-model",
"iris",
"serde_json",
"transcript-ui",
]
[[package]] [[package]]
name = "transcript-ui" name = "transcript-ui"
version = "0.1.0" version = "0.1.0"
+5 -1
View File
@@ -29,6 +29,10 @@ log = "0.4.28"
# which Cargo's `unused_dependencies` lint (on by default) correctly flags. # which Cargo's `unused_dependencies` lint (on by default) correctly flags.
tabs-ui = { path = "../tabs-ui", optional = true } tabs-ui = { path = "../tabs-ui", optional = true }
transcript-ui = { path = "../transcript-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 } client-core = { path = "../../client-core", optional = true }
event-model = { path = "../../event-model", optional = true } event-model = { path = "../../event-model", optional = true }
serde_json = { version = "1", features = ["float_roundtrip"], 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 # `event-model` -- `lib.rs`'s `ActiveClient` selection gives this feature
# priority over `transcript-screen`'s own `TranscriptClient` when both are # priority over `transcript-screen`'s own `TranscriptClient` when both are
# listed, which is how this crate's build command names both explicitly. # 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] [profile.release]
panic = "abort" panic = "abort"
+13 -51
View File
@@ -6,14 +6,11 @@
//! //!
//! **Reuses `transcript_client.rs`'s shape** (folded items, the same //! **Reuses `transcript_client.rs`'s shape** (folded items, the same
//! `TranscriptScreen::apply` incremental update on every event) with the //! `TranscriptScreen::apply` incremental update on every event) with the
//! network half replaced by the checked-in fixture, embedded with //! network half replaced by the checked-in fixture. Reading that fixture
//! `include_str!` -- `app/bench-fixture/assets/transcript.jsonl`, //! and folding it into a screen is **`transcript-fixture`'s** job, not
//! 1,915,760 bytes, generated by `app/bench-fixture/generate.py` and never //! this file's -- the same crate the headless harness and the
//! a real transcript (that file's own README). The first 3,200 lines are //! phone-shaped desktop window open, so all three measure one screen
//! the opening backlog, folded once through //! (AGENTS.md's sharing rule; moved out of here 2026-09-07). The tail is
//! `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,
//! replayed one at a time through `fold_event` -- the same fold path a //! replayed one at a time through `fold_event` -- the same fold path a
//! live SSE reply arrives on -- by the "Run benchmark" control below. //! live SSE reply arrives on -- by the "Run benchmark" control below.
//! Streaming through `apply` rather than a full rebuild per event is what //! Streaming through `apply` rather than a full rebuild per event is what
@@ -22,7 +19,7 @@
use crate::bench_jni::PlatformHandle; use crate::bench_jni::PlatformHandle;
use android_view::jni::{JavaVM, objects::GlobalRef}; 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 event_model::SeqEvent;
use iris::android::{AndroidAppState, AndroidRsc, AndroidUiState, HasAndroidUiState}; use iris::android::{AndroidAppState, AndroidRsc, AndroidUiState, HasAndroidUiState};
use iris::prelude::*; use iris::prelude::*;
@@ -30,13 +27,6 @@ use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant}; 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 /// RUST.md's "Benchmark v2" spec, written once so both apps' bench clients
/// implement the identical four phases -- see that box before changing any /// implement the identical four phases -- see that box before changing any
/// constant here, since a mismatch would make the two reports stop /// 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. /// when a later step in the same phase needs to read state back.
const ANIM_STEP_MS: u64 = 16; 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 /// 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 /// scrolls instead of growing -- roughly a third of a phone screen, the
/// share the pane used to reserve unconditionally. An empty report takes /// 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<serde_json::Value>, Vec<SeqEvent>) {
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: HasEvents>(rsc: &mut Rsc, message: &str) -> StrongWidget { fn placeholder<Rsc: HasEvents>(rsc: &mut Rsc, message: &str) -> StrongWidget {
wtext(message.to_string()) wtext(message.to_string())
.color(Color::WHITE) .color(Color::WHITE)
@@ -322,12 +285,12 @@ impl AndroidAppState for BenchClient {
last_top_pad: 0.0, last_top_pad: 0.0,
}; };
let (backlog, stream_tail) = parse_fixture(); match transcript_fixture::build_screen(rsc) {
client.stream_tail = stream_tail; Ok((opened, tree)) => {
match fold_page(&backlog) { client.items = opened.items;
Ok(items) => { client.stream_tail = opened.stream_tail;
client.items = items; (client.content)(rsc).set(tree);
client.rebuild_transcript(rsc); client.screen = Some(opened.screen);
} }
Err(message) => { Err(message) => {
client.show_message(rsc, &format!("Couldn't fold the bench fixture: {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) { fn rebuild_transcript(&mut self, rsc: &mut Rsc) {
let rows = group_tool_runs(&self.items); let (screen, tree) = transcript_ui::build_tree(rsc, transcript_fixture::rows(&self.items));
let (screen, tree) = transcript_ui::build_tree(rsc, rows);
(self.content)(rsc).set(tree); (self.content)(rsc).set(tree);
self.screen = Some(screen); self.screen = Some(screen);
} }
+38 -11
View File
@@ -100,22 +100,49 @@ pub fn rows(items: &[TranscriptItem]) -> Vec<TranscriptRow> {
group_tool_runs(items) group_tool_runs(items)
} }
/// Build the transcript screen over the fixture's opening page and make /// Everything a caller needs to run the fixture as an app screen would:
/// it the root -- what every layer of the rig opens. Returns the screen /// the screen, the folded items behind it, and the events not yet
/// and the items behind it, so a caller can go on streaming the tail /// streamed. The tree itself comes back separately from
/// through `fold_event`/`TranscriptScreen::apply` as the Android bench /// [`build_screen`], since whoever takes it owns it.
/// does. pub struct Opened {
pub fn open<Rsc: HasEvents>( pub screen: transcript_ui::TranscriptScreen,
rsc: &mut Rsc, pub items: Vec<TranscriptItem>,
ui_state: &mut impl HasRoot, /// The tail, for a caller that goes on replaying it one event at a
) -> Result<(transcript_ui::TranscriptScreen, Vec<TranscriptItem>), String> /// time through `fold_event`/`TranscriptScreen::apply` -- the
/// streaming phase of either app's benchmark.
pub stream_tail: Vec<SeqEvent>,
}
/// 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: HasEvents>(rsc: &mut Rsc) -> Result<(Opened, StrongWidget), String>
where where
Rsc::State: FocusHost + OpenUrl, Rsc::State: FocusHost + OpenUrl,
{ {
let fixture = Fixture::parse(); let fixture = Fixture::parse();
let items = fixture.backlog_items()?; let items = fixture.backlog_items()?;
let screen = transcript_ui::build(rsc, ui_state, rows(&items)); let (screen, tree) = transcript_ui::build_tree(rsc, rows(&items));
Ok((screen, 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: HasEvents>(rsc: &mut Rsc, ui_state: &mut impl HasRoot) -> Result<Opened, String>
where
Rsc::State: FocusHost + OpenUrl,
{
let (opened, tree) = build_screen(rsc)?;
ui_state.set_root(tree);
Ok(opened)
} }
#[cfg(test)] #[cfg(test)]
@@ -17,11 +17,10 @@ use transcript_fixture::{PHONE_FRAME_MS, PHONE_SCALE, phone_size};
/// anchor, which is what every assertion about scroll position reads. /// anchor, which is what every assertion about scroll position reads.
fn opened() -> (Harness, transcript_ui::TranscriptScreen) { fn opened() -> (Harness, transcript_ui::TranscriptScreen) {
let mut h = Harness::new(phone_size(), PHONE_SCALE); let mut h = Harness::new(phone_size(), PHONE_SCALE);
let (screen, _items) = let opened = transcript_fixture::open(&mut h.rsc, &mut h.state).expect("the fixture folds");
transcript_fixture::open(&mut h.rsc, &mut h.state).expect("the fixture folds");
h.frame(0); h.frame(0);
h.frame(PHONE_FRAME_MS); h.frame(PHONE_FRAME_MS);
(h, screen) (h, opened.screen)
} }
fn script(name: &str, text: &str) -> TouchScript { fn script(name: &str, text: &str) -> TouchScript {