43a3a345e405a6019332f468d2b9cda8da9d7e70
5
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
43a3a345e4 |
Incremental text: parley cannot, the app already does it, and the 9.5ms is the fixture
Iris asked to look into incremental text rendering, hoping parley supported it. It does not, by design: a `Layout` re-linebreaks and re-aligns freely but "if the text content or the styles applied to that content change then a new `Layout` must be created", its LRU cache holds harfrust's per-font shaper data rather than shaped runs, and its own `PlainEditor` rebuilds the whole layout from the whole buffer on every keystroke. The app already does what incremental layout would buy: `RowBlocks:: apply_delta` keeps one `TextEdit` per markdown block and re-shapes only the one a delta landed in. Re-splitting the markdown to find it is 18µs at 18,000 characters; comparing the blocks is 470ns. What is left is one `TextBuffer::shape` of that block, linear in its length at ~0.23ms per 1,000 characters here -- and the bench fixture's streamed message is 14,888 characters in a *single* block, a run-on paragraph with no blank line in it, so every delta reshapes all of it. That is 3.5ms of the measured 3.86ms frame. Real replies are not that: across 7,706 top-level blocks from 3,675 real assistant messages on this machine (lengths only, no content copied anywhere), p50 147 characters, p90 449, p99 836, largest 1,580, nothing above 4,000; code fences p50 126, largest 589. At those sizes a reshape is 48µs to 372µs here, roughly 0.12-0.93ms on the phone -- inside a 120Hz budget with no incremental anything. So the recommendation is not to build it, and to give the fixture's streamed message the paragraph structure a real reply has instead. Three runs added to `frame_profile.rs` so none of this is re-derived: what reshaping a growing message costs (including at the sizes real replies reach), where a delta's cost is, and what the fixture actually streams. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
9bf714fa2e |
The frame report says what it measured: idle is not stutter, waiting is not late
Iris's phone came back "now THAT is smooth", and reading that run against the bench's own timings found three things the report was getting wrong -- two of them shipped yesterday in the fix for the last three. `missed vsyncs` counted idleness. Every gap between frames was treated as cadence, so the bench's own pauses read as stutter: 276 for sixteen 300ms rests between flings, 2410 for twelve hundred 50ms keystroke gaps, 821 for four hundred 50ms stream gaps -- each within a few percent of the arithmetic. A gap now measures anything only if the frame before it had asked for another one. `late` counted the swapchain wait as cost. A well-paced loop spends each frame blocked in the acquire, so its total sits at exactly one refresh period and every frame lands on the budget boundary -- 0.4ms of work and 5.7ms of waiting is not a late frame. It is judged on `FrameParts::work`. And the refresh rate is the larger of what the platform claims and what the run sustained, because each can only be wrong one way. `Display.getRefreshRate()` answered 60 for a run that drew 3405 frames in 33.1s, since a phone that varies its rate answers with whatever mode it is in when asked. The first attempt at measuring it instead took the fastest tenth of the gaps and reported 88Hz for this repo's 60Hz emulator, whose app manages 54 -- a budget no frame there could meet, invented out of the app's best moments, and caught only by running the corrected report on the emulator before shipping it. A sustained rate is a floor and cannot do that. Both are printed when they disagree. Also corrected in the docs: "103fps on a 120Hz screen" divided the fling phase by its whole duration, rests included. Both runs sustained ~120.3fps through the motion, so the callback ordering was never costing frames -- what changed is the clock, which moves no frame count at all, which is exactly why nothing in a report could show it. `fling_profile.rs` is `frame_profile.rs` and gained a stream run, which says where the frame time now is: folding an arriving event is 0.35ms and applying the diff 0.41ms, while the frame is 3.86ms here and 9.5ms on the phone. 401 events move the item count 652 -> 654, so nearly every one is a delta into the same row -- the cost is re-shaping one growing message, not `fold_event`'s per-event clone, which was the hypothesis and is what measuring it ruled out. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
42d54eec95 |
Fling: the vsync clock, the frame ask, and a report that can say what it measured
Iris, from her phone: "some stuttering when flinging in particular. Harder to notice with my finger directly moving the scroll." Her fling phase was 103fps on a 120Hz screen at p50 6.3ms. Two of the four things found are corrections to the instrument, not the renderer. The swapchain acquire -- `get_current_texture`, which *blocks* until the compositor frees an image -- was inside the span the report called iris's CPU work, so a fling comfortably ahead of the display read as milliseconds of being slow. A frame is now three measured parts (`FrameParts`: build, acquire, submit), per phase as well as per run. And nothing could say a frame was never *produced*: `late` counts frames that cost too much, which a reader does not see, while a frame that never happens leaves the last one up for two refreshes, which is the stutter. `PhaseStats::missed` counts vsyncs nothing was drawn for. It closes on the emulator: 1548 frames + 452 missed over 33.0s at 60Hz is 1980 vsyncs. The other two are the frame loop. `Choreographer.postFrameCallback` schedules for the next vsync after the call, and iris asked at the *end* of the callback -- so any frame whose work ran past the boundary registered too late and got the vsync after, one frame over budget silently costing a second. It is asked for immediately after `tick_animations` now, on both backends. And the fling was advanced on `Instant::now()` rather than the vsync `do_frame` carries: frames are presented on an even cadence whatever clock computes them, so sampling the spline at "whenever the callback ran" moves the content unevenly with no frame late enough to appear in any report -- and a drag never had it, which is the asymmetry Iris described. `PointerClock` is `DeviceClock` and the view keeps one, anchored by whichever of a touch or a frame comes first, so a fling is advanced on the clock its velocity was measured on. `opt-level` for the Android release build goes from "s" to 3. The table in RUST.md picked "s" on bytes alone; over the same warm fling eight times iris's own per-frame work is p90 0.15ms/p99 0.42ms at "s" against p90 0.09ms/p99 0.26ms at 3, for 1.8 MB of arm64 APK. `app-rust/tests/fling_profile.rs` is the rig that established what a fling frame actually costs and is kept for next time (Iris: "please keep the profiling rig around for future use"): only one frame in six lays anything out, and the multi-millisecond spikes are all first-pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
09778346a0 |
Prune the docs of work already done: 18,252 -> 7,567 lines
Iris: "the documentation is also pretty crazy too. Can you go through it
and remove everything that's already done and decided? There's entire md
files iirc for projects already complete. And many with checkboxes already
ticked off that just fill up context."
docs/RUST.md 8503 -> 905 the framework bake-off (options,
recommendation, twelve closed
experiment boxes) and two superseded
"where things stand" sections, out;
what the experiments settled kept as
one line each
docs/IRIS_TODO.md 1383 -> 229 fifty closed items and six
phone-report sections whose defects
are all fixed
docs/LAYOUT.md 1116 -> 829 the pre-implementation framing: the
old trait, the checklist, the
migration list, the pass conditions
docs/TEXTURES.md 496 -> 240 the prior-art survey, the proposal
and its review, all implemented
docs/REVIEW-*.md 673 -> 0 two completed review passes; the two
findings left open on purpose (mask
hit-testing, the phone's font set)
moved into RUST.md
What survives a prune is what cannot be cheaply re-derived: measurements
(the APK-size table, the phone bench reports), dead ends, invariants and
their reasons, and the design of what exists now rather than the route to
it. AGENTS.md now says that, so the next session prunes as it goes rather
than appending; docs/IRIS_TODO.md's header says items are deleted when
they land rather than ticked.
Deleting the two review files left eighteen citations dangling in code
comments that state their reason inline and cited the file for provenance
only — those now read "(review, 2026-09-06)" and carry no dead pointer.
The emulator's measured GPU capabilities moved to the this-machine-android
skill, where machine facts belong. IRIS.md and DECISIONS.md are dated
records and were not rewritten; each gained one note that paths in older
entries predate the 2026-09-08 crate merge, pointing at the mapping.
Not touched, deliberately: docs/DECISIONS.md's entries (that file *is* the
queue of things for Iris to review, so deleting decided items would remove
what it exists for) and iris/readme.md and iris/TODO, which are hers.
Verified: ./run-tests.sh and `cd iris && cargo test` green, clippy and fmt
clean in every workspace, and every remaining docs/*.md cross-reference
resolves.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
6d5a231f5c |
iris is the framework alone; the app is one crate in app-rust/
Iris: "the organization of the rust rewrite is a mess right now... there shouldn't be anything related to the app inside of iris. Iris is supposed to be the UI framework alone." And, on the crate count: "I'm confused why the app only code needs more than one crate though." Nine cargo workspaces become three, and the port's project code -- which sat in five places, four of them inside the framework -- becomes one crate, `ai-app`, in `app-rust/`: client-core -> app-rust/src/client iris/transcript-ui -> app-rust/src/ui iris/transcript-fixture -> app-rust/src/ui/fixture.rs + tests/ + touch/ iris/desktop-app -> app-rust/src/desktop + src/bin_desktop.rs iris/android-app -> app-rust/src/android + android-project/ android-shell -> app-rust/src/shell iris/ keeps core, macro, the iris crate, tabs-ui and rig-input, and now mentions no session, transcript, setup or server anywhere. Only two of the old splits had a reason that survived reading. event-model stays a crate at the repo root because server/ depends on it too, so a crate is what makes the backend and the app agree by construction. The two Android .so names looked like a hard constraint -- a package produces one library artifact -- until P2 turned out to already plan merging those two Android apps into one; both faces now come out of libai_app.so, picked apart by features so `--no-default-features --features shell` keeps wgpu, parley and iris out of the Compose app's APK. docs/RUST.md's "One app crate" has the rest, including what each remaining feature is for. DECISIONS.md and SUBAGENTS.md move into docs/ with everything else. Verified: ./run-tests.sh and `cd iris && cargo test` green, clippy and fmt clean in all five workspaces, `cargo ndk -t x86_64` links libai_app.so, build-apk.sh produces an APK that installs and launches on this checkout's emulator (Gl ... virgl, as expected), and the phone-sized headless screenshot renders the transcript unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |