iris android-app: strip+LTO+cgu1+opt-level=s halve libmain.so, no feature trim needed

Baseline had panic=abort only. Measured each setting in order (docs/RUST.md's
new "APK size (2026-09-07)" subsection has the full table and crate
breakdown): strip=true, lto="fat", codegen-units=1, opt-level="s" take
libmain.so from 18,546,488 to 11,193,608 bytes (-39.7%) and the release APK
from 20,678,956 to 13,326,076 bytes (-35.5%), arm64-v8a. opt-level="z" was
measured (another ~800KB) but not adopted without a frame-time check.

Investigated naga/wgpu backend features and tabs-ui/tabs-screen as trim
candidates; both are already fully eliminated by the linker on Android
(0 symbols in `llvm-nm` on the baseline .so), so no Cargo feature change
would shrink the binary -- left as documented findings rather than a diff.

Embedded Noto Sans fonts (3.6 MB) and the wgpu/naga/font-shaping stack
account for most of what remains vs. Compose, which borrows the platform's
own renderer and fonts for free; recorded honestly in the doc rather than
trimmed, since subsetting fonts or dropping a backend would change what
iris can render.
This commit is contained in:
iris committed 2026-09-07 15:46:40 -04:00
1 parent 4274b8b8d0
commit 42af780639
2 files changed
+184

No files matched your search

