8.9 KiB
Rust app and Iris
The production app is one Rust crate under app/, with shared Android and
desktop UI drawn by the in-tree iris framework. This document keeps the
open work and measured constraints that are expensive to rediscover.
Boundaries
app/src/clientcontains transport, config, caching, folding, highlighting, and other platform/UI-independent logic. It must not depend on Iris.app/src/uicontains product screens and reusable product widgets.app/src/androidandapp/src/desktopcontain only platform integration. Platform services are exposed to shared UI through small traits.iris/is only a UI framework. Session, transcript, server, setup, and product styling concepts never belong there.iris/src/rscgroups resources carried by or installed throughStdRsc: attributes, events, overlays, pointer sensing/input, tasks, and per-widget state. It is the standard bundle used by Iris's hosts, not a requirement; shared widgets depend on its narrow resource traits so a custom bundle can implement only what it needs.widget/is reserved for concrete retained widgets. Public paths such asiris::senseremain aliases for readability.- Phone and desktop layouts may differ. Their component implementations, colors, spacing, text styles, gestures, folding, and network behavior do not.
The application crate has an rlib for tests and desktop and a cdylib for
Android. Features select optional surfaces: screens links Iris, fixture
embeds the synthetic transcript, and bench adds the benchmark runner. An
ordinary APK uses screens; only a benchmark build adds bench.
Current state
The transcript screen runs in the headless harness, a desktop window, the
Android emulator, and a real phone. The phone performance gate passed; the
preserved reports are under docs/bench/.
Open session-screen work:
- History paging, viewport-sized prefetch, distinct unknown/empty/error states, and jump-to-latest.
- Images, attachments, session settings, usage display, and dialogs.
- Re-verification of IME/inset behavior on the real phone.
Later product work:
- Root navigation and the sessions/import/models/setups surfaces.
- File explorer and editor, including conflict handling.
- Settings, notification permission, sharing, and notification service.
- Desktop layouts for those surfaces.
Client route coverage and gaps live in docs/CLIENT_CORE.md; framework work
lives in iris/TODO.
Test layers
Use the cheapest layer that can answer the question.
1. In-process harness
iris::harness builds the real widget tree with no window, compositor, or
GPU. Its clock is controlled by the test. TouchScript replays the same
historical samples Android supplies. Recordings live under app/touch/.
cd app && cargo test
The suite covers fling velocity and duration, catching a moving list, cancellation, taps, long-press selection, list bounds, and composer insets. It cannot answer pixel, atlas, GPU upload, surface lifecycle, JNI, clipboard, or real-inset questions.
iris/tests/mask_sdf.rs is the narrow exception: it uses a headless GPU
compute pass to prove the shader and CPU hit-test arithmetic agree.
2. Phone-shaped desktop window
cd iris
./scripts/run-headless.sh phone --phone --dir ../app --shot /tmp/phone.png
./scripts/run-headless.sh phone --phone --dir ../app \
--replay ../app/touch/flick-120hz.touch --shot /tmp/replay.png
This layer is for visual output and desktop input integration. Debug frame times on the VM's GPU are not phone measurements.
3. Android
cd app
./build-apk.sh debug --abi x86_64
Android is required for JNI, lifecycle, IME, insets, clipboard, accessibility,
and renderer fallback. Read the this-machine-android skill first. Use a
release arm64 build for real-phone measurements.
The emulator is deliberately a GLES rig: Vulkan is SwiftShader, while GLES
is host-accelerated through virgl. Do not compile with force-gles; Iris's
runtime fallback must make the same decision the shipped binary makes. Read
the iris renderer: log before interpreting any result.
Performance rigs and established results
scripts/rigs/ui-profile/tests/frame_profile.rs measures the CPU half of a
frame. Run it only in release mode:
cd scripts/rigs/ui-profile
cargo test --release --test frame_profile -- --ignored --nocapture
A fling is not CPU-bound after the first pass over content: only roughly one frame in six performs layout, and later passes measured p99 0.26 ms. Initial multi-millisecond spikes are text shaping for newly visited transcript rows.
A streamed event originally measured 3.86 ms in the host rig and 9.5 ms on the phone. Folding was 0.35 ms and applying the widget diff 0.41 ms; shaping dominated because the old fixture grew one 14,888-character markdown block. Real replies measured a largest block of 1,580 characters over 7,706 blocks. The fixture now inserts blank lines and retains the run-on block only as a one-shot stress case.
arena_churn.rs measures GPU-array upload by reporting the byte-diff floor,
the bytes Iris uploads, and the former whole-array cost. The rig established:
- Whole-array writes cost 758 MB over one fling and 1.2 GB over 401 streamed deltas.
ArrBufmust retain allocation across length changes.- Redraw must reuse primitive slots within the frame rather than freeing and appending them.
- Dirty ranges must track changed entries and cancel provisional writes that are restored before upload.
Painter::set_child_offsetand stableLazySpanrow boxes reduce pinned growth to a 1.1% instance upload, matching its 1.1% floor. Median instance upload fell from 176,496 bytes to 1,488 bytes.
Do not optimize folding again without new evidence. The measurements ruled it out as the streamed-frame bottleneck.
Text and fonts
Body and monospace text come from platform fonts. Icons use the bundled Nerd Font subset because their codepoints are a small closed set no platform must provide. Android font enumeration previously rendered some bold spans as blank gaps, so every real-phone build must visually verify bold text and the triangle disclosure glyphs; desktop and emulator font sets are not evidence for the phone.
Markdown is split into blocks once and retained. A delta reshapes only the block it changes. Tables wrap cells and horizontally scroll only after their minimum useful column width. Links share a text widget and hit regions; they must not become one layout node per link.
Scrolling and retained rendering
docs/SCROLL.md defines scrolling. One ScrollController owns position,
gesture, fling, and pin state. ScrollArea and LazySpan each own one through
Scrollable.
The transcript is bottom-anchored. Appending while pinned keeps the newest edge fixed; loading history above the anchor must not move visible content. Expanding a row holds the edge nearest the tap. Retained child offsets move a subtree without rewriting every primitive.
A page is a number of events, not a number of visible rows: many deltas fold into one reply and many tool calls can group into one row. Prefetch therefore uses measured viewports, never a fixed row count.
Android host
The Java activity owns the Android lifecycle, the View, enrollment intent,
and insets. iris::android owns surface lifecycle, touch history, generic
motion, hover, mouse buttons, wheel input, IME plumbing, and accessibility.
The product client consumes the same Iris pointer state as desktop.
Static inset dispatch supplies settled values; WindowInsetsAnimation
supplies intermediate IME heights and re-reads settled insets at animation
end. Visibility and height are separate signals during animation. targetSdk
35+ is required for reliable IME overlap under enforced edge-to-edge.
The application id is com.example.aiapp, preserving the retired app's
identity. The Java package remains dev.iris.android.demo, an internal JNI
name. Explicit benchmark builds add .bench to the application id.
Build constraints
app,iris, andscripts/rigs/ui-profileuse synchronized rolling nightly toolchains.- Android uses the
android-release/android-devprofiles so phone settings do not affect desktop builds. app/build-apk.shremoves prior ABI native outputs before packaging; Gradle otherwise may reuse a stale merged library from another ABI.- Release builds are signed by the machine-local
~/.config/ai-app/release.jks. No key or enrollment secret belongs in the repository. iris/scripts/run-headless.sh --dir DIRselects the workspace containing the example and defaults toiris/.
Session-screen pass condition
Use app/ui-sandbox.sh with artificial delay, a large transcript, and slow
spawn. Exercise unknown, empty, error, and too-long states. Drive controls by
accessibility label. Verify paging seams, jump-to-latest, selection, tool-card
state, message steering, and IME closure. Then re-run the frame report on the
real phone and compare only with the same Iris reporter—not historical Compose
timings, whose clocks and accounting differ.