Cone Language Reference

Time

On this page

Note: Not built yet: time zones (a DateTime is always UTC, and a written offset is only read, into UTC); dates written by a pattern or in a person's language (only RFC 3339 and HTTP's dates are written and read); repeating timers; and checked or saturating Duration arithmetic (overflow wraps).

Four library packages deal with time. time holds the values: how long (Duration), when on the machine's steady clock (Instant), when on the calendar's clock (Timestamp), and a Clock a program reads instants from, real or synthetic. calendar reads a Timestamp as years, months, days and hours, and writes and reads dates as text. timewheel is a set of timers that costs almost nothing to arm and to cancel. iocore, the I/O completion loop, uses all three to give an operation a deadline and a program a timer.

What runs where: calendar and timewheel call nothing of the operating system's. time calls it for one thing, Instant.now(): Windows' QueryPerformanceCounter, or on Linux clock_gettime, chosen when the program is compiled. iocore runs only on Windows, since the loop is an I/O completion port.

Duration#

A Duration is a signed length of time, a count of nanoseconds in an i64: about 292 years either way. It is made with a constructor that names its unit, Duration.fromNanos, fromMicros, fromMillis, fromSeconds, fromMinutes or fromHours, each taking an i64 count, or Duration.zero(). It is read back with d.nanos(), micros(), millis(), seconds(), minutes() and hours(), each the whole count of its unit with the fraction dropped toward zero (1.999 seconds is 1 second, and -1.999 seconds is -1), or as d.secondsF64(), seconds with the fraction as an f64.

Durations add and subtract (d + e, d - e, -d, and +=, -=), multiply and divide by an i64 (d * 4, d / 4, divided toward zero), and compare with == != < <= > >=. d.isZero(), d.isNegative() and d.abs() do what they say. A negative duration is a value like any other: one instant less a later one is negative.

new Duration() is zero. It is the spelling for a field's or a parameter's default, since a default must be a constant and Duration.zero() is a call.

Overflow is whatever i64 arithmetic does, which is to wrap, with no check: Duration.fromHours(n) for an n past about 2.5 million, a sum past 292 years, and the abs of the most negative duration all wrap round silently. Dividing a duration by zero is as undefined as dividing an i64 by zero.

import stdio use *;
import time use Duration, Instant, Timestamp;

// A field's default must be a constant, so a Duration default is 'new Duration()'
struct Options {
  pub timeout Duration = new Duration();
  pub retries i64 = 3i64;
}

fn main() {
  imm d = Duration.fromMillis(1999);
  printStr("whole seconds: ");
  printInt(d.seconds());                       // 1
  printStr("\nwith the fraction: ");
  printFloat(d.secondsF64());                  // 1.999
  printStr("\nnegated, whole seconds: ");
  printInt((-d).seconds());                    // -1, toward zero
  printStr("\n");

  imm a = Duration.fromSeconds(90);
  imm b = Duration.fromMinutes(1);
  printStr("90 s - 1 min = ");
  printInt((a - b).seconds());                 // 30
  printStr(" s; times 4 = ");
  printInt(((a - b) * 4).seconds());           // 120
  printStr(" s; divided by 4 = ");
  printInt(((a - b) / 4).millis());            // 7500
  printStr(" ms\n");
  printStr(if a > b {"90 s is longer than 1 min\n";} else {"?\n";});
  printStr(if (b - a).isNegative() {"1 min - 90 s is negative\n";} else {"?\n";});
  printInt((b - a).abs().seconds());           // 30
  printStr(" s is its abs\n");

  mut total = Duration.zero();
  total += Duration.fromMillis(250);
  total += Duration.fromMicros(750);
  printInt(total.micros());                    // 250750
  printStr(" us in all\n");

  imm o = new Options();
  printStr(if o.timeout.isZero() {"default timeout is zero\n";} else {"?\n";});
  imm p = new Options(timeout: Duration.fromSeconds(2));
  printInt(p.timeout.millis());
  printStr(" ms\n");
}

This prints:

whole seconds: 1
with the fraction: 1.999
negated, whole seconds: -1
90 s - 1 min = 30 s; times 4 = 120 s; divided by 4 = 7500 ms
90 s is longer than 1 min
1 min - 90 s is negative
30 s is its abs
250750 us in all
default timeout is zero
2000 ms

Instant#

