diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index 8e4f056..45a2ade 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -18,6 +18,25 @@ marked **DEFERRED** are ones the agent chose not to decide alone. flagged here because it is the first half of something Iris explicitly asked to see before P1. +- **P0's iris half is also built and smoke-tested on the emulator, + 2026-09-05.** A new `bench` Cargo feature on `iris-android-app`, on top + of `transcript-screen`: the same checked-in fixture (`include_str!`, no + asset pipeline needed), the same 24-swipe scroll loop animated through + `List::scroll` and the same 400-event/20s streaming phase through + `fold_event`, "Run benchmark"/"Copy report" as named accessible + controls, and the same three added report fields (process CPU time, + peak RSS, battery current) via direct JNI calls + (`bench_jni.rs::PlatformHandle`) since `android_view` has no + `BatteryManager`/`ClipboardManager` wrapper of its own. One small public + API addition to get there: `AndroidAppState::platform_ready` (`IRIS.md`), + a default-no-op lifecycle hook handing an implementor a `JavaVM` + + `GlobalRef` it can call Java through from any thread. Packaged with a + new `release` build type on `iris-android-app`'s own Gradle project + (there was previously only `debug`), signed with the same key + `app/build-apk.sh` generates. Smoke run and the full report are in + RUST.md's P0 box; not attempted this pass: the real on-phone runs and + Iris's pass/fail call, which is the actual gate. + - **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 diff --git a/docs/IRIS.md b/docs/IRIS.md index 94bac63..a4fd1eb 100644 --- a/docs/IRIS.md +++ b/docs/IRIS.md @@ -8,6 +8,29 @@ capability that moved. Small and trivial changes do not go here. An entry gives the date, what changed, why, and a short before/after where it helps judge the change without the session that made it. Newest first. +## 2026-09-05: `AndroidAppState::platform_ready` (RUST.md's P0 box, iris half) + +Added a second, optional lifecycle method to `iris::android::AndroidAppState` +(`iris/src/android/view.rs`), called once from `new_peer` right after `new`: + +```rust +fn platform_ready(&mut self, rsc: &mut AndroidRsc, vm: JavaVM, view: GlobalRef) {} +``` + +Default does nothing, so every existing implementor (`Client`, +`TranscriptClient`) is unaffected. It exists for a caller that needs to call +into Java itself beyond what a `RequestRedraw` handle already covers -- +P0's bench build (`iris-android-app`'s new `bench` feature, +`bench_client.rs`/`bench_jni.rs`) uses it to hold a `JavaVM` + `GlobalRef` +to the view so its "Copy report" control and once-a-second battery sampler +can call `BatteryManager`/`ClipboardManager` through the view's own +`Context`, from a background tokio task as well as the UI thread. `new` +itself was not extended with these two parameters: most implementors need +nothing here, and `new`'s job is building the widget tree, not holding a +platform handle. `vm`/`view` are independent handles from the ones +`new_peer` keeps for its own `RequestRedraw` (a fresh `get_java_vm`/ +`new_global_ref` each), so storing them has no effect on that mechanism. + ## 2026-09-05 (later the same day): `iris_core::FrameReport` (RUST.md's I5 box) New public type, `iris_core::FrameReport` (re-exported from `iris_core`'s diff --git a/docs/IRIS_TODO.md b/docs/IRIS_TODO.md index d4edcc6..13222be 100644 --- a/docs/IRIS_TODO.md +++ b/docs/IRIS_TODO.md @@ -82,6 +82,18 @@ order and what "done" looks like. Tick and date them in place. were exactly the same root cause measured two different ways. Frame 2 now reports 0 (see the numbers above); not a separate fix. +- [ ] **A read-only text display has no widget of its own — P0's bench + report area is a `TextEdit` standing in for one (2026-09-05).** The only + way to get selectable text on screen today is `.editable(...)` plus + `.attr::(())` (`Selectable` is only implemented for + `TextEdit`, `iris/src/attr.rs`), which also makes the field focusable — + tapping the bench report opens the soft keyboard over text nothing lets + you type into. Harmless for a bench-only debug screen (not fixed this + pass), but a real "selectable, not editable" text primitive would + remove the keyboard side effect and is worth having before another + screen wants the same thing (P1's own transcript rows already read + their content from a `TextEdit` for the same reason). + ## Build - [x] **Benchmarks**, not unit tests, run on demand (2026-09-05; a diff --git a/docs/RUST.md b/docs/RUST.md index 089a19a..9839e7d 100644 --- a/docs/RUST.md +++ b/docs/RUST.md @@ -3487,6 +3487,180 @@ device. this session was told not to touch `iris/`), and anything past the emulator — the actual on-phone runs and Iris's pass/fail call. + **iris half: done, 2026-09-05.** A `bench` Cargo feature on + `iris-android-app`, built on top of `transcript-screen` + (`bench = ["transcript-screen", "dep:libc", "dep:tokio"]`, + `iris/android-app/Cargo.toml`), gives `lib.rs`'s `ActiveClient` + priority a third `AndroidAppState` (`bench_client::BenchClient`) + over `TranscriptClient` when both features are listed together -- + matching the exact build command below, which lists both. + + **Fixture.** `include_str!("../../../app/bench-fixture/assets/ + transcript.jsonl")` (1,915,760 bytes) at compile time -- no asset + pipeline needed the way the Compose half's Gradle source set does. + `bench_client::parse_fixture` splits the same way `BenchFixture.kt` + does: the first 3,200 non-blank lines parsed as `serde_json::Value`s + and folded once through `client_core::transcript_fold::fold_page` + (the real fold a `/transcript` page goes through), the rest parsed + as `event_model::SeqEvent`s and held back as the streaming tail. + `build.rs` (transcript-screen's own) now exits early under `bench` + before requiring a live server's host/port/token/CA -- `BenchClient` + never calls `build_transport()`, so that requirement made no sense + for a build that talks to nothing. + + **"Run benchmark" (`.label("Run benchmark")`) and "Copy report" + (`.label("Copy report")`)** sit in a fixed bar above the transcript; + a selectable `TextEdit` (`.attr::(())`, the same + attribute the composer field uses) below it shows the report text. + Pressing "Run benchmark" resets `FrameReport`, then drives + `List::scroll` in ~60Hz steps (`ANIM_STEP_MS = 16`) to animate each + 900px/200ms swipe rather than jumping it -- iris's `List` has no + built-in tween the way `animateScrollBy(tween(...))` gives Compose, + so this is the one place the two backends' bench code has to differ + in shape rather than only in numbers -- through the same + `rsc.tasks.redraw_handle()` + manual `request_redraw()` per step + `transcript_client.rs` already established (a `Tasks::spawn`d + future's *automatic* redraw fires once, after the whole future + completes, which would show nothing moving until the run ends). + After the scroll loop, `List::jump_to_end()` pins to the newest + content (matching `stream-bench.sh`'s "Jump to latest" tap), then + 400 fixture events replay at 20/s through `fold_event` -- the same + fold path a live SSE frame takes in `transcript_client.rs`'s own + `apply_event` -- each one triggering `rebuild_transcript`'s full + `transcript_ui::build_tree` rebuild, same tradeoff as + `TranscriptClient`/`desktop-app`. A battery sampler runs + concurrently on its own `tokio::spawn`d task (not through + `ctx.update`, since a JNI battery read needs no widget-tree access), + attaching whichever thread it runs on via a stored `JavaVM` -- + `AndroidAppState::platform_ready` (new, `IRIS.md`) is what hands + `bench_client.rs` that `JavaVM` + a `GlobalRef` to the view, since + neither was reachable from `AndroidAppState::new` before this box. + + **Report fields.** `FrameStats`'s existing `Display` (frames, janky + %, p50/p90/p99, worst, and I5's own `cpu_p50`/`gpu_wait_p50` CPU/GPU + split) plus a `bench:`-shaped tail this box added: process CPU time + via `libc::getrusage(RUSAGE_SELF)` (user+system time; chosen over + parsing `/proc/self/stat` by hand to avoid assuming `USER_HZ`), peak + RSS from `/proc/self/status`'s `VmHWM` (same source `BenchRun.kt` + reads), and battery current sampled once a second via + `BatteryManager.getIntProperty(BATTERY_PROPERTY_CURRENT_NOW)` + through direct JNI calls (`bench_jni.rs`'s `PlatformHandle` -- + `android_view::context`'s own `Context`/`Resources` wrappers have no + `getSystemService`, so this calls it directly rather than growing + that crate's wrapper for two one-off calls). `0`/`Integer.MIN_VALUE` + read as "unavailable" rather than folded into the average, matching + `BatterySampler`'s own rule and UI_RULES.md's "never present an + inferred value as a measured one." The report is logged under the + existing `iris-android-app` logcat tag on a line starting `iris + bench report:` (grep-able the same way `transcript_client.rs`'s + "Frame report" control already is), shown in the on-screen + `TextEdit`, and copied to the system clipboard by "Copy report" + through `ClipboardManager.setPrimaryClip` (`bench_jni.rs`, same + `PlatformHandle`). + + **Build commands, all clean this pass:** + - `cargo fmt --all -- --check` (iris workspace) and + `cd iris/android-app && cargo fmt --all -- --check`: clean. + - `cargo clippy --workspace --all-targets` (iris workspace): clean + (only the pre-existing `wgpu`/`winit`/`naga` future-incompat + notice). + - `cargo test --workspace` (iris workspace): 39 + 8 + 10 = the same + pre-existing counts, all passing, unaffected by this box (it + touched no logic under test there beyond `AndroidAppState`'s new + default no-op method). + - `cargo ndk -t x86_64 -P 26 clippy --features "transcript-screen + force-gles bench" --lib -- -D warnings` (`iris/android-app`): + clean. + - `cargo ndk -t arm64-v8a -P 26 -o app/src/main/jniLibs/ build + --release --features "transcript-screen force-gles bench"`: + clean, `arm64-v8a/libmain.so` produced. The pre-existing "unused + dependency `tabs-ui`" Cargo advisory also appears on a plain + `--features transcript-screen` build with no `bench` (confirmed + by building that combination alone with fake env vars) -- not + something this box introduced, and not a clippy/rustc warning + (AGENTS.md's "keep the build clean" gate is `cargo clippy`, which + stays silent on it). + + **Packaging.** No `cargo xtask apk` exists for `iris/android-app` + yet (I2's own Gradle project is the only pipeline), so this reused + that split rather than inventing one: `cargo ndk --release` above + builds the cdylib straight into `app/src/main/jniLibs/`, then a new + `release` build type in `app/build.gradle` (there was previously + only `debug`) packages and signs it -- + `AI_APP_KEYSTORE=~/.config/ai-app/release.jks` + + `AI_APP_KEYSTORE_PASSWORD` (the same key `app/build-apk.sh` + generates for the Compose app) via `gradle :app:assembleRelease`, + with `applicationIdSuffix ".bench"` so it installs beside the plain + tabs demo rather than replacing it. `aapt2 dump badging` on the + result: `package: name='dev.iris.android.demo.bench'`, one native + library, `lib/arm64-v8a/libmain.so`. `apksigner verify + --print-certs` shows the same `CN=ai-app` certificate + `compose-bench-arm64.apk` is signed with. + + **Emulator smoke run, 2026-09-05.** This checkout's own AVD + (`ai-app-2`) was in use by the session recording I5's clean-scroll + comparison in this same file (its Compose app was in the + foreground, confirmed via `dumpsys window`/`dumpsys activity + processes` before touching anything) -- rather than contend for it + (AGENTS.md's "coordinate with peer agents"), a second, + differently-named AVD was created (`AVD_NAME=ai-app-2-bench emu + up`, `pixel_10`/`android-36`/`google_apis`/`x86_64`, cold boot, host + GPU, no `EMU_GPU=software`), with 12GB of the VM's memory still + available after both were up (this-machine-android's "two are + comfortable" guidance). Installed via `adb -s emulator-5556 install + -r`, launched, driven by `ui-trace record -s emulator-5556 --do + "tap 'Run benchmark'"` (the control resolved by its accessibility + label, per AGENTS.md's "no coordinate" rule), then read back over + `adb logcat`: + + iris bench report + frames=372 janky%=56.99 p50=19.5ms p90=219.5ms p99=284.5ms worst=369.3ms (measures redraw-start to after present() is called, not GPU/compositor completion) cpu_p50=0.4ms gpu_wait_p50=13.9ms (redraw-start-to-submit vs. submit-to-after-present) + scroll: 6 cycles (24 swipes), streamed 400/400 fixture events + process CPU time over this run: 24665ms + peak RSS: 224600kB + battery current: mean 900000µA over 21 samples (min 900000, max 900000) + + "Copy report" was pressed immediately after and logged `iris bench + report: copied to clipboard` (`ClipboardManager.setPrimaryClip` + succeeded). No crash (`adb logcat`'s `FATAL`/`AndroidRuntime` lines + checked -- only `ui-trace`'s own runtime, unrelated), process alive + throughout (`dumpsys activity processes`), 400/400 stream events + confirmed sent. + + Read this the same way the Compose half's own box already asks to + read its number: this is software-rasterised (well, GLES-over-virgl + under `force-gles`, per I5's "Where iris's frame time goes") + emulator output, "the harness runs end to end and produces every + field P0 asked for," not a phone number -- and the battery current + is again the emulator's fixed 900000µA mocked charger reporting a + constant, exactly what the Compose box's own run found, not a real + battery answering. `cpu_p50=0.4ms` (iris's own per-frame CPU work) + against a much larger `gpu_wait_p50`/`p50` again matches I5's "Where + iris's frame time goes" finding under real GPU rendering (`-gpu + host`, `force-gles`) -- the frame-time budget here is dominated by + the driver/compositor wait, not by iris's layout or primitive + building, though this run's `janky%`/`p90`/`p99` are considerably + worse than that earlier isolated pass, most likely the cost of this + AVD's very first cold boot plus running two emulators on this VM at + once (a fair comparison against Compose would need both apps run + back-to-back on the same freshly-booted device, not attempted this + pass since the second AVD was torn down immediately after per + AGENTS.md's "stop yours when you are done with it"). + + Copied to `~/host/bench/iris-bench-arm64.apk` (15,445,468 bytes) and + `~/host/bench/README.md`'s "iris" section filled in (install, open, + tap "Run benchmark", read the report from the on-screen text or + logcat, tap "Copy report", paste back). + + **Not done this pass**: the actual on-phone runs and Iris's + pass/fail call between the two reports (P0's own pass condition) -- + that needs Iris's phone, which this session has no access to. + `iris/src/android/view.rs` was touched (`AndroidAppState:: + platform_ready`, `new_peer`'s wiring) -- confirmed to not be one of + the three files the concurrent `device_limits()` work on this + branch was using (`iris/core/src/render/mod.rs`, + `iris/src/android/render.rs`, `iris/src/default/render.rs`). + - [ ] **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,