Split HLS cold-start startup phase into spawn / input-open+probe / first-GOP
#472
Closed
opened 2026-07-19 20:33:30 +02:00 by timothy
·
2 comments
No Branch/Tag Specified
main
901-pin-the-artifact-whole
renovate/meziantou.analyzer-3.x
release/v26.15.0-notes
renovate/lucene.net
renovate/cliwrap-3.x
issue-806-guard-populations
renovate/dotnet-monorepo
scratch/767b-poisoned
scratch/767b-control
release/v26.14.0-notes
release/v26.14.0
renovate/sqlitepclraw.bundle_e_sqlite3-3.x
docs/510-skill-logo-bug-policy
fix/510-watermark-resolution-policy
fix/629-verdict-classifier-falseopens
fix/609-decisions-edit-token-scope
issue-135-clear-to-none
release/v26.12.0-notes
fix/409b-lastscan-api-parity
fix/401-updatechannel-mirror-422
fix/327-playlist-rename-validation
fix/410-scancancel-log-level
fix/409-447-librariesscreen-neverscanned
fix/338-zap-exit-code
fix/367-plex-budget-message
fix/310-debom-legacy-cs
ci/604-lane-rebalance
feat/388-design-mirror
feat/247-test-ownership
feat/247-primary-action
feat/357-player-owned-playback
feat/357-jellyfin-plugin-poc
fix/289-mcp-hardening
issue58-mcp
feat/244-channels-extract
ci/auto-bump-prod-compose
feat/multi-rerun-collections-api
feat/collections-api
feat/quick-wins
feat/185-docs-part2
feat/140-collections-screen
feat/146-channel-edit
feat/147-classic-ui-link
issue22-renovate-dashboard
feat/91-cutover
feat/63-composite-create
feat/65-library-browse
feat/85-epg
feat/86-schedule-editor
feat/109-dashboard-data
feat/99-session-tracking
fix/dockerfile-node-tag
feat/59-spa-foundation
docs/59-ui-redesign-brief
feat/102-json-guide
feat/111-schedule-durations
feat/104-artwork-upload
feat/103-media-sources-api
feat/playouts-read-api
feat/108-health-api
feat/105-picker-list-endpoints
issue-97-channel-state-api
issue42-jellyfin-musicvideos
issue46-rest-api-error-contract
dependabot/nuget/ErsatzTV.FFmpeg.Tests/multi-d307a2e06f
qsv-improvements
hdr-vulkan-cuda-test
v26.15.0
v26.14.0
v26.13.0
v26.12.0
v26.11.0
v26.10.0
v26.9.0
v26.8.0
v26.7.0
blazor-final
v26.6.0
v26.5.0
v26.4.0
v26.3.1
v26.3.0
v26.2.0
v26.1.1
v26.1.0
v25.9.0
v25.8.0
v25.7.1
v25.7.0
v25.6.0
v25.5.0
v25.4.0
v25.3.1
v25.3.0
v25.2.0
v25.1.0
v0.8.8-beta
v0.8.7-beta
v0.8.6-beta
v0.8.5-beta
v0.8.4-beta
v0.8.3-beta
v0.8.2-beta
v0.8.1-beta
v0.8.0-beta
v0.7.9-beta
v0.7.8-beta
v0.7.7-beta
v0.7.6-beta
v0.7.5-beta
v0.7.4-beta
v0.7.3-beta
v0.7.2-beta
v0.7.1-beta
v0.7.0-beta
v0.6.9-beta
v0.6.8-beta
v0.6.7-beta
v0.6.6-beta
v0.6.5-beta
v0.6.4-beta
v0.6.3-beta
v0.6.2-beta
v0.6.1-beta
v0.6.0-beta
v0.5.8-beta
v0.5.7-beta
v0.5.6-beta
v0.5.5-beta
v0.5.4-beta
v0.5.3-beta
v0.5.2-beta
v0.5.1-beta
v0.5.0-beta
v0.4.5-alpha
v0.4.4-alpha
v0.4.3-alpha
v0.4.2-alpha
v0.4.1-alpha
v0.4.0-alpha
v0.3.8-alpha
v0.3.7-alpha
develop
v0.3.6-alpha
v0.3.5-alpha
v0.3.4-alpha
v0.3.3-alpha
v0.3.2-alpha
v0.3.1-alpha
v0.3.0-alpha
v0.2.5-alpha
v0.2.4-alpha
v0.2.3-alpha
v0.2.2-alpha
v0.2.1-alpha
v0.2.0-alpha
v0.1.5-alpha
v0.1.4-alpha
v0.1.3-alpha
v0.1.2-alpha
v0.1.1-alpha
v0.1.0-alpha
v0.0.62-alpha
v0.0.61-alpha
v0.0.60-alpha
v0.0.59-alpha
v0.0.58-alpha
v0.0.57-alpha
v0.0.56-alpha
v0.0.55-alpha
v0.0.54-alpha
v0.0.53-alpha
v0.0.52-alpha
v0.0.51-alpha
v0.0.50-alpha
v0.0.49-prealpha
v0.0.48-prealpha
v0.0.47-prealpha
v0.0.46-prealpha
v0.0.45-prealpha
v0.0.44-prealpha
v0.0.43-prealpha
v0.0.42-prealpha
v0.0.41-prealpha
v0.0.40-prealpha
v0.0.39-prealpha
v0.0.38-prealpha
v0.0.37-prealpha
v0.0.36-prealpha
v0.0.35-prealpha
v0.0.34-prealpha
v0.0.33-prealpha
v0.0.32-prealpha
v0.0.31-prealpha
v0.0.30-prealpha
v0.0.29-prealpha
v0.0.28-prealpha
v0.0.27-prealpha
v0.0.26-prealpha
v0.0.25-prealpha
v0.0.24-prealpha
v0.0.23-prealpha
v0.0.22-prealpha
v0.0.21-prealpha
v0.0.20-prealpha
v0.0.19-prealpha
v0.0.18-prealpha
v0.0.17-prealpha
v0.0.16-prealpha
v0.0.15-prealpha
v0.0.14-prealpha
v0.0.13-prealpha
v0.0.12-prealpha
v0.0.11-prealpha
v0.0.10-prealpha
v0.0.9-prealpha
v0.0.8-prealpha
v0.0.7-prealpha
v0.0.6-prealpha
v0.0.5-prealpha
v0.0.4-prealpha
v0.0.3-prealpha
v0.0.2-prealpha
v0.0.1-prealpha
Labels
Clear labels
ad-hoc
api
bug
ci-cd
content
dependencies
enhancement
frontend
in-progress
jellyfin
parked
priority: high
priority: low
priority: medium
review
security
One-off / ad-hoc work not tracked by a dedicated issue
REST API / HTTP endpoints
Something isn't working
Build, test, deploy pipeline
Channel content / schedules / playlists
Dependency updates (Renovate)
New feature or improvement
ChicoryTV React SPA frontend
Claimed by an active session — do not pick up
Jellyfin tuner / IPTV integration
Excluded from automatic queue pickup; work only when explicitly selected
Adversarial review finding
Security / vulnerability fix
Milestone
No items
No Milestone
Projects
Clear projects
No projects
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: timothy/ersatztv#472
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Follow-up to #350, scoped by its measurement (36 cold-starts on prod 26.10.0, N=33 content samples — see that comment).
Why
The #446 instrumentation split cold-start into
setup + startup + fill. The data says:setupstartupfillstartupis 81% of total and carries 100% of the variance, but it is a single opaque bucket spanning FFmpeg process spawn → input open/probe → decoder/encoder init → first GOP →live.m3u8appears.Crucially, the same channel varies up to 7.2× across repeat tunes with an identical filter graph (ch120
[3601, 500], ch124[3516, 3534, 601], ch125[8127, 6116, 1579]). So the driver is per-tune, not per-profile — which already falsified all three optimizations #350 had planned (see that comment). Two candidates remain and they need opposite fixes:.mkv, cold page cache, network stall) → fix is probe/analyzeduration tuning, prefetch, or cachingrenderD128) → fix is scheduling/priority or device selectionGuessing between them is what produced #350's wrong hypothesis ranking the first time. Measure, then optimize.
Scope
Extend the existing
HLS cold-startstructured log with a sub-split ofstartup. Suggested boundaries:spawnMs—Process.Start→ first FFmpeg progress/stderr activityinputOpenMs— → input opened / probe complete (parse FFmpeg's own-progress/stderr milestones, or bracket with-loglevelmarkers)firstGopMs— →live.m3u8existsKeep it Information-level, new-session path only, same one-line-per-cold-start shape. No new endpoint, no config knob, no transcode behavior change — consistent with #446.
Note the exact boundaries may need to follow what FFmpeg actually reports; if a clean three-way split isn't achievable from the process output, a two-way
spawn+openvsfirstGopsplit still discriminates the two hypotheses and is acceptable.Done-when
Pre-merge (this issue):
startupsub-split emitted in the cold-start log lineColdStartFeaturesTestsprecedent)Post-release (tracked on #350, not a merge gate — see note below):
Notes
priority: medium, matching #350.Claiming (Claude Code orchestrator session, 2026-07-19).
Selected via the deterministic selector (
scripts/select-queue.sh) — top of thepriority: mediumtier was #350, but its own wrap comment names this issue as "the actual next step", so #350 is the parent/container and #472 is the pickup. Unclaimed, deps clear, no prior comments.Plan, in the spirit of the lesson #350 taught: instrument first, don't guess. I'll extend the #446
HLS cold-startline with astartupsub-split, keeping the exact boundaries honest to what FFmpeg actually reports — if a clean three-way split isn't derivable from the process output, I'll ship the two-wayspawn+openvsfirstGopsplit the issue explicitly accepts, and say so rather than inventing a boundary the data can't support. Parsing logic gets pure unit tests per theColdStartFeaturesTestsprecedent.Prod sampling (last Done-when box) happens after the release reaches prod; if that isn't possible this session I'll leave that box unticked rather than tick it on a local run.
Closed — shipped in PR #482 (merged, CI green @
37674d65)What landed
The
HLS cold-startline now sub-splitsstartup:prep— ErsatzTV-side work before FFmpeg exists (playout-item resolution, pipeline build, graphics-engine spawn)ffmpegInit— FFmpeg launch → its first-progressoutputfirstGop— →live.m3u8existsLog-only: no transcode behavior change, no endpoint, no config knob. No API/SPA surface, so no doc or OpenAPI regen was owed.
Read this before interpreting the data — the split does NOT resolve this issue's headline question
The pipeline runs
-loglevel error -nostats -hide_banner, so a healthy FFmpeg writes nothing to stderr. Input-open/probe therefore cannot be separated from encoder-init without changing the FFmpeg command — which this instrumentation must not do.-progresson stdout is the only zero-cost milestone available, soffmpegInitstill contains BOTH rival hypotheses (NFS input open and VAAPI init under contention).What is genuinely new is the ErsatzTV-prep vs FFmpeg-time boundary. The issue explicitly accepted a two-way outcome, so this is in scope — but if prod samples show
ffmpegInitdominating (likely), the NFS-vs-VAAPI question is still open and needs a different instrument, or a deliberately-justified loglevel change. Do not read a largeffmpegInitas evidence for either hypothesis.Three caveats, documented on the type rather than left to be rediscovered
Runentry, not thestartupstopwatch — soprep + ffmpegInit + firstGopis a superset ofstartup(prep overlaps the tail ofsetup). Hencespans runEntryin the log line itself. Do not subtract these fromstartup.firstGop, the smallest bucket — and can flip a sample betweenThreeWayandTwoWayLateProgress.ffmpegInitspans the retry.Design stance: degrade, never invent
Milestones are recorded on the worker thread and read on the request thread, so ordering is validated, not assumed. Any missing/out-of-order milestone degrades to a coarser
splitKind(ThreeWay→TwoWay/TwoWayLateProgress→Unavailable) rather than producing a negative or fabricated bucket. A stalelive.m3u8(possible when the pre-session folder wipe fails —EmptyFolderswallows failures into a warning) is reportedUnavailablerather than as a plausible-looking sample. Milestones are set-once viaInterlocked, so a long session's repeatedTranscode()calls can't overwrite the cold-start numbers.Files
ColdStartStartupSplit.cs(new, Core/FFmpeg),PlaylistSegmentsResult.cs,HlsSessionWorker.cs,StartFFmpegSessionHandler.cs,ColdStartStartupSplitTests.cs(new, 12 cases).Review
Four cold-context adversarial passes, each over code the previous hadn't seen; every Medium-or-above fixed, none waived. Full record in the PR comment. Notably, two Mediums were the same defect class this PR exists to prevent — a comment whose rationale didn't match the mechanism — each landed inside the fix for the previous one. One Low is accepted and documented rather than fixed (stated in that comment).
Done-when: reworded, not quietly ticked
The original checklist required prod samples before merge, which is circular — prod only runs this after a release. Split into pre-merge criteria (all met) and a post-release item now owned by #350. Rationale recorded in the issue body so the next instrumentation issue doesn't repeat it.
Deferred
Prod sampling, by decision this session. It needs a release carrying this change to actually reach prod; the measurement stays open on #350 and remains the gate on any cold-start optimization. Nothing should be optimized off the old #350 numbers.