An Instant is a moment of the machine's steady clock. The clock never goes back and is not moved when the calendar clock is set, so it is the clock to measure with and to set deadlines by. One Instant means nothing by itself, since its origin is arbitrary (near the machine's start); two taken in one run of a program give the time between them.

  • Instant.now() reads the steady clock. i.elapsed() is Instant.now() - i, a Duration.
  • i + d and i - d are an Instant a Duration later or earlier; i - j is the Duration from j to i, negative if j is later. Instants compare with == != < <= > >=, earlier being less.
  • i.nanos() and Instant.fromNanos(n) are the count of nanoseconds from the clock's origin, for a program that keeps a count of its own. Only differences between them mean anything. new Instant() is the origin.

Deadlines in Cone's I/O are Instants, never durations: to wait five seconds from now, add the Duration to Instant.now(). thread.sleep and the timed waits of sync take a Duration (Concurrency).

Timestamp#

A Timestamp is a moment of Coordinated Universal Time: a signed count of nanoseconds since 1970-01-01 00:00:00 UTC, as Unix time counts, negative before 1970. Its range is 1677 to 2262. Leap seconds are ignored, as Unix time ignores them: every day is 86400 seconds. It is read from the calendar clock, which may be set back or forward while a program runs, so measure a duration with Instants, never with two Timestamps.

  • Timestamp.now() is UTC now. Timestamp.fromUnixSeconds(s) and Timestamp.fromUnixNanos(n) make one from a count, as a file's time and libc.time() count them; t.unixSeconds() and t.unixNanos() read it back. unixSeconds rounds down, toward the past, as Unix time does: half a second before 1970 is in second -1. new Timestamp() is the epoch.
  • t + d and t - d are a Timestamp a Duration later or earlier; t - u is the Duration from u to t. They compare like instants.

A Timestamp has no years, months or days, and no text form; those are the calendar package's, below.

The package is called time, and libc's time(), the C library's whole seconds since 1970, has the same name. A module that imports the package and also folds all of libc's names in collides on it; leave libc's out with import libc use * but time;, and write libc.time(null) where it is wanted.

Clock, real and synthetic#

A Clock is where a program that measures time reads it, so that it can be handed a synthetic clock instead of the real one: in a test, a replay, or a simulation run faster than the wall. Clock.real() reads Instant.now(). Clock.synth() starts at Instant.fromNanos(0) and moves only when told to, by exactly what it is told: c.advance(d). c.now() is the clock's instant, and the public field c.synthetic says which kind it is. advance asserts that the clock is synthetic and that d is not negative: a clock never goes back.

Code written to take a Clock runs on time a test chooses. The same loop, timed by a synthetic clock advanced by a frame at a time, takes 960 milliseconds every run:

import stdio use *;
import thread;
import time use Duration, Instant, Timestamp, Clock;

// Code that times its work by whatever clock it is handed. A frame loop
// advances a synthetic clock by its frame time; a real one needs no help.
fn run(clock &mut Clock, frames i64) Duration {
  imm start = clock.now();
  mut i = 0i64;
  while i < frames {
    if clock.synthetic { clock.advance(Duration.fromMillis(16)); }
    i = i + 1i64;
  }
  clock.now() - start;
}

fn main() {
  mut synth = Clock.synth();
  printInt(synth.now().nanos());                    // 0: a synthetic clock starts at its origin
  printStr(" ns at the start\n");
  printInt(run(&mut synth, 60).millis());           // 960, every run
  printStr(" ms for 60 synthetic frames\n");
  synth.advance(Duration.fromSeconds(1));
  printInt(synth.now().nanos());                    // 1960000000
  printStr(" ns now\n");

  // The real clock: only comparisons are deterministic
  mut real = Clock.real();
  imm start = Instant.now();
  thread.sleep(Duration.fromMillis(20));
  imm took = start.elapsed();
  printStr(if took >= Duration.fromMillis(20) {"slept at least 20 ms\n";} else {"too short\n";});
  printStr(if took < Duration.fromSeconds(5) {"and under 5 s\n";} else {"too long\n";});
  imm r = real.now();
  printStr(if r >= start {"the real clock has not gone back\n";} else {"back?\n";});

  // A Timestamp is the calendar's clock, UTC since 1970
  imm now = Timestamp.now();
  printStr(if now.unixSeconds() > 1767225600 {"it is after 2026-01-01\n";} else {"?\n";});
}

This prints:

0 ns at the start
960 ms for 60 synthetic frames
1960000000 ns now
slept at least 20 ms
and under 5 s
the real clock has not gone back
it is after 2026-01-01

calendar: civil dates and times#

