# Mobile V2 performance audit

Two focused fixes are published, stacked on [#2829](https://github.com/pingdotgg/t3code/pull/2829): cache-write coalescing in [#12109](https://github.com/pingdotgg/t3code/pull/12109), then unchanged project-enrichment suppression in [#12110](https://github.com/pingdotgg/t3code/pull/12110). Neither is merged. These remove unnecessary work; they do not establish that all mobile lag is fixed.

## Findings and disposition

| Finding | Evidence | Action |
|---|---|---|
| Whole-cache serialization repeats during streaming | Captured shell caches contain 761 + 122 threads; the larger cache is about 1.5 MB. Small shell events trigger full serialization. | Cache writes now coalesce with a ten-second minimum interval. Initial save remains prompt; lifecycle flushes retain latest state. |
| Identical project enrichment is republished | Existing server refresh path sends metadata-only snapshots without an equality gate. | Subscription-local dedupe preserves changed metadata, root-resolution progress, project deltas, authoritative snapshots, and resume behavior. |
| Provider catalogs are large and repeatedly refreshed | Captured status frames are 605,992 UTF-8 bytes, with nine providers and 671 models. One consecutive pair differs only in three checkedAt fields; other frames reorder models or change real data. | Deferred. Recursive timestamp suppression affects freshness in Settings and the mobile usage widget. A small freshness event or a separate catalog revision needs a contract/design decision. |
| Full encodes still cause JS scheduling gaps | Patched Home capture still has roughly 450 ms rAF gaps with Effect/schema and GC work. | Investigate incremental cache persistence or cheaper encoding, with a controlled Hermes replay. Throttling reduces frequency, not individual encode cost. |
| Rendering and allocation contribute to long stalls | Before Send's longest JS rAF interval was 7.06 seconds; the matching sampled window includes GC, ReactFabric, and Effect/schema work. Home/list, favicon URL, and swipe-row work also appears while a thread response is visible. | Investigate offscreen Home subscriptions, row invalidation, and favicon URL recomputation before changing architecture. These are leads, not isolated proven defects. |
| Send/navigation still has a large gap after the patch | Patched Send's largest rAF interval was 776 ms, with ReactFabric, GC, schema and native commit work. | Further targeted navigation/render profiling, then release-device confirmation. |

## Controlled before/after evidence

| Workload | Before | After |
|---|---:|---:|
| Ten updates through 10.5 seconds | 10 cache saves | 2 cache saves |
| One update per second for 60 seconds | 60 cache saves | 6 cache saves |
| Final pending update after settling | Already saved | Seventh save contains latest value |
| 60 identical enrichment frames reconstructed from captured project metadata | 60 frames, 381,540 bytes | 1 frame, 6,359 bytes |

Cache measurements use deterministic Effect TestClock workers. Additional tests exercise the actual shell, thread, and server-config state workers. Enrichment is a synthetic replay of captured project data, not a live network throughput benchmark.

The longer persistence interval trades approximately ten seconds of cache freshness on abrupt process termination for fewer full encodes. In-memory and authoritative server state remain current. Normal disconnect/teardown flushes preserve current data. Review found a queued snapshot could restore a deleted cache; that is fixed and covered through cooldown and teardown.

## Mobile coverage and measurement limits

Used one iPhone 16 Pro simulator on iOS 18.6, paired to the MacBook and Mac mini environments. Verified the native development client's fingerprint. Exercised Home scrolling and settled-list expansion, project/draft navigation, model-picker opening and Pi catalog expansion, Settings/Appearance, existing thread display, and two successful new tasks with streamed no-tool responses. Reloaded the final patched bundle and confirmed populated Home state without startup storage alerts.

The JS bundle used `dev=false&lazy=false` inside a debug native client. This is not an App Store release or a physical iPhone benchmark. Instrumentation recorded Hermes CPU samples, JS rAF delivery, large JSON.stringify results, and incoming WebSocket string lengths. It does not measure native dropped frames, whole-process CPU or battery use.

Live observations:

| Scenario | Before large stringifies | After large stringifies | Before / after longest JS rAF interval |
|---|---:|---:|---:|
| Home, 30 seconds | 17, 22.98 million code units | 5, 5.03 million code units | 3215 / 451 ms |
| Send, 45 seconds | 24, 35.13 million code units | 0 above the 50,000-unit threshold | 7064 / 776 ms |

These windows are **not controlled responsiveness comparisons**. Home expansion succeeded before but its corresponding after action was not confirmed in that window. Both Send prompts were identical, but provider completion took 33 versus 16 seconds and incoming traffic differed. The zero large-stringify count does not mean zero small writes or zero cache work. No percentage improvement in physical-device responsiveness is claimed.

## Correction to Fable's initial results

The earlier bundle combined `dev=false` with `lazy=true`. Dynamic imports attempted production chunk loading without a defined global.location. This broke SQLite and file-storage initialization, causing the startup alerts and a failed send. A fresh eager bundle removed those errors and successfully persisted cache rows and sent tasks. The old captures cannot establish successful disk-write rates or a production storage bug.

The reported four-second scroll delays were the automation tool's settle heuristic, not input-to-paint latency. The claimed 21,000 collections and 1.7 GB allocated per 30 seconds have no saved counter pair supporting that rate. They are excluded from this audit. The old idle capture supports about 7.7% non-root sample weight, not whole-process CPU.

## Verification and scope

74 focused client worker tests and 24 server tests passed. After the deletion guard, all 41 thread-sync tests passed again; its regression fails without the guard. Client-runtime and server typechecks passed. Focused lint and formatting passed apart from pre-existing lint warnings. Independent source review found no remaining blockers in the submitted changes.

Cache scheduling is shared by web, desktop, and mobile. Enrichment filtering applies to every V2 shell subscriber, independent of provider and local/remote/relay transport. No wire schema, navigation entry point, or UI layout changed. Provider freshness behavior remains unchanged. Android and physical-device performance were not measured. Only the patched client ran against the live environments; the server-side improvement is verified by focused tests/replay rather than deploying to either live server.

No application source changes were made in the main V2 worktree. Fixes live in the separate perf-cache-dedupe worktree. Local lockfile-only changes are uncommitted and excluded from both PRs. No live environment configuration or server deployment was changed. The profiling app and extra Metro process were stopped; the pre-existing Metro and booted simulator were left in place.

## Final PR state

Both latest heads have passing CI and no unresolved review threads. [#12110](https://github.com/pingdotgg/t3code/pull/12110) is approved at `2cee44587df`. [#12109](https://github.com/pingdotgg/t3code/pull/12109), at `559af163e46`, needs human review: Macroscope declined automatic approval because the ten-second cache freshness gap after abrupt termination affects persistence/offline-resume behavior. Its correctness and Effect-convention checks passed. Neither PR is merged.
