diff --git a/examples/async_date_timerfd/main.v b/examples/async_date_timerfd/main.v index 7f39dc6..5f2ce62 100644 --- a/examples/async_date_timerfd/main.v +++ b/examples/async_date_timerfd/main.v @@ -28,7 +28,6 @@ import time 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. const date_line_len = 37 @@ -36,29 +35,25 @@ const date_line_len = 37 // Every static byte gets its place ONCE; rebuilds only overwrite 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 — +// The refresh is time.update_http_header (V stdlib, #27639): because the +// IMF-fixdate is FIXED-WIDTH, a 1 Hz refresh almost never changes more than the +// two seconds digits, so it rewrites 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. +// and reads the clock with time.unix_now() (#27641 — a bare time() call, vDSO, +// ~2 ns) instead of time.utc() (~1.2 us of clock + calendar conversion). This +// example originally hand-rolled the technique that both those PRs upstreamed; +// it now just calls the stdlib. Measured same-minute tick: ~2 ns vs the old +// dynamic-array + push_to_http_header rebuild at 1.25 us. 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 in line[6..35] (0 = never formatted) } fn make_state() voidptr { @@ -67,54 +62,21 @@ 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] +// rebuild_at re-encodes `now` into the cached line via stdlib +// time.update_http_header, touching only the digits that changed: the common +// (same-minute) path is a 2-byte store; a full reformat happens only on day +// rollover. dc.line[6] is the 29-byte IMF-fixdate; [0..6] is "Date: " and +// [35..37] the CRLF — both seeded by make_state and never rewritten here. +// Split out from rebuild() as a pure seam over (dc.last, now) so the tests can +// drive it through every rollover without depending on the wall clock. 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) - } + unsafe { time.update_http_header(&dc.line[6], date_line_len - 6, 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))) + dc.rebuild_at(time.unix_now()) } fn arm_periodic(tfd int, ms int) { diff --git a/examples/async_date_timerfd/main_test.v b/examples/async_date_timerfd/main_test.v index bdcb28e..50cb93e 100644 --- a/examples/async_date_timerfd/main_test.v +++ b/examples/async_date_timerfd/main_test.v @@ -4,8 +4,9 @@ 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. +// ORACLE. rebuild_at delegates to stdlib time.update_http_header (which only +// touches the digits that changed); this test guards THIS example's integration +// of it — the &line[6] offset and the make_state seeding of the static frame. // expected builds the reference line via vlib (test scaffolding). fn expected(u i64) string { diff --git a/examples/auth/src/main.v b/examples/auth/src/main.v index a23c26f..87ee87c 100644 --- a/examples/auth/src/main.v +++ b/examples/auth/src/main.v @@ -154,7 +154,7 @@ fn jwt_verify(token []u8) bool { } payload_b64 := unsafe { tos(&token[first + 1], last - first - 1) } // view string exp := exp_of(base64.url_decode(payload_b64)) - return exp > 0 && exp > time.utc().unix() + return exp > 0 && exp > time.unix_now() } // ---- API key --------------------------------------------------------------- @@ -250,7 +250,7 @@ fn handle(req_buffer []u8, _ int, mut out []u8) ! { payload.write_string('{"sub":"') payload.write_string(demo_user) payload.write_string('","exp":') - payload.write_decimal(time.utc().unix() + 3600) + payload.write_decimal(time.unix_now() + 3600) payload.write_u8(`}`) token := jwt_sign(payload) ws(mut out, 'HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: ') diff --git a/examples/date_header/src/main.v b/examples/date_header/src/main.v index 1120cc2..9f89c38 100644 --- a/examples/date_header/src/main.v +++ b/examples/date_header/src/main.v @@ -26,29 +26,38 @@ import http_server import time import sync.stdatomic -// "Date: " (6) + "Wed, 21 Oct 2015 07:28:00" (25) + " GMT" (4) + "\r\n" (2) = 37 +// "Date: " (6) + "Wed, 21 Oct 2015 07:28:00 GMT" (29) + "\r\n" (2) = 37 const date_line_len = 37 +// The static frame every buffer starts from: only the 29 date bytes at offset 6 +// ever change (write_http_header rewrites exactly those), so the "Date: " prefix +// and trailing CRLF are seeded once and never touched on the refresh path. +const line_template = 'Date: Xxx, 00 Xxx 0000 00:00:00 GMT\r\n' + struct DateCache { mut: bufs [2][date_line_len]u8 idx u64 // active buffer index (0/1); read/written atomically } +// seed lays down the static frame ("Date: " + placeholder + CRLF) in BOTH +// buffers once, so every later refresh only rewrites the 29 date bytes. +fn (mut c DateCache) seed() { + for i in 0 .. 2 { + unsafe { vmemcpy(&c.bufs[i][0], line_template.str, date_line_len) } + } +} + // refresh formats the current UTC time into the INACTIVE buffer, then publishes // it by flipping the atomic index. Called once per second by the ticker, so even -// this off-hot-path work is cheap: `time.push_to_http_header` writes the 29-byte -// RFC 7231 date straight into the buffer with hand-placed bytes — no format -// template to parse (unlike `custom_format`), no intermediate string. +// this off-hot-path work is cheap: `time.write_http_header` writes the 29-byte +// RFC 9110 IMF-fixdate straight into the buffer at offset 6 (after "Date: "), +// allocation-free — no format template to parse, no intermediate string. fn (mut c DateCache) refresh() { - mut line := 'Date: '.bytes() // 6 bytes - time.utc().push_to_http_header(mut line) // + "Wed, 21 Oct 2015 07:28:00 GMT" (29) - line << `\r` - line << `\n` // 37 total cur := stdatomic.load_u64(&c.idx) next := 1 - cur - for j in 0 .. date_line_len { - c.bufs[int(next)][j] = line[j] + unsafe { + time.utc().write_http_header(&c.bufs[int(next)][6], date_line_len - 6) or {} } stdatomic.store_u64(&c.idx, next) // publish atomically } @@ -70,7 +79,8 @@ const resp_tail = 'Content-Type: text/plain\r\nContent-Length: 2\r\nConnection: fn main() { mut cache := &DateCache{} - cache.refresh() // seed before the first request is served + cache.seed() // lay down the static frame in both buffers once + cache.refresh() // format the date before the first request is served // One ticker for the whole server (not per connection): refresh ~1×/second. spawn fn [mut cache] () { diff --git a/examples/date_header/src/main_test.v b/examples/date_header/src/main_test.v index 147f44d..d88dde7 100644 --- a/examples/date_header/src/main_test.v +++ b/examples/date_header/src/main_test.v @@ -1,11 +1,13 @@ module main -// The cache logic is pure/in-memory, so the format, the publish, and the handler -// injection are all unit-testable without a clock-dependent assertion on the -// exact value. +// The cache logic is pure/in-memory, so the format, the publish, and the response +// composition are all unit-testable without a clock-dependent assertion on the +// exact value. seed() must run before refresh() (it lays the static "Date: " / +// CRLF frame that write_http_header does not touch), exactly as main() does. fn test_refresh_produces_valid_date_line() { mut c := DateCache{} + c.seed() c.refresh() line := c.date_line().bytestr() assert line.starts_with('Date: ') @@ -15,18 +17,26 @@ fn test_refresh_produces_valid_date_line() { fn test_double_buffer_flips() { mut c := DateCache{} + c.seed() c.refresh() first := c.idx c.refresh() assert c.idx == 1 - first // publishes to the other buffer each time } -fn test_handler_includes_date_header() ! { +fn test_response_includes_date_header() { mut c := DateCache{} + c.seed() c.refresh() - out := handle('GET / HTTP/1.1\r\nHost: x\r\n\r\n'.bytes(), -1, c)!.bytestr() - assert out.contains('HTTP/1.1 200 OK\r\n') - assert out.contains('Date: ') - assert out.contains(' GMT\r\n') - assert out.contains('Content-Length: 2\r\n') + // Reproduce exactly what the request_handler closure writes into `out`: + // the two static halves plus the cached, zero-copy Date line. + mut out := []u8{} + out << status_head + out << c.date_line() + out << resp_tail + resp := out.bytestr() + assert resp.contains('HTTP/1.1 200 OK\r\n') + assert resp.contains('Date: ') + assert resp.contains(' GMT\r\n') + assert resp.contains('Content-Length: 2\r\n') } diff --git a/examples/efficient_date/main.v b/examples/efficient_date/main.v index 3324521..79c24bf 100644 --- a/examples/efficient_date/main.v +++ b/examples/efficient_date/main.v @@ -20,12 +20,6 @@ module main import http_server import time -#include - -// C.time returns the current Unix second (time_t) directly — far cheaper than -// building a full calendar Time per request just to read its second. -fn C.time(t voidptr) i64 - // DateCache is one worker's cached Date line + the unix second it is valid for. struct DateCache { mut: @@ -44,18 +38,21 @@ fn make_state() voidptr { // refresh rebuilds the cached Date line only when the second has advanced. @[direct_array_access] fn (mut dc DateCache) refresh() { - // Hot path: ONE cheap C.time() (no calendar decomposition, no allocation) to - // detect a second boundary. Only when the second actually advances do we pay - // for time.utc()'s full RFC-1123 formatting — once per second, not per request. + // Hot path: ONE cheap time.unix_now() (a bare time() call, ~2 ns, served from + // the vDSO — no calendar decomposition, no allocation) to detect a second + // boundary. Only when the second actually advances do we pay for time.utc()'s + // full RFC-1123 formatting — once per second, not per request. // (Was: time.utc() on EVERY request just to read its .unix() second.) - now_sec := C.time(unsafe { nil }) + now_sec := time.unix_now() if now_sec == dc.sec { return } dc.sec = now_sec dc.line.clear() dc.line << 'Date: '.bytes() - time.utc().push_to_http_header(mut dc.line) // appends "Sun, 06 Nov 1994 08:49:37 GMT" + // push_to_http_header writes "Sun, 06 Nov 1994 08:49:37 GMT" — now allocation-free + // itself (wraps time.write_http_header), so this once-a-second rebuild is cheap. + time.utc().push_to_http_header(mut dc.line) dc.line << '\r\n'.bytes() }