The calendar package reads a Timestamp the way a calendar does. It uses the proleptic Gregorian calendar, its leap-year rule run back before 1582 and on past 9999, with years counted astronomically (year 0 is 1 BC, year -1 is 2 BC). Every day is 86400 seconds, with no leap second. All of it is UTC. A part out of its range is never wrapped or carried into a value: a constructor answers an Option, None for a date or a time that does not exist, and reading a text answers a Result whose error says what was wrong (TimeError).

calendar.isLeapYear(y) and calendar.daysInMonth(y, m) (0 for a month not 1 to 12) are the rules for a year or month alone.

Date and Weekday#

A Date is a year, a month and a day of the month, made only when the day exists. Date.fromYmd(y, m, d) answers Some date or None: Date.fromYmd(2026, 2, 29) is None. Date.fromUnixDays(n) is the date n days after 1970-01-01, and new Date() is 1970-01-01 itself.

  • d.year() (an i32), d.month() and d.day() (i8s), d.dayOfYear() (1 to 365 or 366, an i16), d.unixDays() (the days since 1970-01-01, negative before it).
  • d.weekday() is a Weekday, the enum Weekday.Monday to Weekday.Sunday; d.isoWeekday() is ISO 8601's number for it, 1 (Monday) to 7.
  • d.isLeapYear(), d.daysInMonth(), d.daysInYear().
  • d.addDays(n) and d.subDays(n) are the date n days later or earlier; d.daysSince(e) is the days from e to d, negative when e is later. Dates compare with == != < <= > >=, earlier being less.

A year that arithmetic carries past the range of an i32 (about two billion years) wraps.

TimeOfDay#

A TimeOfDay is an hour (0 to 23), a minute and a second (0 to 59) and a nanosecond (0 to 999999999). TimeOfDay.fromHms(h, m, s) and TimeOfDay.fromHms(h, m, s, ns) answer Some or None when a part is out of range, so fromHms(24, 0, 0) is None. t.hour(), minute() and second() are i8s, t.nanosecond() an i32, and t.sinceMidnight() a Duration. They compare like dates; new TimeOfDay() is midnight.

DateTime#

A DateTime is a Date and a TimeOfDay together, in UTC. It is the calendar's reading of a Timestamp, over any year a Date holds, where a Timestamp holds only 1677 to 2262.

  • DateTime.fromTimestamp(t): every Timestamp has one. dt.toTimestamp() answers Some timestamp, or None outside 1677-09-21T00:12:43.145224192Z to 2262-04-11T23:47:16.854775807Z.
  • DateTime.fromYmdHms(y, mo, d, h, mi, s), with an optional seventh argument of nanoseconds, answers Some or None; DateTime.fromParts(date, tod) joins two values already made.
  • dt.date() and dt.time() take it apart.
  • dt + d and dt - d are a Duration later or earlier, carried across days, months and years; dt - e is the Duration from e to dt (it wraps past 292 years, as a Duration does). They compare as dates do; new DateTime() is the epoch.

Writing and reading RFC 3339#

Each of Date, TimeOfDay and DateTime writes itself with rfc3339(), a String of the collections package, and reads what it writes with its own parseRfc3339(text), a Result. A Timestamp is the time package's type, so it is written and read by two functions of this one: calendar.rfc3339(t) and calendar.parseRfc3339(text).

  • A date is 2026-10-03, a time 14:05:09, a date-time 2026-10-03T14:05:09Z, always UTC.
  • A fraction of a second is written only when there is one, in as few digits as hold it exactly: 14:05:09.5, 14:05:09.000000001. It is read in one to nine digits; more than nine is refused, not rounded.
  • A date-time is read with its offset, which RFC 3339 requires: Z, or +hh:mm or -hh:mm, converted to UTC. The T and Z may be lower case; a space for the T is refused, as is a date-time with no offset.
  • A second of 60, a leap second, is refused, since a Timestamp does not count them.
  • RFC 3339's years are 0000 to 9999. A year outside them is written and read in ISO 8601's expanded form, a sign and at least six digits (+010000-01-01, -000044-03-15), so that every DateTime is written and read back. calendar.parseRfc3339 refuses a text outside a Timestamp's range, and says the range.

HTTP dates#

HTTP's dates (RFC 9110, section 5.6.7) are written and read for a Timestamp only. calendar.httpDate(t) writes IMF-fixdate, Sun, 06 Nov 1994 08:49:37 GMT, dropping the fraction of a second toward the past. calendar.parseHttpDate(text) reads any of the three forms HTTP allows: that one, RFC 850's (Sunday, 06-Nov-94 08:49:37 GMT) and asctime's (Sun Nov 6 08:49:37 1994). Names are case-sensitive, as HTTP-date is. The day's name must be the date's: Mon, 06 Nov 1994 is refused, since 6 November 1994 was a Sunday. RFC 850's two-digit year is read as the latest year ending in those digits that is not more than 50 years after now. parseHttpDate takes now from Timestamp.now(); calendar.parseHttpDateAsOf(text, now) takes it as a Timestamp, for a test or a replay. A leap second is refused.

