diff --git a/examples/async_date_timerfd/README.md b/examples/async_date_timerfd/README.md
new file mode 100644
index 0000000..ef43851
--- /dev/null
+++ b/examples/async_date_timerfd/README.md
@@ -0,0 +1,87 @@
+# async_date_timerfd — cached `Date:` header, refreshed by a per-worker timerfd
+
+The architecturally-pure way to send the RFC 9110-mandated `Date` header at
+hundreds of thousands of req/s: format it **once per second**, not per request
+— and do the refresh **inside each worker's own epoll loop** (the nginx
+model), so there is no extra thread, no shared state, no lock, and the request
+hot path does *zero* time work: it appends 37 cached bytes.
+
+Time is treated as an **event, not a sleeping thread**: a 1 s periodic
+`timerfd` is registered in the worker's reactor as a clientless watch, and its
+expiry arrives as ordinary fd readability between requests.
+
+## Requirements
+
+The moving parts live in V's stdlib as of these PRs (until they merge, build V
+from the corresponding branches):
+
+| vlang/v PR | provides |
+|---|---|
+| [#27639](https://github.com/vlang/v/pull/27639) | `time.write_http_header` / `time.update_http_header` (in-place, incremental) |
+| [#27641](https://github.com/vlang/v/pull/27641) | `time.unix_now()` — wall-clock second in ~2 ns |
+| [#27642](https://github.com/vlang/v/pull/27642) | `C.timerfd_*` declarations via `import time` |
+
+## How it works
+
+```mermaid
+flowchart TD
+ subgraph worker["one worker thread (per core) — nothing shared, no locks"]
+ start(["on_worker_start"]) --> mk["make_state()
DateCache: fixed [37]u8 seeded from
'Date: Xxx, 00 Xxx 0000 00:00:00 GMT\r\n'"]
+ mk --> seed["rebuild() — first call:
dc.last == 0 → full format"]
+ seed --> arm["C.timerfd_create(CLOCK_MONOTONIC)
arm_periodic(1000 ms)
(kernel re-arms: cadence never drifts)"]
+ arm --> watch["ac.watch(tfd, .readable, date_tick)"]
+ watch --> loop{{"epoll_wait
one loop, two event kinds"}}
+
+ loop -->|"client fd readable"| req["handle(req, fd, mut out, state):
out << head
push_many(cached 37 bytes)
out << tail
zero time work"]
+ req --> loop
+
+ loop -->|"tfd readable — the 1 s tick"| drain["date_tick():
read(tfd, &expirations, 8)
drains the count, re-levels the fd"]
+ drain --> upd["rebuild():
time.unix_now() (~2 ns)
time.update_http_header (~2 ns):
same minute → 2 digit stores
minute/hour rollover → +2 each
day rollover → full reformat (1×/day)"]
+ upd --> rearm["ac.watch(tfd, ...) again
return .suspend"]
+ rearm --> loop
+ end
+```
+
+The cached line, byte by byte — every refresh rewrites only the digits whose
+bucket rolled over:
+
+```text
+D a t e : W e d , 2 1 O c t 2 0 1 5 0 7 : 2 8 : 0 0 G M T \r \n
+0 6 23 26 29 37
+'Date: ' └────────── IMF-fixdate (time.http_header_len = 29) ──────────┘CRLF
+```
+
+## Why it is fast
+
+| operation | cost |
+|---|---|
+| request hot path | 3 buffer appends — no clock, no formatting, no allocation |
+| per-second refresh (same minute) | `unix_now()` ~2 ns + 2 byte stores (~2 ns) |
+| day rollover | one full 29-byte reformat — once per day |
+| the rebuild this replaced | ~1.25 µs: dynamic-array clear+appends, `time.utc()` calendar math, 2 hidden substr allocations |
+
+The kernel re-arms the periodic timer, so the 1 Hz cadence does not drift with
+processing time, and `read()` returns the number of expirations — missed ticks
+are *counted*, never silently lost. Because refresh and request handling run
+on the same thread, the `[37]u8` cache needs no synchronization at all —
+contrast with `examples/date_header`, where a dedicated sleeper thread writes
+a cache that worker threads read (double buffering + atomic flip required),
+and with `examples/efficient_date`, which refreshes lazily on the first
+request of each second (correct, but the clock read lands on a request).
+
+## Run
+
+```sh
+v run examples/async_date_timerfd/
+curl -i http://localhost:8097/ # note the Date header
+sleep 3
+curl -i http://localhost:8097/ # Date advanced — with zero load
+```
+
+## Tests
+
+`main_test.v` covers this example's wiring: the template/`time.http_header_len`
+relationship, the refresh-current-second contract (against
+`http_header_string()` as oracle) and the handler's framing. The incremental
+update logic itself is oracle-tested in vlib across every rollover
+(second/minute/hour/day, forward and backward jumps).
diff --git a/examples/async_date_timerfd/main.v b/examples/async_date_timerfd/main.v
index 7f39dc6..1895bb3 100644
--- a/examples/async_date_timerfd/main.v
+++ b/examples/async_date_timerfd/main.v
@@ -10,10 +10,20 @@ module main
// touching the cache runs on the one thread that owns it). The request handler
// then does ZERO time work on the hot path: it just appends the cached bytes.
//
+// The moving parts are stdlib since vlang/v#27639 / #27641 / #27642:
+// - `time.update_http_header` refreshes the line IN PLACE, rewriting only the
+// digits whose bucket rolled over (~2 ns/tick; calendar math only on day
+// rollover) — vs ~1.25 µs for the old dynamic-array rebuild it replaced.
+// - `time.unix_now()` reads the wall-clock second (vDSO, ~2 ns) without
+// constructing a `Time`.
+// - the timerfd declarations (`C.timerfd_create`/`C.timerfd_settime`/
+// `C.itimerspec`) ship with `import time` — no local `fn C.` declarations,
+// no hand-built itimerspec in a raw [4]i64.
+//
// Contrast with examples/efficient_date (lazy per-request refresh: correct and
-// simple, but calls time.utc() on the request that crosses each second). Here the
-// clock advances even with NO traffic — the timerfd wakes the idle loop once a
-// second — so the Date is fresh independent of request rate.
+// simple, but pays the clock read on the request that crosses each second). Here
+// the clock advances even with NO traffic — the timerfd wakes the idle loop once
+// a second — so the Date is fresh independent of request rate.
//
// Run: v run examples/async_date_timerfd/
// Try: curl -i http://localhost:8097/ # note Date; re-run after a few
@@ -22,43 +32,25 @@ import http_server
import http_server.core
import time
-#include
-#include
-
-fn C.timerfd_create(clockid int, flags int) int
-fn C.timerfd_settime(fd int, flags int, new_value voidptr, old_value voidptr) int
fn C.read(fd int, buf voidptr, count usize) int
-fn C.time(t voidptr) i64
-// 'Date: ' (6) + IMF-fixdate (29, RFC 9110 §5.6.7) + CRLF (2) = 37 bytes.
+// 'Date: ' (6) + IMF-fixdate (time.http_header_len = 29) + CRLF (2) = 37.
+// (A literal, not `6 + time.http_header_len + 2`: a fixed-array size built
+// from another module's const trips the checker in _test.v builds; the
+// relationship is pinned by a test instead.)
+const date_prefix_len = 6
const date_line_len = 37
-// Every static byte gets its place ONCE; rebuilds only overwrite digits.
+// Static bytes get their place ONCE; every refresh only rewrites date digits.
const line_template = 'Date: Xxx, 00 Xxx 0000 00:00:00 GMT\r\n'
-// day_of_week() is 1..7 = Mon..Sun.
-const wkday_names = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
-const month_names = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov',
- 'Dec']
-
// DateCache is one worker's pre-formatted `Date: ...\r\n` line in a FIXED
-// array — no heap, no growth, cache-line friendly. Only ever touched by that
-// worker's thread (make_state + the timerfd continuation), so no lock.
-//
-// The IMF-fixdate is FIXED-WIDTH, so a 1 Hz refresh almost never changes more
-// than the two seconds digits. rebuild_at exploits that: it re-encodes only
-// the buckets that rolled over —
-// same minute -> 2 byte stores (seconds)
-// same hour -> 4 stores same day -> 6 stores
-// day rollover -> full reformat (calendar math), once per day
-// and reads the clock with C.time(0) (vDSO, ~2.5 ns) instead of time.utc()
-// (~1.2 us of clock + calendar conversion, plus two hidden substr allocations
-// inside push_to_http_header's weekday_str()/smonth()). Measured with the old
-// dynamic-array + push_to_http_header rebuild: 1.25 us -> 9.2 ns, ~135x.
+// array — no heap, no growth. Only ever touched by that worker's thread
+// (make_state + the timerfd continuation), so no lock.
struct DateCache {
mut:
line [date_line_len]u8
- last i64 // unix second currently encoded in line (0 = never formatted)
+ last i64 // unix second currently encoded (0 = never: first refresh writes all fields)
}
fn make_state() voidptr {
@@ -67,63 +59,31 @@ fn make_state() voidptr {
return dc
}
-// put2 writes v (0..99) as two ASCII digits at line[o] — two byte stores.
-@[direct_array_access; inline]
-fn (mut dc DateCache) put2(o int, v int) {
- dc.line[o] = u8(`0` + v / 10)
- dc.line[o + 1] = u8(`0` + v % 10)
-}
-
-// rebuild_at re-encodes `now` into the line, touching only what changed.
-// Pure over (dc.last, now) — the tests drive it through every rollover.
-@[direct_array_access]
-fn (mut dc DateCache) rebuild_at(now i64) {
- if now == dc.last {
- return
- }
- tod := int(now % 86400)
- if dc.last != 0 && now / 86400 == dc.last / 86400 {
- dc.put2(29, tod % 60)
- if now / 60 != dc.last / 60 {
- dc.put2(26, (tod / 60) % 60)
- if now / 3600 != dc.last / 3600 {
- dc.put2(23, tod / 3600)
- }
- }
- } else {
- // Day rollover (or first call): full reformat — the only place that
- // pays calendar math, once per day.
- t := time.unix(now)
- w := wkday_names[t.day_of_week() - 1]
- m := month_names[t.month - 1]
- dc.line[6] = w[0]
- dc.line[7] = w[1]
- dc.line[8] = w[2]
- dc.put2(11, t.day)
- dc.line[14] = m[0]
- dc.line[15] = m[1]
- dc.line[16] = m[2]
- dc.put2(18, t.year / 100)
- dc.put2(20, t.year % 100)
- dc.put2(23, t.hour)
- dc.put2(26, t.minute)
- dc.put2(29, t.second)
+// rebuild refreshes the cached line for the current second: one ~2 ns clock
+// read + an in-place update of only the digits that changed.
+fn rebuild(mut dc DateCache) {
+ now := time.unix_now()
+ unsafe {
+ // 31 writable bytes >= time.http_header_len: cannot fail.
+ time.update_http_header(&dc.line[date_prefix_len], date_line_len - date_prefix_len,
+ dc.last, now) or {}
}
dc.last = now
}
-// rebuild refreshes the cached line for the current second.
-fn rebuild(mut dc DateCache) {
- dc.rebuild_at(i64(C.time(0)))
-}
-
+// arm_periodic starts the kernel-paced tick: first expiry after `ms`, then
+// every `ms` — re-armed by the KERNEL, so the cadence never drifts with the
+// continuation's processing time.
fn arm_periodic(tfd int, ms int) {
- mut spec := [4]i64{} // { it_interval{s,ns}, it_value{s,ns} } — both set = periodic
- spec[0] = i64(ms / 1000)
- spec[1] = i64(ms % 1000) * 1_000_000
- spec[2] = spec[0]
- spec[3] = spec[1]
- C.timerfd_settime(tfd, 0, voidptr(&spec[0]), unsafe { nil })
+ interval := C.timespec{
+ tv_sec: ms / 1000
+ tv_nsec: i64(ms % 1000) * 1_000_000
+ }
+ spec := C.itimerspec{
+ it_value: interval
+ it_interval: interval
+ }
+ C.timerfd_settime(tfd, 0, &spec, unsafe { nil })
}
// on_start runs once per worker (client_fd = -1): build the cache now so the very
@@ -140,8 +100,8 @@ fn on_start(mut ac core.AsyncCtx) {
// and re-arm. Returns .suspend (keep the background watch alive). It never writes
// to `out` (there is no client) and lives for the worker's whole lifetime.
fn date_tick(mut out []u8, mut ac core.AsyncCtx) core.AsyncStep {
- mut tmp := [8]u8{}
- C.read(ac.ready_fd(), &tmp[0], 8) // drain the expiration count to re-level the fd
+ mut expirations := u64(0)
+ C.read(ac.ready_fd(), &expirations, 8) // drain the count to re-level the fd
mut dc := unsafe { &DateCache(ac.state) }
rebuild(mut dc)
ac.watch(ac.ready_fd(), .readable, date_tick, unsafe { nil })
diff --git a/examples/async_date_timerfd/main_test.v b/examples/async_date_timerfd/main_test.v
index bdcb28e..d977b61 100644
--- a/examples/async_date_timerfd/main_test.v
+++ b/examples/async_date_timerfd/main_test.v
@@ -2,60 +2,40 @@ module main
import time
-// SOLUTION: rebuild_at is pure over (dc.last, now) — drive it through every
-// bucket rollover and compare byte-for-byte against the vlib formatter as the
-// ORACLE. The incremental path only ever touches the digits that changed, so
-// the oracle equality is exactly the property that matters.
-
-// expected builds the reference line via vlib (test scaffolding).
-fn expected(u i64) string {
- return 'Date: ' + time.unix(u).http_header_string() + '\r\n'
-}
-
-fn cache() &DateCache {
- return unsafe { &DateCache(make_state()) }
-}
+// SOLUTION: the incremental-update logic itself lives in vlib now
+// (time.update_http_header, oracle-tested there across every rollover) —
+// these tests cover THIS example's wiring: the template seeding, the
+// refresh-current-second contract, and the handler's framing.
fn line_of(dc &DateCache) string {
return unsafe { tos(&dc.line[0], date_line_len) }.clone()
}
-fn test_first_format_and_idempotence() {
- mut dc := cache()
- dc.rebuild_at(1735689600) // 2025-01-01 00:00:00 UTC (full format path)
- assert line_of(dc) == expected(1735689600)
- dc.rebuild_at(1735689600) // same second: no-op, line unchanged
- assert line_of(dc) == expected(1735689600)
+fn expected(u i64) string {
+ return 'Date: ' + time.unix(u).http_header_string() + '\r\n'
+}
+
+fn test_line_len_matches_vlib() {
+ assert date_line_len == date_prefix_len + time.http_header_len + 2
}
-fn test_all_rollovers_match_oracle() {
- mut dc := cache()
- seq := [
- i64(1735689600), // seed (full format)
- 1735689601, // +1 s: seconds digits only
- 1735689659, // :59
- 1735689660, // minute rollover
- 1735693199, // 00:59:59
- 1735693200, // hour rollover
- 1735775999, // 23:59:59
- 1735776000, // day rollover (full reformat)
- 1740787199, // jump forward weeks (leap-February territory)
- 999999999, // jump BACKWARD across years — must fully reformat
- 1000000000, // and advance again
- ]
- for u in seq {
- dc.rebuild_at(u)
- assert line_of(dc) == expected(u), 'mismatch at unix=${u}'
- }
+fn test_rebuild_encodes_the_current_second() {
+ mut dc := unsafe { &DateCache(make_state()) }
+ rebuild(mut dc)
+ assert dc.last > 0
+ assert line_of(dc) == expected(dc.last)
+ rebuild(mut dc) // same second: no-op; a second later: seconds digits only
+ assert line_of(dc) == expected(dc.last)
}
-fn test_every_second_across_midnight() {
- // Brute-force a window over a day boundary: every second must match the
- // oracle (catches any missed bucket update in the incremental path).
- mut dc := cache()
- start := i64(1735775990) // 2025-01-01 23:59:50 UTC
- for u in start .. start + 30 {
- dc.rebuild_at(u)
- assert line_of(dc) == expected(u), 'mismatch at unix=${u}'
- }
+fn test_handle_serves_the_cached_date() ! {
+ state := make_state()
+ mut dc := unsafe { &DateCache(state) }
+ rebuild(mut dc)
+ mut out := []u8{}
+ handle('GET / HTTP/1.1\r\nHost: x\r\n\r\n'.bytes(), -1, mut out, state)!
+ s := out.bytestr()
+ assert s.starts_with('HTTP/1.1 200 OK\r\nDate: ')
+ assert s.contains(time.unix(dc.last).http_header_string())
+ assert s.ends_with('Connection: keep-alive\r\n\r\nok')
}