perf(gui): share repeated media reads within a frame
History and page frames can name the same content-addressed attachment many times. The native reader previously called std::fs::read and allocated a separate buffer for every reference. Share successful reads within each history/message/chat frame, retaining existing full/preview adoption and retrying failed reads. No map survives publication or an account switch. Production change is confined to crates/gui/src/session/frames.rs, separate from #275 and #277. No manifests, lockfiles or productive dependencies change.
Synthetic filesystem reproduction, base e6e3020 versus 6253f38, same rustc 1.99.1 runner and -O build. Four 12-row workloads, nine alternating process pairs per timing/heap mode, excluded warmups, repeated independently. For twelve references to one 1 MiB file, bytes read and retained payload fall from 12 MiB to 1 MiB, additional peak requested live heap from 12,582,951 to 1,048,765 B, and median fill time from 3.690 to 0.303 ms. Three distinct repeated files reduce reads/retained payload to 3 MiB and median time from 3.784 to 0.921 ms. Results repeated at 3.597 to 0.282 ms and 3.416 to 0.943 ms.
The all-unique cases save no payload bytes. Twelve tiny unique files regress from 18.3 to 28.6 us, adding 15 allocation calls and 556 peak requested live bytes. The table adds key/map/refcount overhead for one frame. This tradeoff and raw samples are recorded in docs/frame-media-performance.md and docs/measurements/frame-media*.csv.
The benchmark extracts production fill helper bodies with minimal domain/trait shims and real std::fs::read. It excludes actual domain adoption, IPC, GUI rendering, decoder, connected daemon, scheduling and account startup. Heap instrumentation runs separately; its allocator wrapper remains installed but disabled in timing mode. Process RSS/CPU include startup/assertions/teardown and are reported separately with a C wait4 launcher. No application-wide CPU/RSS or real-account startup gain is claimed. No CodSpeed, eBPF, root, account data or credentials are used, as authorized.
This does not defer distinct attachments until conversation selection. The real profile's 11.3 MB transient peak established the eager-read path but not duplicate frequency. Removing the eager path safely still requires selection-aware hydration preserving Status, previews and offline reading. This PR cannot claim elimination of that whole peak.
Validation: 589 native GUI all-feature tests passed, 7 measurement tests ignored; GUI all-target/all-feature Clippy with -D warnings, workspace formatting and diff checks passed. Added tests cover pointer sharing, preview MIME/flags, no cache reuse across frames, changing contents and retry after missing media; existing wire-preview coverage checks original MIME/full adoption. CI and Codex review pending. No merge or deploy.
Summary by cubic
Shares repeated media file reads within a single history, message, or chat frame so the same content-addressed attachment is read once instead of once per reference. Previously fill called std::fs::read and allocated a fresh buffer for every message or chat pointing at the same payload, which left e.g. 12 messages naming one 1 MiB attachment holding 12 MiB of duplicate bytes.
The new fill_many holds a map of successful reads for the duration of the frame and builds on fill_with, preserving existing full/preview MIME adoption. Failed reads are retried on later references, and the map is dropped before publication and never survives an account switch. The change is confined to crates/gui/src/session/frames.rs; no manifests, lockfiles, or productive dependencies change.
Benchmarks in docs/frame-media-performance.md report median fill time for 12 references to one 1 MiB file dropping from 3.690 to 0.303 ms and retained payload falling from 12 MiB to 1 MiB (3.784 → 0.921 ms and 12 → 3 MiB with three distinct files). All-unique cases save no bytes and pay overhead: 12 tiny unique files add ~10 us, 15 allocation calls, and 556 requested live bytes per frame. The file also notes this is an isolated synthetic reproduction, not a full application-startup gain claim.
Validation
- 589 native GUI all-feature tests pass; 7 measurement tests ignored; Clippy with
-D warningsand workspace formatting pass. - New tests cover pointer sharing, preview MIME/flags, no cross-frame reuse, changing contents, and retry after a failed read.
Head 66abb0a only normalizes the two measurement CSV files to LF and configures the writer to emit LF. Parsed measurement records are unchanged; production and test source are identical to validated 6253f38. The full committed diff now passes git diff --check. Pages succeeded at 6253f38; checks for the formatting-only head are pending.