TimeError#

A text that is not a date or a time is refused, never read as some other moment: the whole text must be the format, and nothing else, every part in range. The refusal is the Error of the Result, holding a TimeError: the public fields at, the byte it stopped at counted from 0, and reason, a String; and e.message(), the line to show a person.

import stdio use *;
import collections use String;
import time use Duration, Timestamp;
import calendar use Date, Weekday, TimeOfDay, DateTime;

// A Date.fromYmd answers an Option: None for a day that does not exist
fn show(y i64, m i64, d i64) {
  match Date.fromYmd(y, m, d) {
    case imm date Some {
      printStr(date.rfc3339().view());
      printStr(" is day ");
      printInt(i64.from(date.dayOfYear()));
      printStr(" of ");
      printInt(i64.from(date.daysInYear()));
      printStr(", ISO weekday ");
      printInt(i64.from(date.isoWeekday()));
      printStr(if date.weekday() == Weekday.Saturday or date.weekday() == Weekday.Sunday {", a weekend\n";} else {", a weekday\n";});
    }
    case is None { printStr("no such date\n"); }
  }
}

// Reading an RFC 3339 date-time, converted to UTC, or why not
fn read(text &str) {
  printStr(text);
  printStr("  ->  ");
  match DateTime.parseRfc3339(text) {
    case imm ok Ok { printStr(ok.rfc3339().view()); }
    case imm e Error { printStr(e.message().view()); }
  }
  printStr("\n");
}

fn main() {
  show(2026, 10, 3);
  show(2026, 2, 29);
  show(2024, 2, 29);

  // Date arithmetic is by days
  match Date.fromYmd(2026, 12, 25) {
    case imm xmas Some {
      imm base = xmas.subDays(100);
      printStr(base.rfc3339().view());
      printStr(" + 100 days = ");
      printStr(base.addDays(100).rfc3339().view());
      printStr("; days between: ");
      printInt(xmas.daysSince(base));
      printStr("\n");
    }
    case is None {}
  }

  // A Timestamp read as a DateTime, and back
  imm ts = Timestamp.fromUnixNanos(1759500309250000000);
  imm dt = DateTime.fromTimestamp(ts);
  printStr(dt.rfc3339().view());                    // 2025-10-03T14:05:09.25Z
  printStr("\n");
  printStr(calendar.rfc3339(ts).view());            // the same, straight from the Timestamp
  printStr("\n");
  printStr(calendar.httpDate(ts).view());           // Fri, 03 Oct 2025 14:05:09 GMT
  printStr("\n");
  printInt(i64.from(dt.date().year()));
  printStr("\n");
  imm later = dt + Duration.fromHours(36);          // carried across the day
  printStr(later.rfc3339().view());
  printStr("\n");
  printInt((later - dt).hours());
  printStr(" hours between\n");
  match dt.toTimestamp() {
    case imm t Some { printStr(if t == ts {"round trips to the Timestamp\n";} else {"?\n";}); }
    case is None { printStr("outside a Timestamp\n"); }
  }

  // A year outside a Timestamp's range is still a DateTime
  match DateTime.fromYmdHms(3000, 1, 1, 0, 0, 0) {
    case imm far Some {
      printStr(far.rfc3339().view());
      match far.toTimestamp() {
        case is Some { printStr(" has a Timestamp\n"); }
        case is None { printStr(" has no Timestamp\n"); }
      }
    }
    case is None {}
  }

  // A time of day
  match TimeOfDay.fromHms(14, 5, 9, 500000000) {
    case imm t Some { printStr(t.rfc3339().view()); printStr("\n"); }
    case is None {}
  }
  match TimeOfDay.fromHms(24, 0, 0) {
    case is Some { printStr("24:00:00 exists\n"); }
    case is None { printStr("24:00:00 does not\n"); }
  }

  // Reading: offsets are converted, malformed text is refused with where and why
  read("2026-10-03T19:35:09.25+05:30");
  read("2026-10-03t19:35:09z");
  read("2026-10-03 19:35:09Z");
  read("2026-13-03T19:35:09Z");
  read("2026-10-03T19:35:60Z");
  read("2026-10-03T19:35:09");

  // HTTP's dates, in all three forms
  match calendar.parseHttpDate("Sunday, 06-Nov-94 08:49:37 GMT") {
    case imm ok Ok { printStr(calendar.httpDate(ok).view()); printStr("\n"); }
    case imm e Error { printStr(e.message().view()); printStr("\n"); }
  }
  match calendar.parseHttpDate("Mon, 06 Nov 1994 08:49:37 GMT") {
    case is Ok { printStr("read\n"); }
    case imm e Error { printStr(e.message().view()); printStr("\n"); }
  }
}

