feat(phase 1.4): setActive load driver — 100 calls in 60s + p50/p95/p99 latency #22

Merged
deco merged 1 commit from feat/phase-1.4-load-driver into main 2026-05-20 23:09:09 +03:00
Owner

Summary

Closes the script half of Phase 1.4 from .omc/plans/stream-a-thon-2026-06-13.md. PROD-HOST execution remains operator-driven — this PR ships the driver only.

What it does

  1. GET ${webui}/api/streams → live stream pool.
  2. Round-robin (stream × screen) picker over --screens (default: all 7 SCREEN_POSITIONS).
  3. Schedules --calls (default 100) POST /api/setActive evenly across --duration-ms (default 60000) — one start every ~600 ms; calls overlap if any takes longer than the interval (intentional — bursts are part of what we want to load-test).
  4. Classifies each response: ok / validation / db_lock / http_error / network_error. db_lock is a dedicated bucket because the Phase 1.4 plan acceptance specifically calls out "no SQLite 'database is locked' errors" as a pass criterion.
  5. Records per-call HTTP latency; reports p50 / p95 / p99 / min / max.
  6. Writes a JSON report to docs/phase-1.4-load-report.<ISO>.json.
  7. Exits 0 if Phase 1.4 SLO passes (p95 ≤ 2000 ms warm AND db_lock=0), 1 if SLO fails, 2 on pre-flight error.

Decomposition (Phase 1.3 style — small testable modules behind a thin CLI)

File Role
loadDriver/stats.ts percentile + computeStats (pure)
loadDriver/schedule.ts evenly-spaced fire offsets
loadDriver/classifyResult.ts bucket assignment incl. db_lock detector
loadDriver/picker.ts round-robin (stream × screen), mirrors setActive group-name derivation
loadDriver/runDriver.ts orchestrator (deps-injected fetch/now/setTimeout/logger for unit testability)
loadDriver/reporter.ts console output + evaluateSLO (p95 + db_lock gate)
loadDriver.ts CLI entry — arg parsing, stream-pool fetch, JSON output, exit codes

Test plan

  • Unit: 42 new tests across 6 suites in scripts/loadDriver/__tests__/ (suite total 172/26)
  • Type-check + lint clean
  • npm run audit:sqlite-opens clean (HTTP-only script, no new sqlite sites)
  • Smoke run against local dev (3 seeded streams, 15 calls in 1.5 s, screens=large,left): 15/15 ok, p50=15 ms, p95=144 ms, p99=144 ms, SLO PASS, exit 0
  • Unreachable-webui case exits 2 (verified)
  • --dry-run exits 0 without firing calls (verified)
  • PROD-HOST execution (operator-driven, follows Phase 0.5 baseline capture and the Streamlink supervisor coming up; gates on Phase 4.2 alongside this)

Non-goals (intentional, per scripts/loadDriver/README.md §"What it does NOT do")

  • Does NOT connect to OBS WebSocket. Click→program-change e2e latency is the Phase 4.2 dress-rehearsal gate; this script measures the HTTP latency that setActive owns. Plugin-poll-side delay (≤1000 ms per G2) is constant and additive — folded into the Phase 4.2 SLO, not this one.
  • Does NOT run Streamlink. The "7 concurrent Streamlinks for 30 min" half of Phase 1.4 is operator-driven via the existing supervisor entrypoint (npm run supervisor).
  • Does NOT mutate the database. Read-only against /api/streams.

Usage

```sh
npm run load:setactive -- --webui-url http://192.168.13.21:3000

or

npx tsx scripts/loadDriver.ts --calls 100 --duration-ms 60000
```

Full options in scripts/loadDriver/README.md.

Depends on

  • #21 (Phase 0.8 — schema bootstrap fix). Smoke-tests on a DB whose schema was repaired by the standalone migration scripts; if a fresh DB is provisioned without that PR landed, the driver will exit 2 on its /api/streams pre-flight.
