feat(phase 4.2): switcher latency baseline on prod OBS host — 32× headroom #29

Merged
deco merged 1 commit from feat/phase-4.2-latency-baseline into main 2026-05-22 03:06:32 +03:00
Owner

Summary

Closes the G4.2 SLO gate — the dress-rehearsal latency-acceptance check that the plan was treating as a partial / unverified gate.

Measured on the real Windows OBS host (bridge) at 2026-05-21T23:59Z:

p50 p95 p99 min max mean
Switch latency (ms) 62 63 374 60 374 72

30/30 samples, 0 timeouts. SLO p95 ≤ 2000 ms warm: PASS — 32× headroom.

Methodology

Three priority methods were spelled out in docs/plugin-contract.md after the original Phase 0.5.2 baseline was found to be measuring WebSocket RTT instead of real switch latency. This PR uses the third (and turns out to be the only viable) one: screenshot-hash polling.

Why event-driven detection (priority 1, the original hope) was rejected:

  • scripts/discoverSwitcherEvents.mjs subscribed to ~50 event types and captured zero events across a 6 s window after a known-good file write.
  • The OBS debug-WS log (with WSPR logging enabled) shows zero op: 5 messages in the request/response stream during a switch.
  • ss_large is an input (source_switcher kind), not a scene — so GetSceneItemList / SceneItemEnableStateChanged don't apply at all.
  • settings.current_index does not update at runtime.

Conclusion: the plugin switches silently. Screenshot-hash polling is the only ground-truth signal.

Interpretation

The plugin's current_source_file_interval: 1000 (ms) is not the actual poll cadence — true latency is ~60 ms. That value must throttle some other internal operation (likely a file-stat coalescing window).

The 50 ms screenshot poll interval is now the limiting factor in the measurement, not the plugin. True switch latency could be as low as 10–20 ms.

Files

  • scripts/measureSwitcherLatencyV2.mjs — the real measurement script (plain Node ESM, runs on bridge without tsx)
  • scripts/measureSwitcherLatencyV2.package.json — npm-install template to ship alongside the script
  • scripts/{discoverSwitcherEvents,probeSwitcherMechanism,dumpInputState}.mjs — diagnostics kept in-repo as the audit trail for the methodology decision
  • docs/plugin-contract.md — appended Phase 4.2 baseline section with results, interpretation, caveats, and re-run instructions
  • docs/phase42-latency-baseline-win.json — full sample data

Safety

large.txt was saved at startup and restored on exit; all 30 measurements alternated between two non-original plugin source values (jellyfish_palpatine_streamjellyfish_dellgate_stream). No production state was left mutated.

Test plan

  • 220/220 jest tests pass
  • tsc --noEmit clean
  • npm run audit:sqlite-opens clean
  • Measurement actually ran on the production host with the real plugin
  • Phase 4.1 dress rehearsal should re-run this script under encoder load (7 concurrent ffmpeg_source inputs + streaming output) to confirm the result holds. End-to-end click → program ≈ Phase 1.4 setActive p95 + this ~63 ms switch p95.