This prints:

2026-10-03 is day 276 of 365, ISO weekday 6, a weekend
no such date
2024-02-29 is day 60 of 366, ISO weekday 4, a weekday
2026-09-16 + 100 days = 2026-12-25; days between: 100
2025-10-03T14:05:09.25Z
2025-10-03T14:05:09.25Z
Fri, 03 Oct 2025 14:05:09 GMT
2025
2025-10-05T02:05:09.25Z
36 hours between
round trips to the Timestamp
3000-01-01T00:00:00Z has no Timestamp
14:05:09.5
24:00:00 does not
2026-10-03T19:35:09.25+05:30  ->  2026-10-03T14:05:09.25Z
2026-10-03t19:35:09z  ->  2026-10-03T19:35:09Z
2026-10-03 19:35:09Z  ->  at byte 10: a space between the date and the time: RFC 3339 writes 'T'
2026-13-03T19:35:09Z  ->  at byte 5: the month is 13, outside 01 to 12
2026-10-03T19:35:60Z  ->  at byte 17: the second is 60, a leap second, which this package does not count (a Timestamp ignores leap seconds)
2026-10-03T19:35:09  ->  at byte 19: the date-time has no offset: RFC 3339 ends it with 'Z' or '+hh:mm' or '-hh:mm'
Sun, 06 Nov 1994 08:49:37 GMT
at byte 0: 1994-11-06 is a Sunday, not a Monday

timewheel: timers#

A Wheel is a set of timers for a program that arms a great many and fires almost none: every connection has an idle timeout, every request a deadline, and nearly all are cancelled because what they guard happened in time. Arming and cancelling a timeout cost the same however many are pending, and a timer may fire up to a tick late (a tick is one millisecond, timewheel.tick() as a Duration, timewheel.tickNanos as a count) but never early. A Wheel is pure data: it has no thread, waits for nothing and calls no operating system. Whoever owns it says what time it is, with a Clock (a synthetic one in a test) or Instant.now(), and waits for the time itself; the iocore loop owns one and does exactly that. A Wheel is Sendable: it may be made on one thread and handed to another.

Arming and cancelling#

Wheel.make(now) is an empty wheel whose time starts at now, an Instant. Deadlines are absolute instants, never durations: a caller with “5 seconds” adds it to now. Each timer carries a u64 token, plain data the wheel hands back and never reads; what it means (an operation to cancel, a message to send) is the caller's.

  • w.insert(at, token) arms a timer for the instant at and answers a TimerId. A timer a tick or more away is a timeout in the wheel: it fires at the first tick at or after its deadline. A deadline less than a tick after the wheel's time, or already past, is put in the precise set by itself, and fires on time, to the nanosecond the loop wakes. So one call serves both kinds: what is far off can be late by a tick, what is near cannot.
  • w.insertPrecise(at, token) keeps a far deadline precise too, for a timer whose point is being on time (frame pacing, a fixed step). The precise set is small and costs more to arm and cancel than a timeout does.
  • w.cancel(id) answers true if the timer was pending and now never fires, and false, doing nothing, if it has fired or been cancelled already.
  • w.isPending(id) and w.isPrecise(id) ask about a timer; w.len() and w.isEmpty() count the pending ones.

A TimerId names a timer, not just a place: the id of a timer that fired or was cancelled never names a timer armed later, so cancelling it finds nothing. TimerId.none() names no timer (cancelling it does nothing), id.isNone() asks, and ids compare with == and !=.

Driving the wheel#

The program that owns the wheel is a loop: wait until the wheel needs to be looked at, then ask what is due.

  • w.nextExpiry() is Some the instant the loop must next wake at, or None with nothing armed. It is never later than the timer it is for, but may be earlier: a far timer costs a few wakes in its life where it moves down a level and nothing fires. w.nextPrecise() is the earliest precise deadline alone, and w.nextTimeout() the next tick the wheel needs; nextExpiry is the earlier of the two.
  • w.expired(now, out) moves the wheel's time on to now and appends to out, a List[Expiry], every timer due, answering how many. Each is delivered exactly once, and a delivered timer is gone. What one call delivers comes in deadline order, ties in the order armed. An Expiry has the public fields id, token and deadline (the instant it was armed for). Time does not go back: an instant before the wheel's time is taken as the wheel's time, and w.now() is the latest instant expired was given.
  • w.stats() counts what has happened: pending, armed, cancelled, fired and wakes, among others (a Stats with public fields).
import stdio use *;
import time use Duration, Instant, Clock;
import collections use List;
import timewheel use *;

// The wheel's next wake, in microseconds on the clock, or -1 for none
fn nextUs(w &Wheel) i64 {
  mut r = -1i64;
  match w.nextExpiry() {
    case imm s Some { r = s.nanos() / 1000i64; }
    case is None[Instant] {}
  }
  r;
}

fn main() {
  mut clock = Clock.synth();
  mut w = Wheel.make(clock.now());
  mut out = new List[Expiry];

  // Three timeouts and a deadline under a tick away
  imm idle = w.insert(clock.now() + Duration.fromSeconds(30), 1u64);       // an idle timeout
  imm req = w.insert(clock.now() + Duration.fromMillis(5), 2u64);          // a request's deadline
  imm odd = w.insert(clock.now() + Duration.fromMicros(2500), 3u64);       // 2.5 ms
  imm soon = w.insert(clock.now() + Duration.fromMicros(400), 4u64);       // under a tick
  printStr(if w.isPrecise(soon) {"400 us off: precise\n";} else {"?\n";});
  printStr(if w.isPrecise(odd) {"?\n";} else {"2.5 ms off: a timeout in the wheel\n";});
  printInt(i64.from(w.len()));
  printStr(" pending; next wake at ");
  printInt(nextUs(&w));
  printStr(" us\n");

  // The request finished in time: cancel its deadline
  printStr(if w.cancel(req) {"cancelled the request's deadline\n";} else {"?\n";});
  printStr(if w.cancel(req) {"?\n";} else {"cancelling again does nothing\n";});

  // The loop: move the clock to the next wake, then ask what is due
  while nextUs(&w) >= 0i64 and nextUs(&w) < 100000i64 {
    imm at = nextUs(&w);
    clock.advance(Duration.fromMicros(at) - Duration.fromNanos(clock.now().nanos()));
    out.clear();
    imm n = w.expired(clock.now(), &mut out);
    printStr("at ");
    printInt(at);
    printStr(" us: ");
    printInt(i64.from(n));
    printStr(" due");
    if n > 0usize {
      printStr(", token ");
      printUInt(out[0usize].token);
    }
    printStr("\n");
  }
  printStr(if w.isPending(idle) {"the 30 s idle timeout is still pending\n";} else {"?\n";});
  printStr(if w.cancel(idle) {"and is cancelled\n";} else {"?\n";});
  printInt(i64.from(w.len()));
  printStr(" pending; ");
  printInt(i64.from(w.stats().fired));
  printStr(" fired, ");
  printInt(i64.from(w.stats().cancelled));
  printStr(" cancelled\n");
}

This prints:

400 us off: precise
2.5 ms off: a timeout in the wheel
4 pending; next wake at 400 us
cancelled the request's deadline
cancelling again does nothing
at 400 us: 1 due, token 4
at 3000 us: 1 due, token 3
the 30 s idle timeout is still pending
and is cancelled
0 pending; 2 fired, 2 cancelled

The 2.5 millisecond timeout fires at 3000 microseconds, the first tick at or after its deadline; the 400 microsecond one fires at 400, on time.

Lanes and slop#

A timer goes in one of 8 lanes (timewheel.lanes); insert uses lane 0, and w.insertIn(lane, at, token) names one. A lane's slop is how much later than its tick its timers may fire, so that timers near one another fire in one wake: each is rounded up to the next multiple of the slop on the wheel's grid. A background lane's idle timeouts can take a generous slop; a lane serving frames takes none. w.setSlop(lane, d) sets it (whole ticks; under two ticks is none, and every lane starts with none) and w.slopOf(lane) reads it. Changing a lane's slop moves no timer already armed. Slop never makes a timer precise or early, and the precise set has none. A lane out of range asserts.

import stdio use *;
import time use Duration, Instant, Clock;
import collections use List;
import timewheel use *;

fn main() {
  mut clock = Clock.synth();
  mut w = Wheel.make(clock.now());
  mut out = new List[Expiry];
  imm t0 = clock.now();

  // Lane 1 is for background idle timeouts: they may fire up to 10 ms late
  w.setSlop(1u32, Duration.fromMillis(10));
  w.insertIn(1u32, t0 + Duration.fromMillis(1), 1u64);
  w.insertIn(1u32, t0 + Duration.fromMillis(3), 2u64);
  w.insertIn(1u32, t0 + Duration.fromMicros(9900), 3u64);
  w.insert(t0 + Duration.fromMillis(4), 100u64);                    // lane 0: no slop
  // A precise timer an hour off stays precise
  imm frame = w.insertPrecise(t0 + Duration.fromSeconds(3600), 200u64);
  printStr(if w.isPrecise(frame) {"the hour-off timer is precise\n";} else {"?\n";});

  // Wake at each next expiry and say what fired
  mut wakes = 0i64;
  while wakes < 3i64 {
    match w.nextExpiry() {
      case imm s Some {
        clock.advance(s - clock.now());
        out.clear();
        imm n = w.expired(clock.now(), &mut out);
        printStr("at ");
        printInt(clock.now().nanos() / 1000000i64);
        printStr(" ms: ");
        printInt(i64.from(n));
        printStr(" fired:");
        mut k = 0usize;
        while k < out.len() {
          printStr(" ");
          printUInt(out[k].token);
          k = k + 1usize;
        }
        printStr("\n");
      }
      case is None[Instant] {}
    }
    wakes = wakes + 1i64;
  }
  printInt(i64.from(w.len()));
  printStr(" still pending\n");
}

This prints:

the hour-off timer is precise
at 4 ms: 1 fired: 100
at 10 ms: 3 fired: 1 2 3
at 3600000 ms: 1 fired: 200
0 still pending

Lane 0's timer fires at its own tick, 4 ms; lane 1's three, due at 1, 3 and 9.9 ms, fire together in one wake at 10 ms; and the precise timer an hour off fires exactly at the hour.

Timers, deadlines and cancelling in I/O#

The iocore loop (I/O and Concurrency) owns a timewheel.Wheel and gives a program three time-related things. All its times are Instants; forever() is an instant that never comes. The loop runs on Windows only. Every operation, a timer included, answers exactly once, with a Completion whose c.outcome() is Outcome.Done(n), Outcome.Failed(code), Outcome.Cancelled or Outcome.TimedOut; a buffer lent to the operation comes back in c.buf.

A timer#

