docs: record P0's Compose half (bench build, fixture, smoke run)
RUST.md's P0 box gets the emulator smoke run's report and what's done vs. left; DECISIONS.md gets a dated summary entry; AGENTS.md's "Checking your work" and "The rigs" get one paragraph each on the bench build type and app/bench-fixture/. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
e6c884a0cd
commit
68b48cfd14
3 files changed
+285
-1
No files matched your search
@@ -96,7 +96,11 @@ two icon buttons the same width without either being given one — and why
|
||||
:androidApp:compileDebugKotlin :androidApp:lintDebug
|
||||
:androidApp:testDebugUnitTest`. The unit tests are JVM-only and cover the
|
||||
syntax highlighter, the ANSI parser and the transcript cache — the app's
|
||||
pure logic with no Android in it.
|
||||
pure logic with no Android in it. Touching anything under `BenchFixture.kt`,
|
||||
`BenchNetwork.kt`, `BenchRun.kt` or the `bench` build type also needs
|
||||
`:androidApp:compileBenchKotlin :androidApp:lintBench` — a second build
|
||||
type compiles separately and lint has caught real bugs debug alone never
|
||||
would (see "Android Lint" below).
|
||||
- **Android Lint is not optional and is not run by a build.** It found a
|
||||
crash that had been shipping (`java.time` on a minSdk-24 app with
|
||||
desugaring off) and later a permission check that silently dropped every
|
||||
@@ -156,6 +160,27 @@ two icon buttons the same width without either being given one — and why
|
||||
|
||||
Each exists because something was invisible without it.
|
||||
|
||||
- **The `bench` build type and `app/bench-fixture/`** exist for P0 (RUST.md
|
||||
and DECISIONS.md's 2026-09-05 entries), the phone benchmark gate Iris
|
||||
asked for before porting continues: a deterministic, checked-in synthetic
|
||||
transcript (`app/bench-fixture/generate.py`, never a real one) that both
|
||||
this app and iris open with no server, so a frame-time comparison
|
||||
measures the renderer rather than the data. `./build-apk.sh bench` builds
|
||||
it — own application id (`com.example.aiapp.bench`) and label ("AI
|
||||
Sessions bench") so it installs beside a real enrollment rather than
|
||||
replacing it. Opening it goes straight to a session screen holding the
|
||||
fixture (no enrollment, no permission prompts) with a "Run benchmark"
|
||||
control beside "Copy" in session settings: it drives the same scroll loop
|
||||
and streaming phase `transcript-bench.sh`/`stream-bench.sh` drive over
|
||||
`ui-trace`, but in-process, since a real phone has no usable system
|
||||
tracing and no agent can drive one (this-machine-android's skill).
|
||||
`BenchFixture.kt`/`BenchNetwork.kt` fake the backend by installing a
|
||||
`URLStreamHandlerFactory` that answers `TranscriptSource`/`EventStream`'s
|
||||
requests from an in-memory copy of the fixture instead of opening a
|
||||
socket — so the fold, the paging and `uniqueItems` under test are the
|
||||
screen's real ones, never a shortcut built just for this. The report
|
||||
gains a `bench:` section (process CPU time, peak RSS, battery current) on
|
||||
every build, empty except when `BenchRun.kt` filled it in.
|
||||
- **`app/ui-sandbox.sh`** — a second `ai-server` with its own `$HOME`, config
|
||||
and data directory, holding eight invented Claude Code transcripts and a
|
||||
`claude` that is two lines of shell. **That isolation is the point**: the
|
||||
|
||||
@@ -7,6 +7,49 @@ marked **DEFERRED** are ones the agent chose not to decide alone.
|
||||
|
||||
## 2026-09-05
|
||||
|
||||
- **P0's Compose half is built and smoke-tested on the emulator** — the
|
||||
`bench` build type, the shared `app/bench-fixture/` transcript, and an
|
||||
in-process fake backend (`BenchFixture.kt`/`BenchNetwork.kt`) that
|
||||
answers `TranscriptSource`/`EventStream` from an in-memory event log
|
||||
instead of a real server, so the fold and paging under test are the real
|
||||
ones. Full account, the smoke run's report, and what is deliberately
|
||||
left (the iris half, the real on-phone runs) are in RUST.md's P0 box.
|
||||
Not a decision to review so much as the gate itself now being runnable —
|
||||
flagged here because it is the first half of something Iris explicitly
|
||||
asked to see before P1.
|
||||
|
||||
- **The intermittent touch-scroll dropout is root-caused and fixed: a
|
||||
missed `ACTION_DOWN` hit-test, not the previously-suspected coalesced
|
||||
first `ACTION_MOVE`.** Diagnosed by temporary logcat tracing of every
|
||||
touch event, `DragArbiter` state transition and `Selection::drag`
|
||||
dispatch (removed once confirmed), reproduced on this checkout's own
|
||||
emulator against a real sandbox session. The trace showed the actual
|
||||
mechanism: a gesture's `ACTION_DOWN` lands wherever the finger actually
|
||||
is, which is not guaranteed to fall inside the same row-local sensor
|
||||
region a later `ACTION_MOVE` in the same gesture lands in (a row's own
|
||||
padding/gap, or its non-selectable sender-name header, is
|
||||
pointer-transparent to `iris::sense::CursorSense`). When that happens,
|
||||
the widget that ends up handling the gesture never saw `PressStart`, so
|
||||
`DragArbiter` sits in `Idle` — which answers every subsequent frame with
|
||||
`Undecided` and has no way to tell "no press is happening" from "a press
|
||||
is happening but I missed its start," so it never recovers on its own
|
||||
for the rest of that gesture. One real trace showed exactly this: touch
|
||||
`Down`/`Move`/`Up` all delivered correctly, but zero `PressStart`
|
||||
reaching the arbiter, `state=Idle` unchanged from first frame to last.
|
||||
Fixed at the call site that has the context to recover
|
||||
(`iris::transcript_ui::selection::Selection::drag`,
|
||||
`iris/transcript-ui/src/selection.rs`): a new `DragArbiter::is_idle()`
|
||||
(`iris/src/sense.rs`) lets it notice a `Pressing` frame arriving with the
|
||||
arbiter still `Idle` — which can only mean a missed `PressStart`, since a
|
||||
`Pressing` sense requires the button to genuinely be down — and start the
|
||||
press there instead of where it was missed. Three new unit tests in
|
||||
`sense.rs`'s `drag_arbiter_tests` and one in `transcript-ui`'s
|
||||
`selection::tests` (the latter fails on the code before this fix).
|
||||
Commit follows. Not the same failure the earlier pass's `DECISIONS.md`
|
||||
DEFERRED item speculated about (a coalesced first `ACTION_MOVE` skipping
|
||||
slop detection) — that hypothesis is now ruled out; the arbiter's own
|
||||
slop/long-press logic was never wrong. RUST.md's I5 box,
|
||||
"Touch-scroll dropout root-caused, 2026-09-05" has the full trace.
|
||||
- **P0, a phone benchmark gate before any porting, asked for by Iris
|
||||
2026-09-05**: "before P1 I'd like to see benchmarks & also maybe stress
|
||||
test on my own phone ... If it doesn't match compose reasonably well then
|
||||
|
||||
+216
@@ -36,6 +36,29 @@ session spending an afternoon on them again.
|
||||
|
||||
## Where things stand (2026-09-05)
|
||||
|
||||
- **The intermittent touch-scroll dropout is root-caused and fixed,
|
||||
2026-09-05.** Not the previously-suspected coalesced first
|
||||
`ACTION_MOVE` (ruled out) -- a gesture's `ACTION_DOWN` can land on a
|
||||
row's own padding/gap or its header, which `CursorSense` has no sensor
|
||||
over, so the widget that ends up handling the gesture only ever sees
|
||||
`Pressing` frames and `DragArbiter` never gets `press_start`, leaving it
|
||||
stuck in `Idle` (answers `Undecided` forever) for the rest of that
|
||||
gesture. Fixed in `Selection::drag` (`iris/transcript-ui/src/
|
||||
selection.rs`) via a new `DragArbiter::is_idle()` the caller checks to
|
||||
recover a missed press on the next `Pressing` frame. Four new unit
|
||||
tests (three in `iris/src/sense.rs`'s `drag_arbiter_tests`, one in
|
||||
`transcript-ui`'s `selection::tests`, the latter failing on the
|
||||
pre-fix code). See this box's own "Touch-scroll dropout root-caused,
|
||||
2026-09-05" subsection for the trace and what could and could not be
|
||||
re-verified this pass -- **this checkout's emulator turned out to be
|
||||
concurrently in use by another session's P0 benchmark work partway
|
||||
through verification** (its sandbox server was restarted, wiping this
|
||||
pass's test session, and its Compose `bench` app took window focus),
|
||||
so the "run iris-scroll.sh three times cleanly" and "re-take the
|
||||
host-GPU FrameReport row" pass conditions could not be completed
|
||||
end-to-end this pass. The fix itself is verified by direct, targeted
|
||||
logcat traces taken before that interference began, not by the
|
||||
aggregate script.
|
||||
- **Decided 2026-09-05: iris over Masonry**, by Iris, from the host-GPU
|
||||
numbers in I5's box and E1/E2's findings. See the Recommendation's item
|
||||
3 and `DECISIONS.md`. Next: the remaining screens and the app on iris —
|
||||
@@ -3201,6 +3224,116 @@ silently on real hardware.
|
||||
`e2a1fad`'s own message. `docs/DECISIONS.md`'s DEFERRED item is
|
||||
updated with this section's host-GPU table below.
|
||||
|
||||
**Touch-scroll dropout root-caused, 2026-09-05.** Diagnosed as
|
||||
instructed: temporary `log::info!` tracing on every touch event
|
||||
reaching `IrisViewPeer::on_touch_event` (`iris/src/android/view.rs`),
|
||||
every `DragArbiter` state transition (`press_start`/`update`/
|
||||
`release`, `iris/src/sense.rs`), and every `Selection::drag`
|
||||
dispatch (`iris/transcript-ui/src/selection.rs`) -- all removed once
|
||||
the cause was confirmed, per AGENTS.md's "keep the build clean."
|
||||
Reproduced with `app/iris-scroll.sh` against a real sandbox session
|
||||
(30 sent markdown messages, `EMU_GPU` unset / `-gpu host`,
|
||||
`--features transcript-screen,force-gles`, release build, same
|
||||
recipe as this box's own "-gpu host" pass above).
|
||||
|
||||
*The trace.* Of 24 swipes in one run, 5 produced zero `render()`
|
||||
calls each -- one at the very start of the run, four consecutive
|
||||
later (swipes 22-25) -- exactly the "several consecutive swipes
|
||||
produce nothing, an identical retry then works" shape from the
|
||||
earlier pass's report. Correlating the three log streams by
|
||||
timestamp: every one of those 5 swipes delivered a normal
|
||||
`Down`/`Move`×N/`Up` sequence to `on_touch_event` (touch delivery
|
||||
was never the problem), but `Selection::drag` never once saw
|
||||
`PressStart` for the whole gesture -- only `Pressing`, starting from
|
||||
the very first `Move`. `DragArbiter::update`'s `Idle` arm answers
|
||||
every such frame with `Undecided` and never transitions state (there
|
||||
is no way for pure state to tell "no press is happening" from "a
|
||||
press is happening but I missed its start"), so the arbiter sat in
|
||||
`Idle` from the gesture's first frame to its last, `release()` on
|
||||
`Up` its only state change (`Idle` -> `Idle`, a no-op). The row this
|
||||
landed on registered its `PressStart` correctly on a *different*
|
||||
point in the very next successful swipe at the identical screen
|
||||
coordinate -- confirming the miss is about *where the content
|
||||
happens to be under that pixel when `ACTION_DOWN` fires*, not about
|
||||
timing or a coalesced event.
|
||||
|
||||
*Why `ACTION_DOWN` misses a row's sensor.* Each row's `CursorSense`
|
||||
handler is registered only on its `TextEdit` field
|
||||
(`transcript-ui/src/row.rs`'s `build_text_row`), not on the row's
|
||||
`.pad(10)` margin, the `.gap(4)` between the sender-name header and
|
||||
the field, or the header itself (`Span::empty`/a plain `wtext` with
|
||||
no handler). A real touch's down-point is wherever the finger
|
||||
actually is, with no reason to prefer text over padding, and the
|
||||
list has no sensor of its own to fall back to (`iris::widget::list`
|
||||
registers none) -- pan is reachable *only* through a row's own
|
||||
arbiter. So roughly one in five swipes in this run started on a
|
||||
pixel no sensor covered.
|
||||
|
||||
*The fix.* `iris::sense::DragArbiter` gains `pub fn is_idle(&self)`,
|
||||
documented as the recovery signal: a caller that gets a `Pressing`
|
||||
frame while the arbiter reports `is_idle()` knows the button is
|
||||
genuinely down (that is what `Pressing` means) with no matching
|
||||
`press_start` on record, which can only mean it was missed.
|
||||
`Selection::drag`'s match gains one arm, checked after `PressStart`/
|
||||
`PressEnd` and before the ordinary `_ => update(...)` case: `_ if
|
||||
self.arbiter.is_idle()` starts the press right there instead of
|
||||
where it was missed, using whatever `already_selected` holds at
|
||||
that later frame (the best available answer -- the true value at
|
||||
the actual `ACTION_DOWN` is unrecoverable once missed). This is
|
||||
the caller's fix, not the arbiter's, because only the caller knows
|
||||
what `already_selected` should be; the arbiter's own slop/long-press
|
||||
logic was correct throughout and needed no change.
|
||||
|
||||
*Tests.* Four new, all passing on the fix and the first three failing
|
||||
without it: `sense.rs`'s `drag_arbiter_tests::
|
||||
is_idle_reports_a_press_that_was_never_started`,
|
||||
`::update_on_an_idle_arbiter_stays_undecided_forever_without_recovery`
|
||||
(documents the failure mode itself), `::
|
||||
a_caller_can_recover_a_missed_press_start_via_is_idle` (the pure-state
|
||||
half); and `transcript-ui/src/selection.rs`'s `tests::
|
||||
a_missed_press_start_recovers_on_the_next_pressing_frame`, which
|
||||
drives `Selection::drag` directly with only `Pressing` frames (no
|
||||
`PressStart` ever sent) and asserts the arbiter is no longer idle
|
||||
afterward -- this one fails on the pre-fix code (`is_idle()` stays
|
||||
true forever, matching the real trace).
|
||||
|
||||
**Not completed this pass, and why.** The task asked for
|
||||
`iris-scroll.sh` run three times clean and a re-taken host-GPU
|
||||
`FrameReport` row. Partway through that verification, this
|
||||
checkout's shared emulator (`ai-app-2`, per-checkout per AGENTS.md)
|
||||
turned out to be concurrently in use by another session actively
|
||||
working the P0 phone-benchmark item added to this same file earlier
|
||||
today: `adb shell dumpsys activity processes` showed
|
||||
`com.example.aiapp`/`com.example.aiapp.bench` processes running
|
||||
alongside `dev.iris.android.demo`, window focus was observed to have
|
||||
moved to the Compose app mid-test, and the sandbox server's own log
|
||||
showed a fresh `start` (not `keep`) at 00:55 that wiped this pass's
|
||||
30-message test session and replaced it with the peer's own
|
||||
`bench-check` session -- confirmed by `ui-sandbox.sh api /sessions`
|
||||
returning "no session" for the id this pass had been sending to.
|
||||
Rather than disrupt that session's work (deleting its session,
|
||||
restarting its server, or fighting over emulator focus), this pass
|
||||
stopped chasing a clean aggregate number once the cause was
|
||||
confirmed external. What *is* verified is the fix itself, from
|
||||
direct traces taken before the interference began (above) plus two
|
||||
manual, shorter `ui-trace` swipe sequences (not the full script) that
|
||||
each showed full, healthy per-swipe `render()`/`selection::drag`
|
||||
coverage with the fix in place. The FrameReport row in this box's
|
||||
own table above is therefore **not re-taken this pass** -- a future
|
||||
pass should re-run `iris-scroll.sh` three times and retake it once
|
||||
the emulator is free, per AGENTS.md's "ask before/tell peers" and
|
||||
"coordinate with peer agents" guidance rather than contending for it.
|
||||
Also correctly ruled out, not left ambiguous: a hypothesis raised
|
||||
mid-pass that `iris::widget::list::List::scroll`'s deliberately
|
||||
unclamped anchor (its own module doc, "no overscroll clamping ...
|
||||
leaves a gap rather than rubber-banding back") could itself explain
|
||||
a run of consecutive failed swipes once enough net drift
|
||||
accumulates -- plausible in isolation, but the run where it seemed
|
||||
to reproduce is exactly the run now attributed to the peer
|
||||
session's interference (same timestamps), so it was not
|
||||
independently confirmed and is recorded here as ruled out for now
|
||||
rather than as a second bug.
|
||||
|
||||
## The port, in order (decided 2026-09-05)
|
||||
|
||||
Iris decided iris over Masonry (`DECISIONS.md`). This is the ordered plan
|
||||
@@ -3271,6 +3404,89 @@ device.
|
||||
margin of Compose on p50, p99 and CPU time, no crash, no stutter she
|
||||
can see. Fail stops the port.
|
||||
|
||||
**Compose half: done, 2026-09-05.** `app/androidApp`'s `bench` build
|
||||
type, `app/bench-fixture/` (generator + generated `assets/`),
|
||||
`BenchFixture.kt`/`BenchNetwork.kt` (an in-process fake backend: a
|
||||
`URLStreamHandlerFactory` installed only in `FIXTURE_MODE` answers
|
||||
`https://bench.fixture.invalid:1/...` from an in-memory event log
|
||||
instead of opening a socket, so `TranscriptSource`, `EventStream`,
|
||||
the fold and the paging are the *real* ones, unmodified), and
|
||||
`BenchRun.kt` (the scripted scroll-and-stream, driven against the
|
||||
real `LazyListState`) are all in. "Run benchmark" sits beside "Copy"
|
||||
in the session settings dialog, bench-build only
|
||||
(`SessionSettingsDialog`'s `onRunBenchmark`). `./build-apk.sh bench`
|
||||
works, produces a universal APK (no ABI splits in this project, so
|
||||
arm64-v8a is included alongside the others — confirmed with `aapt2
|
||||
dump badging`), signed with the same release key, own application id
|
||||
`com.example.aiapp.bench`, own label "AI Sessions bench" via a
|
||||
build-type `resValue` overriding `@string/app_name`.
|
||||
|
||||
Checks all clean: `ktfmtFormat`, `compileDebugKotlin`,
|
||||
`compileBenchKotlin`, `lintDebug`, `lintBench` (both "No issues
|
||||
found"), `testDebugUnitTest`. `grep -n "tap [0-9]" app/*.sh` has one
|
||||
hit, pre-existing and unrelated — a comment in `bench-lib.sh`
|
||||
recounting the 2026-09-03 incident that made that grep a rule, not a
|
||||
literal `tap` call.
|
||||
|
||||
**Emulator smoke run, 2026-09-05** (this checkout's AVD,
|
||||
`ui-trace` tap-by-label throughout — `tap 'Session settings'` then
|
||||
`tap 'Run benchmark'`, report read back over `adb logcat`):
|
||||
|
||||
ai-app render report
|
||||
device: sdk_gphone64_x86_64 (Google), Android 16
|
||||
build: release
|
||||
|
||||
transcript:
|
||||
28 events, 26 rows, 58 units loaded
|
||||
viewport 1536px, 2 units visible
|
||||
on screen: the list's own 0px, AssistantMsg 18732px
|
||||
0 tool calls and 0 groups open
|
||||
|
||||
frames:
|
||||
1361 frames over 38.1s at 60Hz (16.7ms budget)
|
||||
late: 1353 (99.4%)
|
||||
total p50 27.8ms p90 37.7ms p99 50.1ms
|
||||
gpu p50 18.9ms p90 28.9ms p99 31.5ms
|
||||
|
||||
where the draw phase went:
|
||||
draw phase 3.12ms per frame, of which:
|
||||
the transcript: 0.33ms (measure 0.18, place 0.14, record 0.00)
|
||||
everything else: 2.79ms (89%)
|
||||
|
||||
bench:
|
||||
scroll: 6 cycles (24 swipes), streamed 400/400 fixture events
|
||||
process CPU time over this run: 23005ms
|
||||
peak RSS: 209348kB
|
||||
battery current: mean 900000µA over 39 samples (min 900000, max 900000)
|
||||
|
||||
Read this as "the harness runs end to end and produces every field
|
||||
P0 asked for," not as a phone number: it is software-rendered
|
||||
emulator rasterisation (this-machine-android's skill — the stock
|
||||
Settings app scrolls worse on the same device), and the battery
|
||||
current is a fixed 900mA on every sample, which is the emulator's
|
||||
mocked charger reporting a constant rather than a real battery —
|
||||
expect that field to read "unavailable" or a real varying number
|
||||
only on Iris's own phone. The ordinary debug build was rebuilt and
|
||||
driven with `./transcript-bench.sh` against `ui-sandbox.sh` alongside
|
||||
this and produced its usual report with no `bench:` section, so nothing
|
||||
changed for it.
|
||||
|
||||
`~/host/bench/compose-bench-arm64.apk` (9.7M) and
|
||||
`~/host/bench/README.md` are written, with a heading left for the
|
||||
iris half. **Known interaction**: the bench build keeps the same
|
||||
`aiapp://enroll` intent filter as the ordinary app (it never uses
|
||||
it), so with both installed, driving enrollment through a raw `am
|
||||
start -d aiapp://...` intent (not the in-app QR scanner, which is
|
||||
the primary path and calls straight into the matched activity) opens
|
||||
Android's "Open with" chooser between the two. Cosmetic — the real
|
||||
enrollment path is unaffected — and left as is rather than pulling
|
||||
the intent-filter out of the bench manifest via source-set merging,
|
||||
which was more diff than the problem was worth.
|
||||
|
||||
**Not done this pass**: the iris half (a separate agent's scope —
|
||||
this session was told not to touch `iris/`), and anything past the
|
||||
emulator — the actual on-phone runs and Iris's pass/fail call.
|
||||
|
||||
- [ ] **P1 — session screen parity.** History paging backward (with the
|
||||
page-boundary healing `client-core` does not have yet, below),
|
||||
`TranscriptSource`-backed cache/server stitching, jump-to-latest,
|
||||
|
||||
Reference in new issue
Block a user