Skip to content

High-Resolution Timer Wheel for Scheduled Soroban Contract Executions with SharedArrayBuffer Timekeeping #56

Description

@elizabetheonoja-art

Problem Statement / Feature Objective

The dashboard schedules recurring Soroban contract executions (e.g., daily meter snapshots, weekly tariff recalculations) that must fire at precise wall-clock times regardless of the browser tab's throttling state. A high-resolution timer wheel must schedule up to 1,000 concurrent jobs with millisecond precision, using SharedArrayBuffer-backed cross-worker timekeeping to resist background-tab throttling. When the browser throttles setTimeout/setInterval to 1,000 ms in background tabs, the timer wheel compensates by running a dedicated Web Worker with a high-resolution loop and posting correction deltas to the main thread.

Technical Invariants & Bounds

  • Timer wheel: 1,024 slots, each slot representing 100 ms (total wheel span: 102.4 seconds). Jobs are hashed into slots by their next-fire-time modulo wheel span.
  • Maximum scheduled jobs: 1,000. Each job has a fire-at timestamp, interval (if recurring), and a handler key.
  • Precision requirement: execution within +/- 50 ms of the scheduled fire-at time.
  • Background-tab detection: document.hidden transitions trigger a recalibration event. The worker stores the last known performance.now() via SharedArrayBuffer (Int32Array) and the main thread writes a "drift" value each frame.
  • Fallback: if SharedArrayBuffer is unavailable (missing COOP/COEP headers), degrade to a polling interval of 200 ms on the main thread with a warning log.

Codebase Navigation Guide

  • src/services/timerWheel.ts - Main thread facing timer wheel API: schedule(job), cancel(jobId), tick(). Interfaces with the worker for timekeeping.
  • src/workers/timerWheel.worker.ts - Worker holding the actual wheel data structure and running the tick loop via Atomics.wait.
  • src/utils/sharedBuffer.ts - SharedArrayBuffer creation and atomic access helpers for drift correction.
  • src/hooks/useScheduledExecution.ts - React hook that wraps timerWheel for component-level scheduling, auto-cancels on unmount.
  • src/store/slices/scheduleSlice.ts - Tracks scheduled job metadata for UI display (next fire time, status, missed-fire count).
  • src/pages/SchedulerDashboard.tsx - Admin page showing all scheduled jobs, their status, and last execution time.

Implementation Blueprint

  1. Create SharedArrayBuffer in src/utils/sharedBuffer.ts: 4 KiB buffer with Int32Array views for "worker heartbeat", "main drift correction", and "command channel" (schedule/cancel/terminate commands).
  2. Implement the timer wheel in src/workers/timerWheel.worker.ts: allocate 1024 buckets (each an array of job references). The worker loop uses Atomics.wait(buffer, HEARTBEAT_INDEX, 0, 100) to sleep 100 ms per tick. On each tick, iterate the current slot, fire all due jobs, postMessage the results, advance the cursor.
  3. In src/services/timerWheel.ts, provide schedule(cb, fireAt, intervalMs) that writes a schedule command into the shared buffer and wakes the worker via Atomics.store + Atomics.notify.
  4. Implement background-drift compensation: the main thread runs a requestAnimationFrame loop; on each frame, if document.hidden === true, compute drift = expectedTime - performance.now() and write it into the shared buffer. The worker reads drift each tick and adjusts its cursor advance accordingly.
  5. The useScheduledExecution hook wraps timerWheel and ties cleanup to useEffect return. It also exposes a status object { nextFire, isMissed, drift }.
  6. scheduleSlice stores all active jobs and their metadata; the SchedulerDashboard renders a table with columns for job name, next fire time, interval, status, and a cancel button.

Metadata

Metadata

Assignees

Labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions