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>
165 lines
6.2 KiB
TOML
165 lines
6.2 KiB
TOML
# The app: everything that is about *this product* rather than about the UI
|
|
# framework it draws with. One package, because splitting it was buying
|
|
# nothing -- see `docs/RUST.md`'s "One app crate" for the account. In short:
|
|
# `client` (the REST/SSE clients, the transcript cache and fold, the
|
|
# highlighter) and `ui` (the screens, in iris widgets) only ever ship
|
|
# together, and the three entry points below are three faces of one binary
|
|
# rather than three programs.
|
|
#
|
|
# `iris/` is the framework and knows nothing about any of this; the
|
|
# dependency runs one way, and a widget or a colour appearing here that is
|
|
# not about a session, a transcript or a setup belongs there instead
|
|
# (AGENTS.md).
|
|
[package]
|
|
name = "ai-app"
|
|
version = "0.1.0"
|
|
edition = "2024"
|
|
|
|
# `cdylib` is the Android face -- both the iris app (`android-project/`,
|
|
# `System.loadLibrary("ai_app")`) and the JNI bridge the Kotlin shell in
|
|
# `app/shellApp` calls (the `shell` feature). `rlib` is what the desktop
|
|
# binary, the examples and `tests/` link against. One package produces one
|
|
# library artifact, so the two Android apps share a `.so` name and pick
|
|
# what goes in it with features rather than with a second crate.
|
|
[lib]
|
|
name = "ai_app"
|
|
crate-type = ["cdylib", "rlib"]
|
|
|
|
[[bin]]
|
|
name = "ai-app-desktop"
|
|
path = "src/bin_desktop.rs"
|
|
required-features = ["screens"]
|
|
|
|
[[example]]
|
|
name = "transcript"
|
|
required-features = ["screens"]
|
|
|
|
[[example]]
|
|
name = "phone"
|
|
required-features = ["fixture"]
|
|
|
|
[dependencies]
|
|
# The event model, shared with `server/` so the two agree by construction.
|
|
# It stays a crate of its own at the repo root for exactly that reason:
|
|
# it is the contract between this app and the backend, not app code.
|
|
event-model = { path = "../event-model" }
|
|
serde = { version = "1", features = ["derive"] }
|
|
# `float_roundtrip` for the same reason `server/` sets it -- AGENTS.md's
|
|
# "Things that have bitten". `raw_value` for the transcript cache.
|
|
serde_json = { version = "1", features = ["float_roundtrip", "raw_value"] }
|
|
ureq = { version = "3", features = ["json"] }
|
|
pulldown-cmark = "0.13.4"
|
|
base64 = "0.23"
|
|
log = { version = "0.4.34", features = ["std"] }
|
|
|
|
# The UI framework. Optional so `--no-default-features --features shell`
|
|
# builds the Kotlin shell's JNI bridge without linking wgpu, parley and
|
|
# the rest of a renderer into an APK that draws with Compose.
|
|
iris = { path = "../iris", optional = true }
|
|
# iris's own tabs demo, kept runnable on Android through this project's
|
|
# Gradle app (the `tabs-screen` feature). The dependency direction is the
|
|
# right way round: the app may reach into the framework's example widget
|
|
# tree, never the reverse.
|
|
tabs-ui = { path = "../iris/tabs-ui", optional = true }
|
|
jni = { version = "0.22", optional = true }
|
|
# `bench` only: `libc` for the process CPU-time and RSS samples, `tokio`
|
|
# for the run's own timer.
|
|
libc = { version = "0.2.189", optional = true }
|
|
tokio = { version = "1.53.1", features = ["rt", "time"], optional = true }
|
|
|
|
[target.'cfg(not(target_os = "android"))'.dependencies]
|
|
winit = "0.30.13"
|
|
|
|
# Pinned to the exact commit RUST.md's E1 measured on this emulator; see
|
|
# `iris/Cargo.toml`'s copy of this pin for what advancing it costs.
|
|
[target.'cfg(target_os = "android")'.dependencies]
|
|
android-view = { git = "https://github.com/rust-mobile/android-view.git", rev = "bec6c62a96cef8239b0fd7fedeef9b184d02e3a1" }
|
|
android_logger = "0.15.1"
|
|
|
|
[features]
|
|
default = ["screens", "fixture"]
|
|
# The iris half: `src/ui` and everything that draws. Off for the Kotlin
|
|
# shell's bridge, which is JNI and `src/client` only.
|
|
screens = ["dep:iris"]
|
|
# `src/ui/fixture.rs` and the harness tests that drive it. Default-on so
|
|
# `cargo test` covers them; `build-apk.sh` passes `--no-default-features`
|
|
# so an APK carries the 1.9 MB fixture only when it asked for `bench`.
|
|
fixture = ["screens"]
|
|
# The two Android widget trees, on the same axis: a build picks one.
|
|
transcript-screen = ["screens"]
|
|
tabs-screen = ["screens", "dep:tabs-ui"]
|
|
# P0's iris half (docs/RUST.md): the fixture screen with a "Run benchmark"
|
|
# control, driving the same scroll loop and streaming phase the Compose
|
|
# bench build type does.
|
|
bench = ["transcript-screen", "fixture", "dep:libc", "dep:tokio"]
|
|
# The JNI bridge `app/shellApp` calls -- notifications, the share target
|
|
# and the Keystore-sealed settings.
|
|
shell = ["dep:jni"]
|
|
# See `iris/Cargo.toml`'s feature of the same name. Never for a phone.
|
|
force-gles = ["screens", "iris/force-gles"]
|
|
|
|
[dev-dependencies]
|
|
tempfile = "3"
|
|
tokio = { version = "1.53.1", features = ["rt", "time"] }
|
|
|
|
# The Android builds, kept off `release`/`dev` so a desktop build is not
|
|
# also optimised for size and unwinding is not also turned off for the
|
|
# tests. `build-apk.sh` passes `--profile android-release`.
|
|
[profile.android-release]
|
|
inherits = "release"
|
|
panic = "abort"
|
|
strip = true
|
|
lto = "fat"
|
|
codegen-units = 1
|
|
# **Speed, not size** (2026-09-09). This was `"s"`, chosen when the
|
|
# question was why the APK was double the Compose one -- but that was
|
|
# measured in bytes only, and `"s"` costs the loop vectorisation and
|
|
# inlining a renderer runs on. Measured with
|
|
# `app-rust/tests/fling_profile.rs`, the same warm fling eight times over:
|
|
# iris's own per-frame work is p90 0.15ms / p99 0.42ms at `"s"` and
|
|
# p90 0.09ms / p99 0.26ms at `3`, so about a third of the CPU half of a
|
|
# scrolling frame was being paid for 1.9 MB of download. The same
|
|
# argument the table in docs/RUST.md gives for refusing `"z"`, applied one
|
|
# level further up.
|
|
opt-level = 3
|
|
|
|
[profile.android-dev]
|
|
inherits = "dev"
|
|
panic = "abort"
|
|
|
|
# Same reasoning as `iris/Cargo.toml`'s copy: full DWARF in every test
|
|
# binary is what made `cargo test` here write tens of gigabytes.
|
|
[profile.dev]
|
|
debug = "line-tables-only"
|
|
|
|
[profile.test]
|
|
debug = "line-tables-only"
|
|
|
|
# The headless harness tests (`iris::harness`, no window and no GPU) all
|
|
# open the bench fixture, so they say so rather than failing to compile
|
|
# when it is off.
|
|
[[test]]
|
|
name = "catch_a_fling"
|
|
required-features = ["fixture"]
|
|
|
|
[[test]]
|
|
name = "fence_fling"
|
|
required-features = ["fixture"]
|
|
|
|
[[test]]
|
|
name = "gesture_cancel"
|
|
required-features = ["fixture"]
|
|
|
|
[[test]]
|
|
name = "input_log_roundtrip"
|
|
required-features = ["fixture"]
|
|
|
|
[[test]]
|
|
name = "phone_screen"
|
|
required-features = ["fixture"]
|
|
|
|
[[test]]
|
|
name = "top_edge"
|
|
required-features = ["fixture"]
|
|
|