niso-sched
Generated from crates/niso-sched/CONTRACT.md when the site was built at 7aa23e2.
Purpose
Section titled “Purpose”HTML’s event loop for one agent: a FIFO of queued tasks, pull sources,
timers with HTML’s nesting and clamping, animation frame callbacks, the
microtask checkpoint’s bracket and a clock that is real or virtual. It
knows no subsystem by name. It does not own script, the document or
rendering; it calls them through LoopHost and the task hook.
niso’s document (niso-core’s Dom) runs on it: the event loop’s former
subsystem clauses are six core sources registered when a Dom is made
(websocket, modules, messages, fetch, crypto, rendering), and DomHost
implements LoopHost. SimpleHost is the second host, a complete
minimal one.
Public surface
Section titled “Public surface”- Rust:
Source(info,ready,run, and the provideddeadline,wants_frame,live,begin_run,begin_turn,held,replenish,settled),SourceInfo(name,task_source,class,wants_frames) andClass(Turn,Batch,Settle,Rendering).Scheduler:new(Clock),register_source,queue_task,tasks_pending,set_timerandclear_timer(script’s, HTML’s nesting),set_crate_timerandclear_crate_timer(niso-host’s),clear_timers,timer_due,nothing_scheduled,request_frame,cancel_frame,frame_requested,set_task_hook,begin_checkpoint,end_checkpoint,now,clock.Clock(real,virtual_time,now,advance,instant_at).LoopHost, what a host implements beyondniso_host::HostServices:scheduler,task_ended,update_rendering,park, and the providedcollect,closing,frame_due.- Over a
&dyn LoopHost:run,ready,needs_frame,next_timer,end_task,run_batch,run_frames. FRAME_MS,REPLENISH_WAIT,TaskHookFn.SimpleHost,Dispatch,Sink.testing(below) and, behind thecontract-testsfeature,contract(scheduler_suiteand its checks,no_alloc,source_suite).testingandSimpleHoststay in the normal build: the cdylib keeps nothing oftestingand 289 bytes ofSimpleHost’s drop glue (nmon the release libniso_app.so; all of niso-sched is 24 KB there), so a feature would not pay for itself.
- C: include/event-loop.h, its section of the composed niso_runtime.h
(
niso_dom_run,niso_dom_needs_frame,niso_dom_next_timer,niso_dom_set_task_hook, the wake hook). The functions stay in niso-capi, unchanged, until step 6c generates them. - Glue: glue/timers.js, the timer globals (setTimeout and its kin), composed into the glue in the monolith’s order until glue modules exist (step 6c).
Host requirements
Section titled “Host requirements”- A host keeps one
Schedulerper agent, registers its sources once, and implementsLoopHostwith each method borrowing the host only for its own duration: script runs between the calls and may reenter the host. task_endedis the host’s safe point (niso-host requirement 2). A host with no binding performs the checkpoint-end hooks there too, asSimpleHostdoes; a binding performs them in its task hook.- A crate’s task the host queues through
HostServices::queue_taskgoes toScheduler::queue_taskwrapped inniso_host::run_task; the scheduler runs what it is given as is. Crate timers go toset_crate_timer, which the scheduler runs withrun_taskitself. parkreturns None only when no source is live, and returns Some(true) once a source may be ready; a host whose wakes all go through itsWakercan park on it, asSimpleHostdoes.SimpleHostreturns None for a wake after which no source is live or ready.- A host’s own sources may find the host from a
CxwithHostServices::as_any, as the document’s do; a crate’s sources must not (niso-host requirement 8).
May assume from dependencies
Section titled “May assume from dependencies”niso-host’s contract: Cx, HostState (failures waiting for the run),
run_task, Waker.
How run(until) orders work
Section titled “How run(until) orders work”Each turn: if the host is closing, return; every source’s begin_turn;
then the first of
- the batch: the tasks queued when the turn began, each ended with
end_task; - a
Turnsource’s task, after which the next turn starts at once, unlessuntilhas passed and the run has run for a frame’s time: then theRenderingsources run and the run returns, as after a batch; - a
Batchsource’s task, which counts as the batch.
After a batch, every Turn and Batch source runs one task in rank order,
the Rendering sources run if a frame’s time passed since they last did,
and the run returns if until has passed and it has run for a frame’s
time. With no batch: if no timer is due and a source is held, return;
else if Settle sources are ready, one empty task ends for all of them,
then each is settled; else the Rendering sources run.
Then the next item is the earliest of the next timer, by (time,
sequence), the frame (frame_due: by default FRAME_MS after the oldest
request; the document says the first frame boundary after it, or a frame
after the last update while animations run) and the earliest source
deadline after now; on a tie a timer goes first, then the frame, then the
deadline. If it is later than now: after a batch, the next turn; else once per wait each source may
replenish (if one did, return when the item is after until, else next
turn); else if a source is live, park until the item or until;
else, if the item is after until, return, else skip the clock ahead. A
due timer runs (script’s at its nesting level, a crate’s through
run_task) and ends its task; a due frame is update_rendering at that time;
at a due deadline the next turn starts, where the source is ready.
Rank order: networking, embedder, posted message, then the rest of
TaskSource; by name within one.
Guarantees
Section titled “Guarantees”needs_frameholds exactly when a frame is due at some time (frame_due), a source wants a frame or a run has work (ready);next_timeris now while a run has work, else exactlymax(now, min(timers ∪ source deadlines)): never before now, now exactly when a run has work or a timer is due, infinite when nothing is scheduled. A run makes progress exactly whennext_timeris now or a frame is due. contract::agreement, over random interleavings of tasks, timers, frames, runs and six sources (turn, budgeted, batch with held messages, settle, rendering, deadlines) on SimpleHost; and on the document, over its queued tasks, crate timers, frames, module messages and window messages (niso-core event_loop.rs tests).- Queued tasks run in FIFO order a batch at a time; a task queued by a task of the batch runs in a later batch. contract::fifo.
- Turn and batch sources run in rank order, one task each per turn after
a batch. contract::fairness. With no batch, turn sources drain before
a due timer runs. contract::turns_drain_before_timers. A turn source
that always has work, or a chain of tasks that each queue the next,
cannot keep a run from returning once
untilhas passed and it ran for a frame’s time. contract::turns_are_bounded, contract::batches_are_bounded. - Timers fire in (due time, sequence) order; a cleared timer never fires, even when cleared by an earlier task of the same turn; a due timer runs after each batch source’s turn. contract::timers. Script’s timers set more than five levels deep wait at least 4 ms; crate timers are never clamped. contract::nesting. A frame goes before a timer only when it is due strictly earlier. contract::frame_and_timer_ties. An animation frame callback requested in a frame runs in the next frame. contract::frames_requested_in_a_frame.
- After every task (queued, a source’s, a timer’s, an animation frame
callback) the task hook runs once, then
task_ended. contract::checkpoint. Settle sources share one empty task. contract::settle. The rendering steps run after the batch. contract::rendering_after_a_batch. - With a virtual clock and no live source,
run(until)moves time only to the next due item and never waits. contract::virtual_time. With a live source it parks until a wake or the item. contract::live_wait. When the last live source stops being live, the parked run returns. contract::live_ends. When a source’s deadline is the next item, the clock moves (or the run parks) to it and the source runs. contract::deadlines. A budgeted source takes per run what it had when the run began, plus one allotment before the clock moves. contract::budgets. While a source isheldand no timer is due, the run returns, settle and rendering included; a due timer keeps it going. contract::held. ready,needs_frameandnext_timerallocate nothing and run no script (for sources that keep their own obligations), after each ran once (a platform may make a lock on first use, as macOS does). contract::no_alloc.
Obligations on sources
Section titled “Obligations on sources”ready is true iff run would return Ok(true), budgets and held
aside; a source woken from another thread is ready before the wake is
delivered; a time-based readiness is reported as a deadline, and the
source is ready once the clock reaches it; live is true while another
thread may still make it ready, and a source that stops being live wakes
the host.
contract::source_suite checks the first of these (not ready, then ready
and runs, then not ready once drained). It runs on the fakes and on the
document’s modules and messages sources (niso-core event_loop.rs tests).
The document’s other four do not run it yet, and get it when they leave
the core: websocket in step 7 (niso-websocket; it is ready while a watcher
fired, which a read may find had nothing), fetch in step 11 (niso-fetch),
crypto in step 12 (niso-crypto). The rendering source stays in the core;
it runs whatever is pending, and a media query list that did not change
runs nothing, so it gets the suite in step 18, when media’s events move to
niso-media’s rendering step and what is left is a plain queue. The
wake-on-not-live and deadline obligations have no generic check; the
fakes keep them and live_ends and deadlines check the loop’s side.
Differences from architecture.md 8.2
Section titled “Differences from architecture.md 8.2”Classhas four kinds, not two, andSourcehasbegin_turn,heldandsettled: the loop’s order before the move (messages yield to timers like a batch, finished fetches and crypto results share one empty task, worker loads are answered every turn) needs them to stay the same.Source::begin_run(until)is new: budgeted sources (native modules) take per run what they had when it began, and the messages source learns the run’suntil.- “
readyiffrunreturns Ok(true)” is relaxed: a budgeted source over its budget, and a held one, are ready but run nothing until the next run (or a replenish). next_timerdoes not say now for a frame that is due:needs_framereports it, and the host runs on its next display frame. 8.2’s “now exactly whenruncan make progress” holds for everything but a due frame (guarantee 1).- 8.2’s
set_checkpoint(fn)andset_render(fn)are replaced: the binding’s checkpoint is the task hook (set_task_hook, the C hook as before) and the host’sLoopHost::task_ended; rendering isLoopHost::update_renderingandframe_due. - Sources register on the
Scheduler(register_source), not through niso-host’sRegistrar: the document’s are core-internal, and no crate has a source yet. The Registrar route comes with the first crate that does (step 7). - 8.2’s obligation that
live“becomes false within one turn after the last such thread is done” is replaced by: a source that stops being live wakes the host. A turn is not a time a parked host would ever reach. queue_tasktakes noTaskSource(there is one FIFO), and script’s and crates’ timers have separate calls, as their ids are separate spaces.- The real-time wait is the host’s
park: the document’s iswait_network’s socket poll until WebSocket leaves the core (step 7). runcalled from inside a task (guarantee 8 of 8.2) is not refused yet; a nestedDom::runkeeps its own run state, as before.- Guarantee 7 is checked on SimpleHost; the document’s sources take locks
in
readyas before. testing::ScriptedSourceis not “ready at given virtual times”: a test adds its tasks, and it has one deadline at a time, at which it becomes ready and polls.
Two changes to the loop’s behaviour came with the step’s review, beyond the move: a turn source is now bounded per run like a batch (3), and a source’s deadline is an item the run moves the clock or parks to (6). The document’s only deadline is an unwatched WebSocket’s poll a frame ahead, so a run with one now parks at most a frame at a time.
Threads
Section titled “Threads”Not Send: a Scheduler and its host live on the agent’s thread. The host’s
Waker is what other threads touch; sources are Sync statics whose state
lives with the host.
Performance and memory
Section titled “Performance and memory”The checks borrow the source list once per call and iterate it (six
sources in the document) with one indirect call each and no allocation;
needs_frame asks wants_frame only of the sources whose SourceInfo
says wants_frames (none of the document’s). Queuing a task boxes its
closure once. needs_frame and next_timer costs before and after the
move are in the step’s metrics.
testing::ScriptedSource (tasks a test adds; optionally budgeted, live,
held, wanting frames or with a deadline; any class), testing::ThreadSource
(a real thread delivers after a delay, or gives up, then wakes the host) and
testing::Recorder (an ordered log). Their constructors leak, since sources
are &'static, so Miri runs the tests with -Zmiri-ignore-leaks:
MIRIFLAGS=-Zmiri-ignore-leaks cargo +nightly miri test -p niso-sched.
ScriptedSource’s checks are atomics only, so they never allocate or lock.
Standalone use
Section titled “Standalone use”SimpleHost::new(Clock::virtual_time()), install a crate, register_source,
set_sink for events, then run(until). It passes niso-host’s
host_suite, as FakeHost and the document do.