# Timers and time Two different needs hide under the word "timer", and danos keeps them apart: - **Reading the clock** — *what time is it?* A read of a free-running counter. - **Waiting** — *wake me in N milliseconds*, or *notify me when a deadline passes.* Both are answered by the **kernel**, because the kernel already owns a timer: it has to, to preempt tasks. The LAPIC heartbeat and the calibrated TSC that back all of this are built in [device-interrupts.md](device-interrupts.md); the scheduler's blocking and wait queues are in [scheduling.md](scheduling.md). This page is about the surface a ring-3 program actually uses, and one deliberate absence: **there is no user-space time service.** ## Why time is a syscall, not a service The tempting microkernel move is to put a timer *driver* in user space and have applications ask it for the time over IPC. For a **monotonic clock that is wrong** — reading `now()` should never cost an IPC round trip. The kernel is already holding the answer: it computes the current time every time it schedules, from the TSC, in a couple of instructions. Surfacing that as a system call is pure mechanism; routing it through a message to another process would be slower *and* redundant, and a device like the HPET (uncacheable MMIO reads) is a particularly bad thing to read on every `now()`. This is the same conclusion every serious system reaches: Linux and Zircon read the counter in the vDSO, L4 exposes a clock field in a shared kernel page, seL4 reads the cycle counter directly. None of them make a clock read an IPC. danos makes it a syscall. That "from the TSC" hides a portability question, because the TSC is only a valid clock when the CPU guarantees it is *invariant* and when every core's TSC is *synchronized*. danos checks both — the invariant-TSC CPUID bit (`0x80000007` EDX[8], set on Intel and AMD), and a cross-core "warp" check as the cores come up — and falls back to the HPET counter when either fails. So `now()` stays accurate on a real Intel box, a real AMD box, and inside a VM alike; only the source behind it differs. The mechanism is in [device-interrupts.md](device-interrupts.md). So the timer hardware lives in the kernel, and there is **no `hpet` driver and no time server** to consume. (An earlier HPET driver existed only to *demonstrate* the driver model; that role now lives in [drivers.md](drivers.md), as documentation.) The one place a user-space time service *is* justified — **wall-clock / calendar time** — is discussed at the end; it is deliberately not built yet. ## The three system calls Time and waiting are three entries in the small syscall table ([syscall.md](syscall.md)): - **`clock` (#23)** → monotonic nanoseconds since boot. It only moves forward. Not wall-clock: no date, no timezone. Backed by `architecture.nanos()` (TSC, scaled with a 128-bit intermediate so a long uptime can't overflow) — a few nanoseconds of resolution, and just an `rdtsc` plus a multiply. - **`sleep` (#3)** → block the caller for N milliseconds. The scheduler records a wake deadline and the tick sweep wakes it (`scheduler.sleep`). - **`timer_bind` (#31)** → arm a one-shot timer that, after N milliseconds, posts a **timer notification** to an IPC endpoint. Unlike `sleep` it does **not** block: a service can keep answering messages on the same endpoint while a deadline is pending. This is the timed wait that stop-sequence escalation, hello deadlines, and restart backoff are built from ([process-lifecycle.md](process-lifecycle.md), [device-manager.md](device-manager.md)). The kernel's own scheduling timer (the LAPIC, vector 32) is never exposed to user space; programs read the TSC through `clock` and get timed wakeups through `sleep`/`timer_bind`, both riding the scheduler tick. ## `runtime.time` — the generic interface Applications don't call the syscalls directly; they use `runtime.time` (`library/runtime/time.zig`), a thin `Instant`/`Duration` layer over them — an ergonomic front door, not new mechanism. ```zig const time = @import("runtime").time; const start = time.now(); // Instant — monotonic doWork(); const took = start.elapsed(); // Duration time.sleep(time.Duration.fromMillis(5)); // block ~5 ms // A deadline delivered as a notification, so a service keeps serving meanwhile: _ = time.after(endpoint, time.Duration.fromMillis(200)); ``` - `Duration` is nanoseconds under the hood, with `fromNanos/fromMicros/fromMillis/ fromSeconds` and `asNanos/asMillis`. `ceilMillis` rounds *up* to the kernel's millisecond granularity, so a sub-millisecond `sleep` never rounds down to zero and returns early. All arithmetic saturates rather than wraps. - `Instant` is a point on the monotonic clock: `since`, `elapsed`, `plus`, `reached` — built for deadline loops (`while (!deadline.reached()) …`). - `now()` / `monotonicNanos()` wrap `clock`. `available()` reports whether the clock is calibrated at all (the kernel returns 0 until the TSC frequency is known, so a caller that needs real time can treat 0 as "unavailable" rather than assume it advances). - `sleep(d)` wraps `sleep`; `spin(d)` busy-polls `now()` for the sub-millisecond delays the millisecond tick can't express; `after(endpoint, d)` wraps `timer_bind`. The raw wrappers (`system.clock`, `system.sleep`, `system.timerOnce`) stay in `library/runtime/system.zig`; `runtime.time` is the layer meant for everyday use. ## Wall-clock time (not built) Everything above is **monotonic**: elapsed time since boot, perfect for timeouts and measurement, useless for "what is the date?" Calendar time — a real-time clock, time zones, leap seconds — is genuinely a **user-space** concern, and it *is* the case a time service is for. It would be backed by an **RTC** driver (the CMOS real-time clock), not the HPET, and exposed as a `CLOCK_REALTIME`-style service alongside the monotonic syscall. It is deferred until something needs it; the monotonic clock the kernel already owns covers every current use. ## Verifying it `runtime.time`'s `Instant`/`Duration` arithmetic has unit tests that run on the host: ``` $ zig build test # includes library/runtime/time.zig ``` End to end, the proof the clock is real is that it *advances*: read `now()`, `sleep` a `Duration`, read `now()` again, and the second reading is later — the kernel's timer driving a ring-3 program with no service in between.