## Summary Closes the script half of Phase 1.4 from `.omc/plans/stream-a-thon-2026-06-13.md`. PROD-HOST execution remains operator-driven — this PR ships the driver only. ### What it does 1. `GET ${webui}/api/streams` → live stream pool. 2. Round-robin (stream × screen) picker over `--screens` (default: all 7 `SCREEN_POSITIONS`). 3. Schedules `--calls` (default 100) `POST /api/setActive` evenly across `--duration-ms` (default 60000) — one start every ~600 ms; calls overlap if any takes longer than the interval (intentional — bursts are part of what we want to load-test). 4. Classifies each response: `ok` / `validation` / `db_lock` / `http_error` / `network_error`. `db_lock` is a dedicated bucket because the Phase 1.4 plan acceptance specifically calls out "no SQLite 'database is locked' errors" as a pass criterion. 5. Records per-call HTTP latency; reports p50 / p95 / p99 / min / max. 6. Writes a JSON report to `docs/phase-1.4-load-report.<ISO>.json`. 7. Exits `0` if Phase 1.4 SLO passes (`p95 ≤ 2000 ms` warm AND `db_lock=0`), `1` if SLO fails, `2` on pre-flight error. ### Decomposition (Phase 1.3 style — small testable modules behind a thin CLI) | File | Role | |---|---| | `loadDriver/stats.ts` | percentile + computeStats (pure) | | `loadDriver/schedule.ts` | evenly-spaced fire offsets | | `loadDriver/classifyResult.ts` | bucket assignment incl. `db_lock` detector | | `loadDriver/picker.ts` | round-robin (stream × screen), mirrors `setActive` group-name derivation | | `loadDriver/runDriver.ts` | orchestrator (deps-injected `fetch`/`now`/`setTimeout`/`logger` for unit testability) | | `loadDriver/reporter.ts` | console output + `evaluateSLO` (p95 + db_lock gate) | | `loadDriver.ts` | CLI entry — arg parsing, stream-pool fetch, JSON output, exit codes | ### Test plan - [x] Unit: 42 new tests across 6 suites in `scripts/loadDriver/__tests__/` (suite total 172/26) - [x] Type-check + lint clean - [x] `npm run audit:sqlite-opens` clean (HTTP-only script, no new sqlite sites) - [x] Smoke run against local dev (3 seeded streams, 15 calls in 1.5 s, screens=large,left): 15/15 ok, p50=15 ms, p95=144 ms, p99=144 ms, **SLO PASS**, exit 0 - [x] Unreachable-webui case exits 2 (verified) - [x] `--dry-run` exits 0 without firing calls (verified) - [ ] **PROD-HOST execution** (operator-driven, follows Phase 0.5 baseline capture and the Streamlink supervisor coming up; gates on Phase 4.2 alongside this) ### Non-goals (intentional, per `scripts/loadDriver/README.md` §"What it does NOT do") - Does **NOT** connect to OBS WebSocket. Click→program-change e2e latency is the Phase 4.2 dress-rehearsal gate; this script measures the HTTP latency that `setActive` owns. Plugin-poll-side delay (≤1000 ms per G2) is constant and additive — folded into the Phase 4.2 SLO, not this one. - Does **NOT** run Streamlink. The "7 concurrent Streamlinks for 30 min" half of Phase 1.4 is operator-driven via the existing supervisor entrypoint (`npm run supervisor`). - Does **NOT** mutate the database. Read-only against `/api/streams`. ### Usage \`\`\`sh npm run load:setactive -- --webui-url http://192.168.13.21:3000 # or npx tsx scripts/loadDriver.ts --calls 100 --duration-ms 60000 \`\`\` Full options in `scripts/loadDriver/README.md`. ### Depends on - #21 (Phase 0.8 — schema bootstrap fix). Smoke-tests on a DB whose schema was repaired by the standalone migration scripts; if a fresh DB is provisioned without that PR landed, the driver will exit 2 on its `/api/streams` pre-flight.
feat(phase 1.4): setActive load driver — 100 calls in 60 s + p50/p95/p99 latency
All checks were successful
Lint and Build / build (pull_request) Successful in 3m11s
b6810ece01
Closes the script half of Phase 1.4 from the stream-a-thon 2026-06-13
plan. PROD-HOST execution remains operator-driven (this commit ships
the driver only — the operator runs it on event-day rehearsal night
against the production webui).

What it does
  1. GET ${webui}/api/streams → live stream pool.
  2. Round-robin (stream × screen) picker over --screens (default: all
     7 SCREEN_POSITIONS).
  3. Schedules --calls (default 100) POST /api/setActive across
     --duration-ms (default 60000) — one start every ~600 ms; calls
     overlap if any takes longer than the interval (intentional —
     bursts are part of what we want to load-test).
  4. Classifies each response: ok / validation / db_lock / http_error /
     network_error. db_lock is a dedicated bucket because the Phase 1.4
     plan acceptance specifically calls out "no SQLite 'database is
     locked' errors" as a pass criterion.
  5. Records per-call HTTP latency; reports p50 / p95 / p99 / min / max.
  6. Writes a JSON report to docs/phase-1.4-load-report.<ISO>.json.
  7. Exits 0 if Phase 1.4 SLO passes (p95 ≤ 2000 ms warm AND
     db_lock=0), 1 if SLO fails, 2 on pre-flight error.

Decomposition (Phase 1.3 style — small testable modules behind a thin
CLI):
  - stats.ts: percentile + computeStats (pure)
  - schedule.ts: scheduleFireOffsets (evenly-spaced offsets)
  - classifyResult.ts: bucket assignment incl. db_lock detector
  - picker.ts: round-robin (stream × screen) → setActive payload
    + expectedGroupName mirroring app/api/setActive/route.ts:46-49
  - runDriver.ts: orchestrator. Deps (fetch, now, setTimeout, logger)
    are injected for unit testability — runDriver tests verify call
    payloads, bucket counts, network-error handling, and latency
    capture without spinning a real webui.
  - reporter.ts: console + evaluateSLO (p95 + db_lock gate).
  - loadDriver.ts: CLI entry with arg parsing, stream-pool fetch,
    JSON output, exit codes.
  - README.md: usage, options, operator runbook, explicit non-goals.

Tests: 42 new tests across 6 suites in scripts/loadDriver/__tests__/.
Suite total: 172/26 (was 130/20 on main). Type-check + lint clean;
sqlite-opens audit unchanged (this script makes HTTP calls only).

Smoke test (local, against fresh seeded DB, 15 calls in 1.5 s,
screens=large,left): 15/15 ok, p50=15 ms, p95=144 ms, p99=144 ms,
SLO PASS, exit 0. Unreachable-webui case exits 2.

Non-goals (intentional, per README §"What it does NOT do"):
  - Does NOT connect to OBS WebSocket. Click→program-change e2e
    latency is the Phase 4.2 dress-rehearsal gate; this script measures
    only the HTTP latency that setActive owns. Plugin-poll-side delay
    (up to 1000 ms per G2) is constant and additive — folded into the
    Phase 4.2 SLO, not this one.
  - Does NOT run Streamlink. The "7 concurrent Streamlinks for 30 min"
    half of Phase 1.4 is operator-driven via the existing supervisor
    entrypoint (npm run supervisor).
  - Does NOT mutate the database. Read-only against /api/streams.
deco force-pushed feat/phase-1.4-load-driver from b6810ece01
All checks were successful
Lint and Build / build (pull_request) Successful in 3m11s
to 573cc8d02e
All checks were successful
Lint and Build / build (pull_request) Successful in 2m53s
2026-05-20 23:06:13 +03:00
Compare
deco scheduled this pull request to auto merge when all checks succeed 2026-05-20 23:06:57 +03:00
deco merged commit dcc2e5cabd into main 2026-05-20 23:09:09 +03:00
deco deleted branch feat/phase-1.4-load-driver 2026-05-20 23:09:09 +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!22
No description provided.