From 42af780639c650f0ba7a67b1e3b6a598968293a8 Mon Sep 17 00:00:00 2001 From: iris <2+iris@noreply.localhost> Date: Mon, 7 Sep 2026 15:46:40 -0400 Subject: [PATCH] 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. --- docs/RUST.md | 172 ++++++++++++++++++++++++++++++++++++ iris/android-app/Cargo.toml | 12 +++ 2 files changed, 184 insertions(+) diff --git a/docs/RUST.md b/docs/RUST.md index d642332..66ce214 100644 --- a/docs/RUST.md +++ b/docs/RUST.md @@ -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 diff --git a/iris/android-app/Cargo.toml b/iris/android-app/Cargo.toml index 11f1664..fb2db6c 100644 --- a/iris/android-app/Cargo.toml +++ b/iris/android-app/Cargo.toml @@ -73,6 +73,18 @@ bench = ["transcript-screen", "dep:transcript-fixture", "dep:libc", "dep:tokio"] [profile.release] 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] panic = "abort"