lp.after(at, to) is a timer: it answers Done at the instant at, to the target to (Target.Here(token) is the loop's own poll), unless it is cancelled first. It answers an OpId. lp.afterIn(at, to, group) puts the timer in a cancel group.

lp.poll(until, out) is the one place the loop waits. It appends the answers that are ready to out and answers how many; it waits in the kernel at most until the instant until, or until the loop's next timer is due, whichever is sooner. poll(forever(), out) waits as long as it takes and poll(Instant.now(), out) never waits. Windows' own wait is only as fine as its timer tick, 15.6 ms by default, so the loop arms a high-resolution timer for the next due instant and has the kernel deliver its expiry to the completion port: the wait ends on time without raising the whole system's timer resolution. A timer a tick or more off goes through the wheel and may be up to a millisecond late; one nearer fires at the instant the loop wakes for it.

A deadline on an operation#

A deadline is an absolute Instant. lp.submitBy(op, to, deadline) starts an operation that answers TimedOut if it has not finished by then; lp.submitIn(op, to, group, deadline) does the same inside a cancel group, the deadline being the tighter of the one given and the group's (forever() to use the group's alone).

  • An operation whose deadline has already passed is answered TimedOut at submit and never started.
  • At the deadline the loop asks the operation to cancel, and the answer says TimedOut. The kernel (or the pool's thread) then has a grace, 1 ms unless lp.setGrace(d) says otherwise, to give the buffer back. A socket's cancel is answered in microseconds, and the TimedOut comes with its buffer.
  • Past the grace, the loop answers TimedOut at once without the buffer, promptly, as a frame loop needs even when a read on a hung share is stuck. The buffer goes home to its pool when the kernel lets it go. After such an answer the operation's effect is unknown: a receive may have taken bytes, a send may have sent. If the kernel answers between the loop's decision and its delivery, the kernel's own outcome is the answer.
  • A blocking-pool job still queued at its deadline is taken out of its queue and answered TimedOut with its buffer, never run. One already running cannot be stopped, and is answered after the grace without its buffer.

A timer has no separate deadline: its time is its deadline.

Cancelling#

lp.cancel(id) asks an operation or a timer to end. It is a request, so the operation still answers exactly once, and the answer says how it ended: Cancelled if the cancel caught it, Done or Failed if it had finished first; the buffer comes back either way. It answers true while the operation's answer is still to come, and false once the answer is decided or for an id that names nothing, so asking twice does no harm. A cancelled timer is answered Cancelled there and then, so a second cancel of it says false. lp.request(id) is a Request, a handle another thread may hold: its cancel(), or its death, posts the cancel to the loop, which acts on it at its next poll and wakes if it sleeps; detach() lets it go without cancelling.

A CancelGroup cancels many operations and timers at once, from any thread, and carries a deadline that its children inherit as the tighter. Its nesting, its shields, and what each answers are in Cancel groups. The part a time user needs: CancelGroup.until(d) is a group whose operations submitted with submitIn (or externalIn) answer TimedOut by d; g.childUntil(d) is a child with the tighter of d and its parent's deadline; and a timer joined with afterIn belongs to the group for its cancel only, answering Done at its own time whatever the group's deadline.

// (helpers: waitFor polls until n answers are in 'out'; how and say print an answer's
// token, outcome and whether its buffer came back; pair makes two connected
// sockets, a and b, on which nothing is ever sent, so a receive on one is silent)

fn main() {
  imm pool = Pool.make();
  mut lp = Loop.mustMake();
  mut out = List[Completion].empty();
  mut a = Handle.none();
  mut b = Handle.none();
  a, b = pair(&mut lp);

  // 1. A timer answers Done at its time; a cancelled one answers Cancelled
  lp.after(Instant.now() + Duration.fromMillis(20), Target.Here(1u64));
  imm hour = lp.after(Instant.now() + Duration.fromSeconds(3600), Target.Here(2u64));
  printStr(if lp.cancel(hour) {"cancel: true\n";} else {"cancel: false\n";});
  printStr(if lp.cancel(hour) {"cancel again: true\n";} else {"cancel again: false\n";});
  waitFor(&mut lp, &mut out, 2usize);
  sayAll(&mut out);

  // 2. An operation with a deadline: a receive on a silent socket
  imm t0 = Instant.now();
  lp.submitBy(Op.Recv(a, pool.mustTake(100usize)), Target.Here(3u64), t0 + Duration.fromMillis(50));
  waitFor(&mut lp, &mut out, 1usize);
  sayAll(&mut out);
  printStr(if t0.elapsed() >= Duration.fromMillis(50) {"not before the deadline\n";} else {"EARLY\n";});

  // 3. A deadline already past: answered at submit, never started
  lp.submitBy(Op.Recv(a, pool.mustTake(100usize)), Target.Here(4u64), Instant.now() - Duration.fromMillis(1));
  waitFor(&mut lp, &mut out, 1usize);
  sayAll(&mut out);

  // 4. A group with a deadline, and the same group cancelled
  imm t1 = Instant.now();
  imm g = CancelGroup.until(t1 + Duration.fromMillis(40));
  lp.submitIn(Op.Recv(a, pool.mustTake(100usize)), Target.Here(5u64), g, forever());
  lp.afterIn(t1 + Duration.fromMillis(200), Target.Here(6u64), g);
  waitFor(&mut lp, &mut out, 2usize);
  sayAll(&mut out);
  printStr(if t1.elapsed() >= Duration.fromMillis(200) {"the timer waited for its own 200 ms\n";} else {"the timer ended early\n";});

  imm h = CancelGroup.make();
  lp.submitIn(Op.Recv(a, pool.mustTake(100usize)), Target.Here(7u64), h, forever());
  lp.afterIn(Instant.now() + Duration.fromSeconds(3600), Target.Here(8u64), h);
  h.cancel();
  waitFor(&mut lp, &mut out, 2usize);
  sayAll(&mut out);
  printUInt(lp.inFlight());
  printStr(" in flight\n");
}

This prints:

cancel: true
cancel again: false
token 2: Cancelled, buffer none
token 1: Done, buffer none
token 3: TimedOut, buffer back
not before the deadline
token 4: TimedOut, buffer back
token 5: TimedOut, buffer back
token 6: Done, buffer none
the timer waited for its own 200 ms
token 8: Cancelled, buffer none
token 7: Cancelled, buffer back
0 in flight

Answers arrive in the order they were decided, not the order they were submitted: the cancelled timer (token 2) was decided at once, and the 20 millisecond timer (token 1) only when its time came. The receive with a 50 millisecond deadline (token 3) answers TimedOut with its buffer; the one whose deadline had already passed (token 4) was never started. In the group with a 40 millisecond deadline, the receive (token 5) times out and the timer (token 6) still answers Done at its own 200 milliseconds. The cancelled group answers its timer and its receive Cancelled and leaves nothing in flight.