Skip to content

Keep the advert pass off the listeners' thread, and make it skip what it has already done - #15

Merged
antoine-debroye merged 1 commit into
mainfrom
ad/audit-tier3-performance
Oct 9, 2026
Merged

antoine-debroye merged 1 commit into
mainfrom
ad/audit-tier3-performance

Conversation

@antoine-debroye

Copy link
Copy Markdown
Collaborator

Why

Tier 3 of the October audit (doc/audit-2026-10-09.md, section 3, plus X2 from section 2). The advert pass decoded audio on the HTTP thread, re-did all of its work every five minutes, and read whatever was on the share without the containment check the serving routes use. Stacked on #14.

What changed

  • P1 — decoding and fingerprinting run in the worker thread. New src/lib/audio-tasks.js (fingerprint a file, profile a file, decode one transcription window to WAV) runs in the existing worker with transferred typed arrays; the main thread only ever sees results. A per-task timeout terminates a wedged worker and fails that episode rather than hanging the pass. Measured with a 10 ms interval during a 1-hour fingerprint: inline 10,046 ms late, worker 1 ms late. On a NAS this was the difference between /health answering and the container being restarted mid-pass.
  • X2 — pipeline reads are contained and size-gated. New src/lib/pipeline-file.js: resolveContained → stat → regular file only → 768 MB cap, re-checked on the open descriptor with O_NONBLOCK so a FIFO is refused instead of blocking. Used by the fingerprinter, transcriber, trimmer and the sample routes. A refusal names the file once in the activity log and raises a banner.
  • P2 — the pass is incremental. Detectors run only when a per-show key (fingerprints, transcripts, episodes, markers, settings, owner actions, catalogue digest) changed since the last pass; locateAnchor runs only for (anchor, episode) pairs without a fresh hit row (checked_at is now read); fingerprints load lazily; settleHolds is one GROUP BY instead of a query per episode.
  • P3 — no more whole-file reads for a clip. src/lib/mp3-range.js walks frame headers in chunks and reads only the byte range a sample needs, with a small cache keyed on (path, size, mtime); the trimmer streams cut parts instead of holding two copies.
  • P4 — @fastify/compress for HTML, CSS, JS and JSON only, registered inside encapsulated contexts so it can never touch /media or /feeds; explicit text types, no request-body decompression.
  • P5/P6 — rolling: false sessions (12 hours from sign-in, matching "Stay signed in for"); PRAGMA optimize on close and daily; journal_size_limit; hoisted prepared statements; migration 012 adds the four missing FK indexes on the advert tables.

How it was verified

Known limits: the "last detected" key is in memory, so every show is looked at once after a restart; a decode failure in the worker during transcription counts toward the existing whisper breaker.

🤖 Generated with Claude Code

… it has already done

Audit tier 3 (P1-P6) and X2.

P1: reading, hashing and decoding an episode — to fingerprint it or to hand a
window of it to the recogniser — now runs in the audio worker that already held
the corpus searches. The main thread hands over a gated path and writes down what
comes back; typed arrays are transferred, not copied, and the fingerprint is byte
for byte what the main thread produced. Measured on an hour of audio: a 10 ms
timer fired 10,046 ms late inline and 1 ms late with the worker. A worker that
crashes or wedges past its timeout fails that episode loudly and is restarted.

X2: fingerprinting, listening and cutting all go through one gate that resolves
the path inside the show folder, stats it, and refuses a link out of the folder,
anything that is not an ordinary file, and anything over 768 MB — before a byte is
read, and again on the open descriptor. A refusal is a banner naming the file and
one line in the pass's activity entry.

P2: the detectors run only when their inputs or the catalogue content changed
since the last pass (a per-show key the pipeline keeps in memory, plus a counter
every owner action bumps). The jingle is located only in episodes whose
fingerprint is newer than their hit row; fingerprints are read lazily, a header
only where timing is all that is needed; the held-episode count is one GROUP BY.
A taught boundary links to the jingle within the pass instead of the next one.

P3: the two sample routes read only the frames they play, through a per-file
frame table built by a chunked walk proven frame-for-frame equal to readFrames;
the trimmer hashes and streams the cut's parts rather than concatenating a copy.

P4: @fastify/compress 9.2.1 for HTML, CSS, JS and JSON, in the web and API
contexts only; audio and the pre-compressed feed are untouched, and request
bodies are never inflated.

P5/P6: session not rolling; PRAGMA optimize on close and daily; journal_size_limit;
hoisted prepares; migration 012 indexing the advert tables' foreign keys.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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