Skip to content

Runner: throttle-proof host-timed waits (Worker timer) — 3 s steps no longer run 4 s in a hidden Studio window (LAB-303) - #260

Open
mbreiser wants to merge 1 commit into
mainfrom
claude/runner-worker-timer
Open

mbreiser wants to merge 1 commit into
mainfrom
claude/runner-worker-timer

Conversation

@mbreiser

Copy link
Copy Markdown
Contributor

Summary

LAB-303 follow-up from the Bergamo ScanImage day (2026-10-16, ros2-poc/scanimage-day-2026-10-16.md item 5): host-timed protocol steps ran 4.00 s instead of 3.00 s whenever the Studio window was hidden or covered (the operator working in ScanImage). ArenaRunner._sleep was a plain setTimeout, which Chrome aligns to 1 s ticks once a tab is hidden or the window is occluded (and to 1 min after 5 min hidden).

  • js/arena-runner-g6.js: _sleep now runs on createSleepTimer() — a dedicated Worker built from an inline blob URL (no extra file; the runner stays a classic <script src> module per CLAUDE.md) arms the timeout and posts back. A page setTimeout is armed beside it as a backstop; whichever fires first wins, so the Worker path is never worse than the old behavior. Without Worker/Blob/URL (Node tests, old engines) or after a Worker error, the setTimeout alone runs, exactly as before. _resolveSleep() clears through the same timer, so stop() / _clear() / the runner's finally still wake the wait at once. One Worker per page (sleepTimer()), created on first use.
  • Tripwire: a wait that still overshoots by more than 250 ms emits a warn event (host-timed wait ran 4.00 s instead of 3.00 s (browser timer throttling? keep the Studio window visible)) into the run log, so a throttled or starved page is visible in the record, not only in the data afterwards. This is independent of the document.hidden banner planned for the 2026-10-20 candidate; both can live together.
  • arena_studio.html: bumps the runner's ?v= cache-buster (20261018-worker-timer). This one line will conflict trivially with Studio: stop the run when the FicTrac bridge is lost (BRIDGE_LOST) + ?p= links never wait on GitHub (LAB-303 S1, S2) #257's 20261015-bridge-lost bump once that lands; keep the newer token.
  • Exports: createSleepTimer, sleepTimer, SLEEP_LATE_WARN_MS. ArenaRunnerG6.sleepTimer().kind reads 'worker' in a browser and 'timeout' in Node — a headless check can assert it.

Tier

next candidate (after 2026-10-20), together with #219. No footer/version bump (the release commit does it).

Release notes

Studio

  • Host-timed steps keep their length when the Studio window is hidden or covered. The waits between commands (and a trial's host-side end) used a browser timer that Chrome slows to whole seconds once the tab is hidden or the window is behind another one; on the Bergamo rig every 3 s step ran 4 s while the operator worked in ScanImage. The wait now runs in a background Worker, which the browser does not throttle, with the old timer kept as a fallback.
  • A wait that still runs long is flagged. If a host-timed wait overshoots by more than a quarter second, the run log gets a warning naming timer throttling and asking you to keep the Studio window visible.

Test plan

  • pixi run test (61/61 files; tests/test-arena-runner-g6.js 553 checks: fallback sleep completes, stop()/_clear() wake a 5 s wait, fake-Worker path set/clear/backstop/onerror → fallback, the inline Worker source's message protocol, late-wait warn, runSequence with the real sleep completes and aborts mid-wait)
  • pixi run studio-smoke 0 failures, 0 warnings
  • Prettier clean on the touched JS
  • Browser: open the Studio, run a protocol with 3 s host-timed steps with the window covered/minimized; ArenaRunnerG6.sleepTimer().kind === 'worker' in DevTools; step intervals 3.0 s (step_timing.py)

🤖 Generated with Claude Code

…out backstop (LAB-303)

ArenaRunner._sleep was a plain setTimeout. Chrome aligns page timers to 1 s ticks
once the tab is hidden or the window is occluded, so on the Bergamo 2P rig
(2026-10-16) every host-timed 3 s step ran 4.00 s while the operator worked in
ScanImage over the Studio window.

The wait now runs on createSleepTimer(): a dedicated Worker built from an inline
blob URL (the runner stays a classic <script src> module) arms the timeout and
posts back; a page setTimeout is armed beside it as a backstop and whichever
fires first wins, so the Worker path is never worse than before. Without Worker /
Blob / URL (Node tests) or after a Worker error the setTimeout alone runs.
_resolveSleep() clears the handle through the same timer, so stop() / _clear()
still wake the wait at once. A wait that overshoots by > 250 ms emits a 'warn'
event naming timer throttling, so a starved page shows in the run log.

Tests: fallback completes, stop()/_clear() wake a 5 s wait, fake-Worker path
(set/clear/backstop/onerror → fallback), the inline Worker source's protocol,
late-wait warn, and runSequence with the real sleep (completes; aborts mid-wait).
arena_studio.html bumps the runner's ?v= cache-buster.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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