Skip to content

web: waveform thumbnails show instantly and stop flashing - #30

Merged
vynulldev merged 4 commits into
mainfrom
waveform-flicker
Jul 27, 2026
Merged

web: waveform thumbnails show instantly and stop flashing#30
vynulldev merged 4 commits into
mainfrom
waveform-flicker

Conversation

@vynulldev

Copy link
Copy Markdown
Owner

What & why

Four commits chasing one user-visible bug to its root: newly-added tracks' waveform thumbnails flashed black on view switches, and under a busy analysis queue never appeared at all until a page refresh. Found and fixed during the v0.3.0 release-test pass.

The root cause (last commit, and the one that actually matters): the browser's per-document image cache satisfies a re-created img with the bytes it already holds for that URL - the pre-analysis placeholder - no-store notwithstanding. During a batch analysis the library rev changes every poll tick and the table fully rebuilds each time, so every rebuilt row resurrected the placeholder; nothing racing the rebuilds could win. Measured against the real app: the waveform appeared 27 seconds after analysis completed (whenever the retry cadence happened to land after the churn stopped), and an explicit post-analysis kick only got that to 21 seconds because the next rebuild destroyed it. waveThumbSrc now appends a marker (&a=1) once the track has analysis, memoized per tracks snapshot, so the URL itself changes the moment the poll sees the BPM land - a rebuilt row can no longer collide with the cached placeholder. Measured after the fix: waveform visible in the same poll tick as the BPM, zero heal latency, all rows intact across view switches.

The three earlier commits fix real adjacent bugs the investigation surfaced, each verified in a browser harness against the extracted shipped functions:

  • Rewriting an img src directly blanks the element while an uncached resource loads, exposing the dark cell background - the black flash on view switches and column resizes, most visible on recently-added tracks whose PNG was not disk-cached at the new size. Src swaps now preload through a detached Image and swap only once the bytes are in; the old frame stays up throughout. On the server side, the rendered-PNG disk cache was written with a plain WriteFile racing concurrent reads of the same path, so a reader could get a truncated PNG that the 7-day Cache-Control would then pin in the browser; the cache write is now write-to-temp + rename.
  • A thumbnail healed via the cache-busted retry URL left the browser cache cold under the natural URL every future render uses; healed thumbnails now re-point at the natural URL, priming the cache with one cheap request.
  • The placeholder self-heal retried for a fixed ~6 minutes and then gave up silently - with analysis behind a long queue (bulk add, or the one-time cacheVersion re-analysis pass) the budget exhausted before the track's turn came, and the thumbnail then never appeared without a refresh. A failed fetch (server restart mid-request) killed the chain the same way. The retry now backs off (quick for the first minute, then ~30-45s for up to two hours) and an onerror handler reschedules, so neither long queues nor restarts strand it. The chain remains the fallback for lazy-analysis mode and fetch errors; the URL marker handles the common path.

Verified end to end by driving the real binary with a synthesized 12-track batch in a headless browser, measuring the time from BPM landing on the row to the waveform rendering: 27s before, 0s after, with view switches and a mid-batch tab-away/tab-back leaving every row intact.

Hardware testing

  • Tested on: N/A: web UI + one server-side cache-write fix; no deck-facing change. The waveform PNGs decks never see (they get PWV blobs over dbserver) are unaffected in content, only in cache-write atomicity.

Checklist

  • go build ./..., go vet ./..., and go test ./... pass
  • gofmt -l . is clean
  • New source files carry an SPDX header (GPL-3.0-or-later) (n/a, no new files)
  • Tested on real hardware (deck + firmware noted above), or this change doesn't affect deck behaviour
  • I agree my contribution is licensed under the project's GPLv3

Switching between collection and playlist views (or resizing) settles
the waveform column at a width that can differ from the render-time
estimate, and updateThumbnailSrcs then rewrites every thumbnail src at
the new size. Setting img.src directly blanks the element while an
uncached resource loads, exposing the dark cell background - most
visible on recently-added tracks, whose PNG is not disk-cached at the
new size yet and needs a live server render. Thumbnails showed, went
black, then showed again.

updateThumbnailSrcs now preloads the new URL through a detached Image
and swaps only once the bytes are in, so the old frame stays up
throughout; same-URL calls no-op. The placeholder self-heal retry in
thumbLoaded keeps its direct src set on purpose: its retry chain rides
the onload cycle and the cell is showing the 1x1 placeholder anyway.

Also found on the way: the server wrote the rendered PNG to its disk
cache with a plain WriteFile while concurrent requests read the same
path, so a reader could get a truncated PNG - and the 7-day
Cache-Control would pin the corrupt image in the browser. The cache
write is now write-to-temp + rename.

Verified in a headless browser against the extracted swapImgSrc with a
deliberately slow image server: a direct src set observably blanks the
img during load, the preload swap never does, and same-URL calls
no-op. The live view-switch repro was not run (the release-checklist
instance holds the ports); worth one eyeball after merging.
The placeholder self-heal loads a new track's first real waveform via
a cache-busted URL, and the natural URL was last fetched while it
still returned the no-store placeholder - so the browser has nothing
cached under the URL every future table render will use. Switching to
a playlist and back rebuilt the row cache-cold, and the
freshly-analyzed track sat blank again waiting on a full round-trip
while older tracks (cached from prior sessions) appeared instantly.

After a successful heal, thumbLoaded now swaps the img back to the
natural URL through the preload path: one cheap request (the server
just disk-cached that exact PNG) that primes the browser cache and
keeps the old frame up during the swap. Re-renders after that are
instant. No retry-loop risk: the swapped load re-enters thumbLoaded
with a real width and an unbusted URL and takes no action.

Verified in a browser harness against the extracted functions: a
healed img swaps to the natural URL with no blank frame observed and
exactly one follow-up fetch.
The placeholder self-heal retried for a fixed ~6 minute budget and
then gave up silently. With analysis behind a long queue - a bulk add,
or the one-time cacheVersion re-analysis pass - the budget exhausted
before the track's turn came, and the thumbnail then NEVER appeared,
even after analysis completed, until a page refresh built a fresh img.
A failed fetch (server restart mid-request) killed the chain the same
way: no onload, nothing rescheduled.

The retry now backs off instead of giving up: quick retries for the
first minute (a normal analysis wins that race), then ~30-45s for up
to roughly two hours, which covers any realistic queue while still
ending eventually for permanently-failed analysis. The retried
placeholder responses are no-store and cost the server almost nothing.
An onerror handler reschedules through the same path, so a restart
cannot strand pending thumbnails. The heal-swap bust check also
tightened to a [?&]_= match.

Verified in the accelerated browser harness against the extracted
functions: a track whose analysis completes past the old budget now
heals on completion (the old code was reproduced giving up at 60
tries and staying blank forever), and an img pointed at a dead server
keeps its retry chain alive through repeated fetch errors.
Root cause of new tracks' waveforms not appearing, found by driving
the real app: the browser's per-document image cache satisfies a
re-created <img> with the bytes it already has for that URL - the
placeholder - no-store notwithstanding. During a batch analysis the
rev changes every poll tick and the table fully rebuilds each time, so
every rebuilt row resurrected the placeholder, and neither the retry
chain nor an explicit post-analysis kick could win the race against
the next rebuild (measured: 27s and 21s of lag after analysis
completed; the loser saw the waveform only when the batch finished and
the churn stopped).

waveThumbSrc now appends &a=1 once the track has analysis (memoized
per allTracks snapshot), so the URL itself changes the moment the poll
learns the BPM landed: the rebuilt row can no longer collide with the
cached placeholder, fetches fresh, and every subsequent rebuild reuses
the real bytes cached under the analyzed URL. Measured end to end
against the real app: waveform visible in the same poll tick as the
BPM (0s heal latency, from 27s), all rows intact across view
switches. The self-heal retry chain stays as the fallback for
lazy-analysis mode and fetch errors.
@vynulldev
vynulldev merged commit 09b647a into main Jul 27, 2026
1 check passed
@vynulldev
vynulldev deleted the waveform-flicker branch July 27, 2026 23:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant