Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 18 additions & 56 deletions examples/async_date_timerfd/main.v
Original file line number Diff line number Diff line change
Expand Up @@ -28,37 +28,32 @@ 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

// 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 {
Expand All @@ -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) {
Expand Down
5 changes: 3 additions & 2 deletions examples/async_date_timerfd/main_test.v
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions examples/auth/src/main.v
Original file line number Diff line number Diff line change
Expand Up @@ -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 ---------------------------------------------------------------
Expand Down Expand Up @@ -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: ')
Expand Down
32 changes: 21 additions & 11 deletions examples/date_header/src/main.v
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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] () {
Expand Down
28 changes: 19 additions & 9 deletions examples/date_header/src/main_test.v
Original file line number Diff line number Diff line change
@@ -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: ')
Expand All @@ -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')
}
19 changes: 8 additions & 11 deletions examples/efficient_date/main.v
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,6 @@ module main
import http_server
import time

#include <time.h>

// 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:
Expand All @@ -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()
}

Expand Down
Loading