ReviewOS

zig-utils/zig-js

Runtime: replace blocking shell setTimeout with event-loop timers

#332
Closed chris opened this 11 days ago ·
11 days ago

Tracked by: #134 and #246 Related infrastructure: #330

Problem

The current global setTimeout is a test-shell compatibility shim that sleeps the executing agent, then queues a Promise reaction. It has no timer handle, clearTimeout, interval semantics, ref/unref ownership, or real event-loop scheduling. #330 needs an unrefed internal timer lane for AbortSignal.timeout, but silently treating that focused internal record as a complete host timer API would leave the public globals misleading.

Scope

  • Replace the blocking shim with context-owned monotonic event-loop timer queues.
  • Implement stable timeout/interval handles, clearTimeout/clearInterval, ordering, cancellation, reentrancy, and overflow rules.
  • Define refed keepalive versus unrefed work explicitly for shell, C API, worker, and shared-realm execution.
  • Run callbacks only on the owning JS event loop; background wakeups may publish readiness but must not execute JS.
  • Integrate timer roots/finalizers with precise GC and realm teardown.
  • Add fake-clock or bounded deterministic tests instead of timing-sensitive sleeps where practical.

Acceptance

  • Calling setTimeout never blocks the executing JavaScript agent.
  • Cancellation and intervals have web/Node-compatible observable ordering and exact-once ownership.
  • Timer callbacks participate in normal task/microtask checkpoints and keepalive semantics.
  • Focused tests and a descendant full CI matrix are green.

#330 may land its deliberately unrefed AbortSignal timer record first; this issue owns the complete public host timer API rather than expanding that ABI slice implicitly.

11 days ago

Triage after #330 established the reusable scheduler constraints:

  • General timers must be a separate Context-owned lane; the AbortSignal record remains deliberately unrefed and ABI-shaped.
  • A scheduled timeout/interval must own precise roots for its callback and arguments even when its JS handle is dropped. unref() changes shell keepalive only, never callback lifetime.
  • Timer selection will be deadline then insertion order. Each callback is one task, followed by a microtask checkpoint before another due timer is selected (the ordering primitive landed in 19d10631).
  • Cancellation during a running callback must leave the record valid until callback unwind and suppress interval rescheduling.
  • Main, worker, and shared-realm timers need an owning event-loop token; no background thread may execute JS.
  • Refed timers may park only in the host keepalive tail. Scheduling itself must return immediately; unrefed timers run only at later host checkpoints.
  • Handles will remain inspectable after fire/cancel without retaining a dangling native pointer; active records stay Context-owned and teardown drains them exactly once.

I am starting implementation after #330's descendant matrix is accepted so another push does not cancel that gate.

11 days ago

Implementation is now on main in b5ee60ad with documentation in 2edd8eda.

Delivered:

  • nonblocking Context-owned monotonic timeout/interval records, separate from the AbortSignal ABI lane;
  • stable Node-style handles (ref, unref, hasRef, refresh, close, numeric coercion) and both clear globals;
  • deadline/insertion ordering, one task plus microtask checkpoint per callback, cancellation/reentrancy, and uncaught-exception dispatch;
  • precise callback/argument roots with concurrent-mark insertion barriers;
  • refed keepalive and unrefed later-checkpoint behavior across shell/module, workers, and shared-realm Thread event loops;
  • owner-specific teardown, including dropped unrefed Thread timers;
  • concise docs at docs/timers.md and a README link.

Focused normal and TSan timer suites are green in arena, precise-GC, GIL, and no-GIL modes. Awaiting the descendant full matrix before closure.

11 days ago

Acceptance run advanced to descendant head bd045c9c so one full matrix validates both the timer implementation and the next CI-only profile gate: https://github.com/zig-utils/zig-js/actions/runs/29708347319. Before that advance, all four ordinary unit shards (including the new timer tests), docs, test262, ABI, benchmark, feature-profile, SIMD, and no-GIL witness jobs were green with no failures; focused timer TSan was already green locally.

11 days ago

The non-blocking context-owned timer implementation landed in b5ee60ad, its docs/ownership follow-ups landed in 2edd8eda and bd045c9c, focused local TSan was green, and descendant full CI completed with 42 successes, one intentional nightly skip, and zero failures/cancellations: https://github.com/zig-utils/zig-js/actions/runs/29716788619.

Sign in to comment on this issue.