The audio side had its own lag: `dumpsys media.audio_flinger` showed the track on the fast mixer with runs of ~55 ms underruns. The 8192-frame buffer above moved it to the normal mixer; the phone walk samples the underrun counters before and after and fails if they rose. ### The long session Every walk is minutes long; a phone runs the app for hours, and nothing in the port had ever been looked at for a leak. It has now, by driving the real bundle in headless Chrome for lap after lap of the whole app — five tabs, the Library, a record, playback, the mini bar, all three faces of the sheet, the artwork viewer, a row's half sheet, an artist page — and counting what the app was still holding at the start and at the end. **Listeners and timers do not leak, and the obvious counter says they do.** With a registry installed at document start — an investigation, not what ships — add minus remove climbs 23 → 119 over eight laps, about twelve a lap, almost all of them `scroll`. None of it is a leak: `{#key nav.seq}` replaces the `.screen` element on every navigation, so each new screen gets its own handler and the old element is dropped whole. Counting only what is still ATTACHED — `window`, `document`, or an element that is still connected — gives 29 at the first lap and 29 at the eighth, key for key. Intervals are the same story: two at boot (the media card's 30 s, the player's 10 s session writer), three from the first lap on, and three at the last; `Lane` and `Diagnostics` both clear theirs in their effect teardown, and every `bind*()` in `src/mobile/` is guarded by a module-level `bound` flag, so no Tauri subscription is ever registered twice. **What the walk ships is narrower than that, and says so.** A script registered with `Page.addScriptToEvaluateOnNewDocument` does not survive the first-run block's reloads — the re-attach opens a new DevTools session — so the registry the walk installs goes in as a STEP, after everything else, and can only see what is registered from that moment on. Its absolute counts are therefore small and depend on how much ran first: `tallyEars` reads 2 at the end of the full mock walk and 8 from a settled boot, `tallyTicks` 1 in both (the mock's own playback ticker). Those numbers are not a census of the app and are not asserted as one. What is asserted is the DELTA across the laps, which is where a per-navigation leak shows, and that was proved by planting one: a listener and an interval stranded once a lap take `grewEars` 0 → 9 and `grewTicks` 0 → 9, and the step fails. **The thumbnail map did leak, and is bounded now.** `src/mobile/art.svelte.js` kept every cover it had ever fetched for the WebView's lifetime: one browse of a 114-cover library took it from 5 keys to 114 and gave nothing back, and a catalog crawl adds to the same map without limit. A 360 px cover comes back from `album_art` as base64 JPEG — about 63 KB of string, measured against this machine's own thumbnail cache — so that browse alone is roughly 7 MB retained. The bound is the desktop's, by its own numbers: `ART_THUMB_CACHE_MAX` 512 and `ART_THUMB_CACHE_MAX_CHARS` 16 MiB from `src/lib/state.svelte.js`, trimmed oldest first. Two rules the desktop does not need are added, because the desktop's callers ask through `getArt(path)` while the phone's templates INDEX the map (`art[path]` in Cover, Release, Sheet, Viewer, Artist, Discography, Home and the media card): the entry that has just arrived is never dropped, and neither is one a connected `` is painting — `Cover`'s `visible` action re-arms only when its path PROP changes, so an evicted cover that is still mounted would stay a disc icon for good. The few places that paint a cover as a `background-image` rather than an `` — the ground, the sheet's wash, the artist hero — are covered twice over: each of them reads `art[path]` inside an effect that calls `wantArt`, so an eviction under one re-fetches on its own. The cache is therefore allowed to sit over its cap for as long as one screen needs all of it, and comes down the moment that screen goes. Built with the cap forced to 8 and scrolled through eighteen screens of a 140-card Library grid, the map sat at 61 — every one of them spared for being painted — and not one cover on screen lost its image at any point; the cards that had scrolled away repaint cold and fill as they come back, which is what a cold first visit already looks like. At the shipped cap of 512 a whole library walk runs the trim zero times and the census is identical either way: on Kyle's phone the change is inert, and it is the crawl past 512 covers that it exists for. The accent table gets the same cap — an accent is a pure function of its cover, so dropping one costs a recompute and nothing else. **Playlist covers.** `pls.covers` is keyed `id:revision`, and an edit bumps the revision, so every edit stranded one 360 px data URL under a key nothing could ever read again. The new cover now drops that playlist's other revisions as it lands — no cap and no policy, just the keys that are unreachable by construction. **Three things are unbounded on purpose.** `catalog.svelte.js`'s ten maps are cached for the session by design, and evicting from them is not safe the way the art map is: `Artist.svelte` calls `lookupArtist` inside `untrack`, so a page whose entry went missing under it would never look it up again. `lib.svelte.js`'s `tracksCache` is unbounded but finite — it is bounded by the library. And `player.svelte.js`'s `scheduleRecents` leaves a 47-second timeout per track start, which self-drains and is the desktop's own shape; coalescing the two callers into one slot would cancel one of them, so it is watched under a ceiling instead of changed. The walk measures all of it. Every step prints `tallyArt`, `tallyChars`, `tallyAcc`, `tallyPend`, `tallyCat`, `tallyQueue`, `tallyXfer` and `tallyNodes` from the app itself (`artTally()` on the `__ss` handle, because the map and its side tables are module-private), and `tallyEars`, `tallyTimers`, `tallyTicks` plus the `grew*` deltas from a registry the session block installs in the page. The registry is installed by a STEP, not by `Page.addScriptToEvaluateOnNewDocument`: the first-run block reloads three times and the re-attach opens a new DevTools session, so a script registered on the old one would be gone. The block's last step takes it out again — `EventTarget` and the four timer functions restored, `__ssCount` and the baseline deleted — so nothing appended after it inherits a patched platform, and every step after the block reads −1 for those fields, as does the whole phone walk, where it is never installed at all. The assertion is a threshold with headroom, not a stopwatch — the listener and interval counts are held flat, the art map is held under its cap, and `tallyTimers` is a ceiling rather than a delta because the recents window keeps a handful pending during a walk this fast. Across eleven laps of the block as it ships, driven against a real build of this tree, the listeners and the intervals moved by 0, the timers stood at 9 against a ceiling of 32, the art map grew 17 against a headroom of 80 and the catalog 5 against 24, and the DOM ended 10 nodes smaller than it started. ## Prerequisites (home machine, done 2026-09-12)