+172
View File
@@ -43,6 +43,178 @@ gated on her verdict**, so this pass works the P0 defects and the pure
prerequisites in this order. Each item is ticked here by the agent that prerequisites in this order. Each item is ticked here by the agent that
closes it. closes it.
### APK size (2026-09-07)
Iris's question: the iris bench APK is about double the Compose bench APK
(20.6 MB vs 10.1 MB, both release). Measured before this pass: 18,088 KiB of
`lib/arm64-v8a/libmain.so`, stored uncompressed (`extractNativeLibs=false`),
plus 2 MB of dex; the Compose APK's dex is 25 MB raw, compressed to 9 MB in
the APK. `iris/android-app/Cargo.toml`'s `[profile.release]` set only
`panic = "abort"` -- no `lto`, no `codegen-units`, no `strip`, default
`opt-level`. All numbers below are `arm64-v8a` release, built with
`./build-apk.sh release` (this checkout, `nice -n 10`, no `CARGO_TARGET_DIR`
override, reusing the incremental `target/`), and are the raw file sizes
(`ls -la`), not what `du` would round to.
| profile.release | APK bytes | `libmain.so` bytes | delta vs previous |
|---|---|---|---|
| `panic="abort"` only (baseline) | 20,678,956 | 18,546,488 | -- |
| + `strip = true` | 16,435,156 | 14,302,688 | -4,243,800 |
| + `lto = "fat"` | 15,751,212 | 13,618,744 | -683,944 |
| + `codegen-units = 1` | 15,185,204 | 13,052,736 | -566,008 |
| + `opt-level = "s"` | 13,326,076 | 11,193,608 | -1,859,128 |
| + `opt-level = "z"` (not adopted, see below) | 12,507,276 | 10,374,808 | -818,800 |
Adopted: `strip = true`, `lto = "fat"`, `codegen-units = 1`, `opt-level = "s"`.
Baseline to final: `libmain.so` **18,546,488 -> 11,193,608 bytes (-39.7%)**,
APK **20,678,956 -> 13,326,076 bytes (-35.5%)**.
**`opt-level = "z"` was measured but not adopted.** It is smaller still --
another 818,800 bytes off `libmain.so` (7.9 MiB total vs `s`'s 8.7 MiB) --
but `z` trims more aggressively than `s` in ways that can cost frame time
(fewer inlines, more size-motivated codegen choices, per rustc's own docs),
and this pass did not have an iris-side frame-time benchmark run against
it (the app's own `run-bench.sh`/render report was not exercised here,
per this task's scope, and this checkout has no emulator currently up).
Trading an unmeasured runtime cost for ~700 KB more off the download is not
a call to make blind, so `s` is what shipped, and `z` is left as something
to try only alongside a `transcript-bench.sh`/`stream-bench.sh`-equivalent
run for iris to confirm it does not regress.
Not stripped (baseline) had a live `.symtab` (`llvm-readelf -S`): section
25, `SYMTAB`, 0x17ed40 bytes (~1.49 MiB) covering 65,223 raw symbols. `strip
= true` removes it at build time -- notably, `stripReleaseDebugSymbols`
(AGP's own strip task) had already logged "Unable to strip the following
libraries, packaging them as they are: libmain.so" on the baseline, so
Cargo's own strip is also the fix for that.
Baseline section sizes (`llvm-readelf -S`, before any profile change):
| section | bytes |
|---|---|
| `.text` | 6,463,032 |
| `.rodata` | 6,548,192 |
| `.eh_frame` | 721,872 |
| `.data.rel.ro` | 441,328 |
| `.gcc_except_table` | 13,652 |
| `.symtab` | 1,563,456 |
No `bloaty` on this machine (`which bloaty` empty); used
`llvm-nm -S --size-sort -C` on the baseline (unstripped) `.so`, summed by
the symbol's leading crate/module name. 7.02 MB of the 18.5 MB `.so` carries
a name at all (the rest is `.rodata` blobs -- embedded data, padding,
relocations -- that never get a symbol). Top 8 by that accounting:
| crate | bytes (named symbols only) |
|---|---|
| `naga` | 1,100,099 |
| `core` (std) | 698,862 |
| `wgpu_core` | 599,720 |
| `harfrust` | 372,694 |
| `alloc` | 347,324 |
| `read_fonts` | 326,824 |
| `wgpu_hal` | 299,648 |
| `skrifa` | 291,332 |
Also notable further down: `hashbrown` 244,712, `jni` 199,660, `std`
199,126, `serde` 168,256, `zeno` 157,320, `pulldown_cmark` 116,568,
`iris_core` 95,112, `parley` 78,816, `iris` 76,616, `swash` 72,940,
`serde_json` 72,312, `fontique` 45,464.
**The other ~11.5 MB of `.rodata`/unnamed data is mostly the embedded
fonts**: `iris/core/src/primitive/text.rs` `include_bytes!`s six Noto Sans
TTFs (`iris/core/assets/fonts/`) -- Regular/Bold/Italic/BoldItalic for Noto
Sans plus Regular/Bold for Noto Sans Mono -- totalling **3.6 MB** of raw
font data (`du -ch iris/core/assets/fonts/*.ttf`). That is real render
data, not something to strip: unlike the Compose app's Nerd Fonts icon
subset (`app/build-icon-font.sh`, which subsets because the app only ever
draws ~100 fixed glyphs), iris's Noto Sans embedding backs arbitrary text
in a chat transcript, so a subset would have to be a Unicode-coverage
subset (Latin/Latin-Extended/common punctuation, dropping CJK/Cyrillic/etc)
rather than a fixed-codepoint one -- a real behaviour change (text in a
language outside the subset would fall back to tofu or a missing glyph) and
out of scope for a size-only pass. Left as a follow-up, flagged for Iris:
subsetting would plausibly save 1-2 MB but changes what scripts render
correctly, which is a product decision.
**naga/wgpu backend features: investigated, not trimmed, because the
trim would not change the binary.** `iris/core/Cargo.toml` and
`iris/Cargo.toml` depend on `wgpu = "28.0.0"` with default features, which
via `wgpu`'s own defaults (`dx12`, `metal`, `gles`, `vulkan`, `wgsl`,
`webgpu`) forward `naga/hlsl-out`, `naga/msl-out`, `naga/glsl-out`,
`naga/spv-out`, `naga/wgsl-in`, `naga/wgsl-out` -- Cargo feature
unification is not per-target, so all of those are nominally "on" for the
Android build too, not just the ones Android actually uses (`glsl-out` for
GLES, `spv-out` for Vulkan). But `wgpu-hal`'s own `build.rs`
(`cfg_aliases!`) gates the *modules* themselves on the real target:
`dx12: target_os = "windows" AND feature = "dx12"`, `metal: target_vendor =
"apple" AND feature = "metal"` (`wgpu-hal-28.0.0/build.rs`,
`wgpu-hal-28.0.0/src/lib.rs`'s `#[cfg(dx12)] pub mod dx12;` etc). So on
`aarch64-linux-android` the dx12/metal modules never compile, nothing calls
into `naga::back::hlsl` or `naga::back::msl`, and the linker's normal
dead-code elimination already drops them: `grep -c
"naga::back::hlsl\|naga::back::msl\|naga::front::spv\|naga::front::glsl"
/tmp/nm_size.txt` on the **baseline** (no LTO yet) `.so` returns **0** --
none of that code reached the linked binary in the first place. `regex`
(pulled in transitively by `env_filter`, which `android_logger` uses for
`RUST_LOG`-style filtering) is in the same position: present in
`Cargo.lock` but only a handful of small generic-drop symbols in `nm`, not
a real contributor. Neither is worth a Cargo-level feature trim (which
would also need a per-target dependency table to avoid stripping dx12/metal
off the desktop build, adding real complexity for a change that measures
as zero). No emulator use was needed for this finding since no feature
flag changed; the earlier per-step size measurements (strip/LTO/cgu/opt-level)
were likewise not re-verified on the emulator, since this task's brief
scoped emulator use to the naga-trim step specifically, and that step's
answer was "don't."
**`tabs-ui`/`tabs-screen`: also investigated, also already dead.**
`build-apk.sh`'s default features are `"transcript-screen bench"`, passed
without `--no-default-features`, so the crate's own `default =
["tabs-screen"]` (`iris/android-app/Cargo.toml`) is *also* on for every
build this script produces, including the bench APK. `src/lib.rs`'s doc
comment already says the three screens are mutually exclusive at runtime
(`ActiveClient` gives `bench` priority over `transcript-screen`, which
takes priority over the default `tabs-screen`), and checking the actual
`#[cfg(...)]` gates confirms it is mutually exclusive at *compile* time
too: the `Client` struct and its one call to `tabs_ui::build` are behind
`#[cfg(not(feature = "transcript-screen"))]`, which is false whenever
`transcript-screen` is on, so that code does not even get generated, let
alone linked. `llvm-nm -C` on both the baseline and the final `.so` confirm
it: `grep -ci "tabs_ui\|sungals"` is **0** in both. So there is nothing to
trim here either -- `tabs-ui` and its `sungals.png` (8.9 KB) never reach
the linked binary in a `transcript-screen`/`bench` build, regardless of the
feature being nominally "on" in `Cargo.toml`.
**Comparison Iris asked for, honestly**: most of the remaining ~13.3 MB vs
Compose's 10.1 MB is not a build-settings gap, it is what each app links.
Compose's APK carries ~9 MB of *compressed* dex and links Android's own
platform renderer, text shaper (HarfBuzz/Minikin) and font files from the
system image at zero cost to the APK -- none of that is bytes Compose ships.
Iris ships its own copy of all of that: `wgpu`+`naga`+`wgpu_hal` (a
software/hardware-portable GPU backend and shader cross-compiler, roughly
2 MB of named symbols alone), `harfrust`+`read_fonts`+`skrifa`+`swash`+
`parley`+`fontique` (a full third-party font-loading/shaping/rasterizing
pipeline, another ~1.2 MB of named symbols), and 3.6 MB of embedded font
data because it cannot borrow the platform's fonts the way Compose does.
Build settings (this pass) closed real ground -- 39.7% off `libmain.so` --
but did not remove any of those linked systems, because removing them would
mean iris stops being a self-contained native renderer, which is the whole
point of the port (`AGENTS.md`'s "no Dioxus and nothing that draws through
a WebView", `no-dioxus-or-webview-ui-true-native-only` memory). Install size
(what `dumpsys package`/`du` on the installed `lib/arm64-v8a/` directory
would show) was not separately measured this pass: with
`extractNativeLibs=false` the `.so` is mapped directly out of the APK
rather than copied onto disk a second time, so install size tracks the APK
size closely for the native library and is not a second, larger number the
way it would be under the old `extractNativeLibs=true` default -- checking
this precisely needs the emulator, which this task scoped to the naga-trim
verification only.
Committed: `iris/android-app/Cargo.toml`'s `[profile.release]` now reads
`panic = "abort"`, `strip = true`, `lto = "fat"`, `codegen-units = 1`,
`opt-level = "s"`, with a comment naming the measured savings.
### Queue, 2026-09-07 (orchestrator) ### Queue, 2026-09-07 (orchestrator)
In order; two builders at a time. Each is ticked here by the agent that In order; two builders at a time. Each is ticked here by the agent that
+12
View File
@@ -73,6 +73,18 @@ bench = ["transcript-screen", "dep:transcript-fixture", "dep:libc", "dep:tokio"]
[profile.release] [profile.release]
panic = "abort" panic = "abort"
# Measured 2026-09-07 (docs/RUST.md's "APK size" subsection): together these
# take libmain.so from 18,546,488 to 11,193,608 bytes (-39.7%) and the APK
# from 20,678,956 to 13,326,076 bytes (-35.5%), arm64-v8a release. `strip`
# also works around AGP's own stripReleaseDebugSymbols failing silently on
# this .so ("packaging them as they are"). `opt-level = "s"` over `"z"`:
# `z` measured another ~800 KB smaller but was not checked against iris's
# own frame-time bench, so it is not worth the unmeasured risk -- see the
# doc for the number and the follow-up this leaves.
strip = true
lto = "fat"
codegen-units = 1
opt-level = "s"
[profile.dev] [profile.dev]
panic = "abort" panic = "abort"