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:
1 parent
4274b8b8d0
commit
42af780639
2 files changed
+184
No files matched your search
+172
@@ -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
|
||||
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)
|
||||
|
||||
In order; two builders at a time. Each is ticked here by the agent that
|
||||
|
||||
Reference in new issue
Block a user