Everything written this session that has not been through the device: rounds 43 to 48 (each patch adversarially verified, 43's transient-notice expectation fixed), a partial round 49, the pipeline scripts, the Android Auto design and the icon comparison mock. Also the two harness changes that were never A/B'd — adaptive waits, which may only shorten a declared wait and never lengthen it, and a ship that runs the mock walk and the deploy at the same time. The A/B and its 255-step baseline are in handoff/tools; main stays at what the phone has actually walked.
112 lines
7.2 KiB
Plaintext
112 lines
7.2 KiB
Plaintext
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 `<img>` 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 `<img>` — 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)
|