## Summary Closes the **G4.2 SLO gate** — the dress-rehearsal latency-acceptance check that the plan was treating as a partial / unverified gate. Measured on the real Windows OBS host (bridge) at 2026-05-21T23:59Z: | | p50 | p95 | p99 | min | max | mean | |---|---|---|---|---|---|---| | Switch latency (ms) | 62 | **63** | 374 | 60 | 374 | 72 | 30/30 samples, 0 timeouts. **SLO p95 ≤ 2000 ms warm: PASS — 32× headroom.** ## Methodology Three priority methods were spelled out in `docs/plugin-contract.md` after the original Phase 0.5.2 baseline was found to be measuring WebSocket RTT instead of real switch latency. This PR uses the third (and turns out to be the only viable) one: screenshot-hash polling. Why event-driven detection (priority 1, the original hope) was rejected: - `scripts/discoverSwitcherEvents.mjs` subscribed to ~50 event types and captured **zero** events across a 6 s window after a known-good file write. - The OBS debug-WS log (with `WSPR` logging enabled) shows **zero `op: 5` messages** in the request/response stream during a switch. - `ss_large` is an *input* (`source_switcher` kind), not a scene — so `GetSceneItemList` / `SceneItemEnableStateChanged` don't apply at all. - `settings.current_index` does not update at runtime. Conclusion: the plugin switches silently. Screenshot-hash polling is the only ground-truth signal. ## Interpretation The plugin's `current_source_file_interval: 1000` (ms) is **not** the actual poll cadence — true latency is ~60 ms. That value must throttle some other internal operation (likely a file-stat coalescing window). The 50 ms screenshot poll interval is now the limiting factor in the measurement, not the plugin. True switch latency could be as low as 10–20 ms. ## Files - `scripts/measureSwitcherLatencyV2.mjs` — the real measurement script (plain Node ESM, runs on bridge without tsx) - `scripts/measureSwitcherLatencyV2.package.json` — npm-install template to ship alongside the script - `scripts/{discoverSwitcherEvents,probeSwitcherMechanism,dumpInputState}.mjs` — diagnostics kept in-repo as the audit trail for the methodology decision - `docs/plugin-contract.md` — appended Phase 4.2 baseline section with results, interpretation, caveats, and re-run instructions - `docs/phase42-latency-baseline-win.json` — full sample data ## Safety `large.txt` was saved at startup and restored on exit; all 30 measurements alternated between two non-original plugin source values (`jellyfish_palpatine_stream` ↔ `jellyfish_dellgate_stream`). No production state was left mutated. ## Test plan - [x] 220/220 jest tests pass - [x] `tsc --noEmit` clean - [x] `npm run audit:sqlite-opens` clean - [x] Measurement actually ran on the production host with the real plugin - [ ] Phase 4.1 dress rehearsal should re-run this script under encoder load (7 concurrent ffmpeg_source inputs + streaming output) to confirm the result holds. End-to-end click → program ≈ Phase 1.4 setActive p95 + this ~63 ms switch p95.
feat(phase 4.2): switcher latency baseline on prod OBS host (screenshot-hash)
All checks were successful
Lint and Build / build (pull_request) Successful in 2m59s
9e0ddac7b1
Closes the G4.2 SLO gate. Measurement target was p95 ≤ 2000 ms warm;
real measurement on bridge (Windows production OBS host) shows:

  p50=62  p95=63  p99=374  min=60  max=374  mean=72
  Samples: 30/30 (0 timeouts)
  SLO PASS — 32× headroom

The plugin's documented current_source_file_interval=1000 (ms) is not
the actual poll cadence — true switch latency is ~60 ms, so that value
must throttle some other internal operation (likely a file-stat
coalescing window).

Methodology: screenshot-hash polling (priority 3 in plugin-contract.md)
turned out to be the only ground-truth signal available. The
event-driven approach (priority 1) was investigated and rejected ---
the plugin emits ZERO observable OBS-WebSocket events for switches,
verified two ways:
- discoverSwitcherEvents.mjs subscribed to ~50 event types, captured 0
- OBS debug-WS log (with WSPR logging enabled) shows 0 op:5 messages
  in the request/response stream during a switch.

Also, ss_large is an *input* (custom obs source kind = source_switcher),
not a scene — so GetSceneItemList / SceneItemEnableStateChanged don't
apply at all. settings.current_index does not update at runtime either.

Files:
- scripts/measureSwitcherLatencyV2.mjs — the real measurement (ESM,
  plain Node, runs on bridge without tsx)
- scripts/measureSwitcherLatencyV2.package.json — npm-install template
  to ship alongside the script onto bridge
- scripts/{discoverSwitcherEvents,probeSwitcherMechanism,dumpInputState}.mjs
  — diagnostics kept in-repo as the audit trail for the methodology
  decision
- docs/plugin-contract.md — appended Phase 4.2 baseline section with
  results, interpretation, caveats, and re-run instructions
- docs/phase42-latency-baseline-win.json — full sample data

Safety contract held: large.txt was saved at startup and restored on
exit; all 30 measurements alternated between two non-original plugin
source values.

220/220 jest tests pass; type-check + audit:sqlite-opens clean.
deco scheduled this pull request to auto merge when all checks succeed 2026-05-22 03:05:28 +03:00
deco merged commit 9ce641b2b3 into main 2026-05-22 03:06:32 +03:00
deco deleted branch feat/phase-4.2-latency-baseline 2026-05-22 03:06:32 +03:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
deco/cuesheet!29
No description provided.