Skip to content

time: allocation-free fixed-buffer variant of push_to_http_header (cached HTTP Date headers) #27638

Description

@enghitalo

Describe the feature

HTTP servers cache the Date: header line and refresh it once per second (nginx model). The natural vlib API for that is time.push_to_http_header, but its current shape makes an allocation-free cached-Date rebuild impossible:

  1. It can only append to a dynamic []u8 — there is no way to write the 29 bytes into caller-owned storage (a fixed array, or a preallocated region of a dynamic array) at an offset.
  2. Internally it calls weekday_str() and smonth(), which each do a substr (long_days[i][0..3], months_string[i*3..(i+1)*3]) — two heap allocations per call.

Measurements (current master, -prod, 1M iterations, x86-64 Linux)

approach per call
line.clear() + appends + time.utc().push_to_http_header(mut line) ~1.25 µs
time.utc() alone ~1.2 µs
C.time(0) ~2.5 ns
fixed [37]u8 cache, incremental digit update (reference impl below) ~9.2 ns (135x)

Proposed Solution — minimal diff, validated (PR #27639)

The body of push_to_http_header already builds the 29 bytes in a stack template via the pointer-based int_to_ptr_byte_array_no_pad — it is 90% of the needed API. The change moves that body into @[unsafe] write_http_header(dst &u8, dst_len int) ! — pointer-based so one API serves fixed arrays, dynamic arrays and arenas at any offset; bounds-checked (refuses dst_len < time.http_header_len, writing nothing) so a caller cannot silently overrun; @[unsafe]-tagged so the raw-pointer contract is explicit at the call site (like vbytes). push_to_http_header becomes a thin wrapper with unchanged behavior — all 17 vlib/time tests pass, output verified byte-identical to http_header_string() across second/minute/hour/day/leap-year transitions on both storage shapes.

diff --git a/vlib/time/format.v b/vlib/time/format.v
index b914ea0e8..beae94955 100644
--- a/vlib/time/format.v
+++ b/vlib/time/format.v
@@ -736,24 +736,42 @@ pub fn (t Time) http_header_string() string {
 	return buf.bytestr()
 }
 
-// push_to_http_header returns a date string in the format used in HTTP headers, as defined in RFC 2616.
-// e.g. "Sun, 06 Nov 1994 08:49:37 GMT"
-pub fn (t Time) push_to_http_header(mut buffer []u8) {
-	day_str := t.weekday_str()
-	month_str := t.smonth()
-
-	mut buf := [day_str[0], day_str[1], day_str[2], `,`, ` `, `0`, `0`, ` `, month_str[0], month_str[1],
-		month_str[2], ` `, `0`, `0`, `0`, `0`, ` `, `0`, `0`, `:`, `0`, `0`, `:`, `0`, `0`, ` `,
-		`G`, `M`, `T`]!
+// http_header_len is the byte length of an HTTP-date (RFC 9110 IMF-fixdate).
+pub const http_header_len = 29
+
+// write_http_header writes the 29-byte HTTP-date ("Sun, 06 Nov 1994 08:49:37 GMT",
+// RFC 9110 IMF-fixdate) at dst, which may point into a fixed array or a dynamic
+// array's data, at any offset. dst_len is the number of writable bytes at dst;
+// an error is returned when it is less than http_header_len, so a caller cannot
+// silently overrun its buffer. No allocation on the success path.
+@[unsafe]
+pub fn (t Time) write_http_header(dst &u8, dst_len int) ! {
+	if dst_len < http_header_len {
+		return error('time.write_http_header: dst_len must be >= 29')
+	}
+	day_str := long_days[iclamp(0, t.day_of_week() - 1, 6)] // read in place: no substr
+	mi := iclamp(0, t.month - 1, 11) * 3
+
+	mut buf := [day_str[0], day_str[1], day_str[2], `,`, ` `, `0`, `0`, ` `, months_string[mi],
+		months_string[mi + 1], months_string[mi + 2], ` `, `0`, `0`, `0`, `0`, ` `, `0`, `0`, `:`,
+		`0`, `0`, `:`, `0`, `0`, ` `, `G`, `M`, `T`]!
 	unsafe {
 		int_to_ptr_byte_array_no_pad(t.day, &buf[5], 2)
 		int_to_ptr_byte_array_no_pad(t.year, &buf[12], 4)
 		int_to_ptr_byte_array_no_pad(t.hour, &buf[17], 2)
 		int_to_ptr_byte_array_no_pad(t.minute, &buf[20], 2)
 		int_to_ptr_byte_array_no_pad(t.second, &buf[23], 2)
+		vmemcpy(dst, &buf[0], 29)
 	}
+}
+
+// push_to_http_header appends the 29-byte HTTP-date to buffer, as defined in RFC 2616.
+// e.g. "Sun, 06 Nov 1994 08:49:37 GMT"
+pub fn (t Time) push_to_http_header(mut buffer []u8) {
+	mut buf := [29]u8{}
 	unsafe {
-		buffer.push_many(&buf[0], buf.len)
+		t.write_http_header(&buf[0], 29) or {} // 29 >= http_header_len: cannot fail
+		buffer.push_many(&buf[0], 29)
 	}
 }
 

Optional follow-up

For the cached use case an incremental "update only the digits that rolled over" helper is another ~100x on top (same minute = only the 2 seconds digits change; full calendar reformat only on day rollover). Worked, oracle-tested reference implementation:
https://github.com/enghitalo/vanilla/blob/main/examples/async_date_timerfd/main.v

Use Case

Any V HTTP server that sends the RFC 9110-mandated Date header without paying per-request (or even per-second) allocations for it.

Other Information

The substr allocations in weekday_str()/smonth() also affect other formatting paths; the diff above removes them from the HTTP-date path without touching those public functions.

Acknowledgements

Note

You can use the 👍 reaction to increase the issue's priority for developers.

Please note that only the 👍 reaction to the issue itself counts as a vote.
Other reactions and those to comments will not be taken into account.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions