From dcb98853b9bb5c6526d1d03d31955e2d8050788d Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Mon, 27 Jul 2026 09:28:48 +0800 Subject: [PATCH] chore: make our use of clock/timestamp more consistent Zig 0.16 changed clocks/timestamps. Our migration to Zig 0.16 took the path of least resistance. For example, we kept 'monotonic' and 'real' even though Zig 0.16 renamed them to 'boot' and 'clock' This commit tries to standardize all timestamp usage to: lp.datetime.timestamp and lp.datetime.milliTimestamp using the new zig 0.16 names ('boot' and 'clock') --- src/Watchdog.zig | 5 +-- src/agent/Spinner.zig | 24 ++++++----- src/browser/Frame.zig | 27 ++++++------- src/browser/js/Scheduler.zig | 11 +++--- src/browser/webapi/Console.zig | 14 ++----- src/browser/webapi/Document.zig | 5 ++- src/browser/webapi/File.zig | 2 +- src/browser/webapi/Performance.zig | 3 +- src/browser/webapi/net/FormData.zig | 2 +- src/browser/webapi/storage/Cookie.zig | 46 +++++++++++----------- src/browser/webapi/storage/CookieStore.zig | 6 +-- src/cdp/domains/network.zig | 11 +++--- src/cdp/domains/page.zig | 3 +- src/cdp/domains/storage.zig | 2 +- src/cookies.zig | 2 +- src/datetime.zig | 30 ++++---------- src/log.zig | 12 +++--- src/network/HttpClient.zig | 9 ++--- src/network/WebBotAuth.zig | 2 +- src/network/cache/Cache.zig | 15 +++---- src/network/cache/FsCache.zig | 16 ++++---- 21 files changed, 110 insertions(+), 137 deletions(-) diff --git a/src/Watchdog.zig b/src/Watchdog.zig index aab4944b2..0ea938b2a 100644 --- a/src/Watchdog.zig +++ b/src/Watchdog.zig @@ -22,7 +22,6 @@ const lp = @import("lightpanda"); const Env = @import("browser/js/Env.zig"); const log = lp.log; -const milliTimestamp = @import("datetime.zig").milliTimestamp; // How often the checker thread scans the entries. const CHECK_INTERVAL_NS = 1 * std.time.ns_per_s; @@ -107,7 +106,7 @@ fn run(self: *Watchdog) void { return; } - const now = milliTimestamp(.monotonic); + const now = lp.datetime.milliTimestamp(.boot); var node = self.entries.first; while (node) |n| : (node = n.next) { const entry: *Entry = @fieldParentPtr("node", n); @@ -157,7 +156,7 @@ pub const Heartbeat = struct { last_activity: std.atomic.Value(u64) = .init(0), pub fn touch(self: *Heartbeat) void { - self.last_activity.store(milliTimestamp(.monotonic), .release); + self.last_activity.store(lp.datetime.milliTimestamp(.boot), .release); } pub fn disarm(self: *Heartbeat) void { diff --git a/src/agent/Spinner.zig b/src/agent/Spinner.zig index 662e68d30..500b55cbc 100644 --- a/src/agent/Spinner.zig +++ b/src/agent/Spinner.zig @@ -28,7 +28,7 @@ const braille = [_][]const u8{ "⡇", "⣆", "⣤", "⣰", "⢸", "⠹", "⠛", const interval_ns: u64 = 100 * std.time.ns_per_ms; /// Minimum dwell on a tool label so the user can read it. Slow tools exceed /// it naturally; fast ones (getUrl, getCookies) get padded. -const min_tool_display_ns: u64 = 1500 * std.time.ns_per_ms; +const min_tool_display_ms: u64 = 1500; const clear_eol = ansi.clear_eol; const max_args_bytes: usize = 256; @@ -45,8 +45,8 @@ const ToolState = struct { name_len: usize = 0, args_buf: [max_args_bytes]u8 = undefined, args_len: usize = 0, - /// Wall-clock at which `setTool` last fired; gates dwell-honoring. - set_ns: i128 = 0, + /// Time at which `setTool` last fired; gates dwell-honoring. + set_ms: u64 = 0, /// Worker should flip back to thinking once dwell elapses. A fresh /// `setTool` clears it, overriding the dwell with a new label. dwell_pending: bool = false, @@ -76,7 +76,7 @@ frame: u8 = 0, paused: bool = false, tool_calls: u32 = 0, -turn_started_ns: i128 = 0, +turn_started_ms: u64 = 0, thread: ?std.Thread = null, should_exit: bool = false, @@ -112,7 +112,7 @@ pub fn start(self: *Spinner) void { self.paused = false; self.frame = 0; self.tool_calls = 0; - self.turn_started_ns = std.Io.Clock.now(.real, lp.io).toNanoseconds(); + self.turn_started_ms = lp.datetime.milliTimestamp(.boot); self.ensureWorkerLocked(); self.cv.signal(lp.io); } @@ -136,8 +136,8 @@ pub fn stop(self: *Spinner) void { self.mu.lockUncancelable(lp.io); defer self.mu.unlock(lp.io); if (self.state == .idle) return; - const elapsed_ns = std.Io.Clock.now(.real, lp.io).toNanoseconds() - self.turn_started_ns; - const elapsed_s = @as(f64, @floatFromInt(elapsed_ns)) / @as(f64, std.time.ns_per_s); + const elapsed_ms = lp.datetime.milliTimestamp(.boot) - self.turn_started_ms; + const elapsed_s = @as(f64, @floatFromInt(elapsed_ms)) / @as(f64, std.time.ms_per_s); var buf: [frame_buf_bytes]u8 = undefined; const summary = std.fmt.bufPrint( @@ -192,7 +192,7 @@ pub fn setTool(self: *Spinner, name: []const u8, args: []const u8) void { self.paused = false; const manual = self.state == .idle; self.tool_calls += 1; - var tool: ToolState = .{ .set_ns = std.Io.Clock.now(.real, lp.io).toNanoseconds(), .manual = manual }; + var tool: ToolState = .{ .set_ms = lp.datetime.milliTimestamp(.boot), .manual = manual }; const name_prefix = truncateUtf8(name, tool.name_buf.len); tool.name_len = name_prefix.len; @memcpy(tool.name_buf[0..name_prefix.len], name_prefix); @@ -213,7 +213,7 @@ pub fn setTool(self: *Spinner, name: []const u8, args: []const u8) void { } /// Request a transition back to the cycling "thinking" state. The worker -/// honors `min_tool_display_ns`: if the current tool label has not been up +/// honors `min_tool_display_ms`: if the current tool label has not been up /// long enough, the flip is deferred until it has. pub fn setThinking(self: *Spinner) void { if (!self.isEnabled()) return; @@ -254,10 +254,8 @@ fn workerLoop(self: *Spinner) void { switch (self.state) { .tool => { if (self.state.tool.dwell_pending) { - // Signed compare: a backward clock jump (NTP slew, suspend/resume) - // can make the delta negative; `@intCast` to u64 would panic. - const delta: i128 = std.Io.Clock.now(.real, lp.io).toNanoseconds() - self.state.tool.set_ns; - if (delta >= @as(i128, min_tool_display_ns)) { + const delta = lp.datetime.milliTimestamp(.boot) - self.state.tool.set_ms; + if (delta >= min_tool_display_ms) { self.state = .thinking; } } diff --git a/src/browser/Frame.zig b/src/browser/Frame.zig index a9e7adfa3..844043250 100644 --- a/src/browser/Frame.zig +++ b/src/browser/Frame.zig @@ -68,9 +68,6 @@ const NavigationKind = @import("webapi/navigation/root.zig").NavigationKind; const HttpClient = @import("../network/HttpClient.zig"); const sys_url = @import("../sys/url.zig"); -const timestamp = @import("../datetime.zig").timestamp; -const milliTimestamp = @import("../datetime.zig").milliTimestamp; - const GlobalEventHandlersLookup = @import("webapi/global_event_handlers.zig").Lookup; pub const parse = @import("frame/parse.zig"); @@ -710,7 +707,7 @@ pub fn navigate(self: *Frame, request_url: [:0]const u8, opts: NavigateOpts) !vo .frame_id = self._frame_id, .loader_id = self._loader_id, .url = request_url, - .timestamp = timestamp(.monotonic), + .timestamp = lp.datetime.timestamp(.boot), }); self.recordNavigateTelemetry(false); @@ -725,7 +722,7 @@ pub fn navigate(self: *Frame, request_url: [:0]const u8, opts: NavigateOpts) !vo .method = opts.method, }, .url = request_url, - .timestamp = timestamp(.monotonic), + .timestamp = lp.datetime.timestamp(.boot), }); if (self.parent == null) { @@ -808,7 +805,7 @@ pub fn navigate(self: *Frame, request_url: [:0]const u8, opts: NavigateOpts) !vo .req_id = transfer.id, .frame_id = self._frame_id, .loader_id = self._loader_id, - .timestamp = timestamp(.monotonic), + .timestamp = lp.datetime.timestamp(.boot), .is_pending_root = is_pending_root, }); @@ -1070,7 +1067,7 @@ pub fn _documentIsLoaded(self: *Frame) !void { .req_id = self._req_id, .frame_id = self._frame_id, .loader_id = self._loader_id, - .timestamp = timestamp(.monotonic), + .timestamp = lp.datetime.timestamp(.boot), }); } @@ -1180,7 +1177,7 @@ fn _documentIsComplete(self: *Frame) !void { .req_id = self._req_id, .frame_id = self._frame_id, .loader_id = self._loader_id, - .timestamp = timestamp(.monotonic), + .timestamp = lp.datetime.timestamp(.boot), }); if (self._event_manager.hasDirectListeners(window_target, "pageshow", self.window._on_pageshow)) { @@ -1277,7 +1274,7 @@ fn frameHeaderDoneCallback(transfer: *HttpClient.Transfer) !HttpClient.Transfer. .req_id = self._req_id, .frame_id = self._frame_id, .loader_id = self._loader_id, - .timestamp = timestamp(.monotonic), + .timestamp = lp.datetime.timestamp(.boot), }); } @@ -1692,7 +1689,7 @@ fn frameErrorCallback(ctx: *anyopaque, err: anyerror) void { self._session.notification.dispatch(.frame_navigate_failed, &.{ .frame_id = self._frame_id, .loader_id = self._loader_id, - .timestamp = timestamp(.monotonic), + .timestamp = lp.datetime.timestamp(.boot), .url = self.url, .err = err, .opts = self._navigated_options orelse .{}, @@ -1809,7 +1806,7 @@ pub fn iframeAddedCallback(self: *Frame, iframe: *IFrame) !void { .parent_id = self._frame_id, .frame_id = new_frame._frame_id, .loader_id = new_frame._loader_id, - .timestamp = timestamp(.monotonic), + .timestamp = lp.datetime.timestamp(.boot), }); const url = blk: { @@ -2325,7 +2322,7 @@ pub fn notifyNetworkIdle(self: *Frame) void { .req_id = self._req_id, .frame_id = self._frame_id, .loader_id = self._loader_id, - .timestamp = timestamp(.monotonic), + .timestamp = lp.datetime.timestamp(.boot), }); } @@ -2335,7 +2332,7 @@ pub fn notifyNetworkAlmostIdle(self: *Frame) void { .req_id = self._req_id, .frame_id = self._frame_id, .loader_id = self._loader_id, - .timestamp = timestamp(.monotonic), + .timestamp = lp.datetime.timestamp(.boot), }); } @@ -3041,13 +3038,13 @@ const IdleNotification = union(enum) { // the first time after being un-triggered). Record the time // so that if the condition holds for long enough, we can // send a notification. - self.* = .{ .triggered = milliTimestamp(.monotonic) }; + self.* = .{ .triggered = lp.datetime.milliTimestamp(.boot) }; }, .triggered => |ms| { // The condition was already triggered and was triggered // again. When this condition holds for 500+ms, we'll send // a notification. - if (milliTimestamp(.monotonic) - ms >= 500) { + if (lp.datetime.milliTimestamp(.boot) - ms >= 500) { // This is the only place in this function where we can // return true. The only place where we can tell our caller // "send the notification!". diff --git a/src/browser/js/Scheduler.zig b/src/browser/js/Scheduler.zig index 42acb1680..6a6170917 100644 --- a/src/browser/js/Scheduler.zig +++ b/src/browser/js/Scheduler.zig @@ -19,7 +19,6 @@ const std = @import("std"); const lp = @import("lightpanda"); const builtin = @import("builtin"); -const milliTimestamp = @import("../../datetime.zig").milliTimestamp; const log = lp.log; const IS_DEBUG = builtin.mode == .Debug; @@ -85,7 +84,7 @@ pub fn add(self: *Scheduler, ctx: *anyopaque, cb: Callback, run_in_ms: u32, opts .sequence = seq, .name = opts.name, .finalizer = opts.finalizer, - .run_at = milliTimestamp(.monotonic) + run_in_ms, + .run_at = lp.datetime.milliTimestamp(.boot) + run_in_ms, }); } @@ -95,13 +94,13 @@ pub fn run(self: *Scheduler) !void { } pub fn hasReadyTasks(self: *Scheduler) bool { - const now = milliTimestamp(.monotonic); + const now = lp.datetime.milliTimestamp(.boot); return queueHasReadyTask(&self.low_priority, now) or queueHasReadyTask(&self.high_priority, now); } pub fn msToNext(self: *Scheduler) ?u64 { var next: ?u64 = null; - const now = milliTimestamp(.monotonic); + const now = lp.datetime.milliTimestamp(.boot); for ([_]*Queue{ &self.high_priority, &self.low_priority }) |queue| { const task = queue.peek() orelse continue; const ms = if (task.run_at <= now) 0 else task.run_at - now; @@ -116,7 +115,7 @@ fn runQueue(self: *Scheduler, queue: *Queue) !void { if (queue.count() == 0) { return; } - const start = milliTimestamp(.monotonic); + const start = lp.datetime.milliTimestamp(.boot); var now = start; while (queue.peek()) |*task_| { @@ -144,7 +143,7 @@ fn runQueue(self: *Scheduler, queue: *Queue) !void { try self.low_priority.push(self.allocator, task); } - now = milliTimestamp(.monotonic); + now = lp.datetime.milliTimestamp(.boot); if (now - start > 500) { return; } diff --git a/src/browser/webapi/Console.zig b/src/browser/webapi/Console.zig index e53f2bdc5..5ea991527 100644 --- a/src/browser/webapi/Console.zig +++ b/src/browser/webapi/Console.zig @@ -21,7 +21,6 @@ const lp = @import("lightpanda"); const js = @import("../js/js.zig"); const Notification = @import("../../Notification.zig"); -const datetime = @import("../../datetime.zig"); const logger = lp.log; @@ -34,7 +33,7 @@ pub const init: Console = .{}; fn dispatchConsoleMessage(values: []js.Value, console_type: Notification.ConsoleMessageType, exec: *js.Execution) void { const notification = exec.session.notification; - const ts = datetime.timestamp(.monotonic); + const ts = lp.datetime.timestamp(.boot); notification.dispatch(.console_message, &.{ .source = .javascript, @@ -133,11 +132,11 @@ pub fn time(self: *Console, label_: ?[]const u8, exec: *js.Execution) !void { return; } gop.key_ptr.* = try exec.arena.dupe(u8, label); - gop.value_ptr.* = timestamp(); + gop.value_ptr.* = lp.datetime.timestamp(.boot); } pub fn timeLog(self: *Console, label_: ?[]const u8) void { - const elapsed = timestamp(); + const elapsed = lp.datetime.timestamp(.boot); const label = label_ orelse "default"; const start = self._timers.get(label) orelse { logger.info(.js, "console.timeLog", .{ .label = label, .err = "invalid timer" }); @@ -147,7 +146,7 @@ pub fn timeLog(self: *Console, label_: ?[]const u8) void { } pub fn timeEnd(self: *Console, label_: ?[]const u8) void { - const elapsed = timestamp(); + const elapsed = lp.datetime.timestamp(.boot); const label = label_ orelse "default"; const kv = self._timers.fetchRemove(label) orelse { logger.info(.js, "console.timeEnd", .{ .label = label, .err = "invalid timer" }); @@ -166,11 +165,6 @@ pub fn groupCollapsed(_: *const Console, values: []js.Value) void { } pub fn groupEnd(_: *const Console) void {} - -fn timestamp() u64 { - return @import("../../datetime.zig").timestamp(.monotonic); -} - const ValueWriter = struct { values: []js.Value, stack: ?[]const u8 = null, diff --git a/src/browser/webapi/Document.zig b/src/browser/webapi/Document.zig index 0ec5ac42d..6a4712408 100644 --- a/src/browser/webapi/Document.zig +++ b/src/browser/webapi/Document.zig @@ -196,7 +196,8 @@ pub fn getLastModified(self: *const Document, frame: *Frame) ![]const u8 { } } } - break :blk std.Io.Clock.now(.real, lp.io).toSeconds(); + // localTime is signed: a parsed Last-Modified can predate the epoch + break :blk @as(i64, @intCast(lp.datetime.timestamp(.real))); }; const tm = try dt.localTime(timestamp); @@ -307,7 +308,7 @@ pub fn setCookie(self: *Document, cookie_str: []const u8, frame: *Frame) ![]cons c.deinit(); return ""; // HttpOnly cookies cannot be set from JS } - try frame._session.cookie_jar.add(c, std.Io.Clock.now(.real, lp.io).toSeconds(), false); + try frame._session.cookie_jar.add(c, lp.datetime.timestamp(.real), false); return cookie_str; } diff --git a/src/browser/webapi/File.zig b/src/browser/webapi/File.zig index 6b1dcf53b..6a8b8958a 100644 --- a/src/browser/webapi/File.zig +++ b/src/browser/webapi/File.zig @@ -54,7 +54,7 @@ pub fn init( file.* = .{ ._proto = blob, ._name = try blob._arena.dupe(u8, name), - ._last_modified = opts.lastModified orelse std.Io.Clock.now(.real, lp.io).toMilliseconds(), + ._last_modified = opts.lastModified orelse @intCast(lp.datetime.milliTimestamp(.real)), }; blob._type = .{ .file = file }; diff --git a/src/browser/webapi/Performance.zig b/src/browser/webapi/Performance.zig index 6026ac82d..8ba0c85ec 100644 --- a/src/browser/webapi/Performance.zig +++ b/src/browser/webapi/Performance.zig @@ -20,7 +20,6 @@ const std = @import("std"); const lp = @import("lightpanda"); const js = @import("../js/js.zig"); -const datetime = @import("../../datetime.zig"); const EventCounts = @import("EventCounts.zig"); const PerformanceObserver = @import("PerformanceObserver.zig"); @@ -48,7 +47,7 @@ _delivery_scheduled: bool = false, /// Get high-resolution timestamp in microseconds, rounded to 5μs increments /// to match browser behavior (prevents fingerprinting) pub fn highResTimestamp() u64 { - const micros = datetime.microTimestamp(.monotonic); + const micros = lp.datetime.microTimestamp(.boot); // Round to nearest 5 microseconds (like Firefox default) const rounded = @divTrunc(micros + 2, 5) * 5; return rounded; diff --git a/src/browser/webapi/net/FormData.zig b/src/browser/webapi/net/FormData.zig index 0db380690..6335ba068 100644 --- a/src/browser/webapi/net/FormData.zig +++ b/src/browser/webapi/net/FormData.zig @@ -215,7 +215,7 @@ fn fileFrom(source: *Blob, name: []const u8, page: *Page) !*File { file.* = .{ ._proto = blob, ._name = try blob._arena.dupe(u8, name), - ._last_modified = std.Io.Clock.now(.real, lp.io).toMilliseconds(), + ._last_modified = @intCast(lp.datetime.milliTimestamp(.real)), }; blob._type = .{ .file = file }; blob.acquireRef(); diff --git a/src/browser/webapi/storage/Cookie.zig b/src/browser/webapi/storage/Cookie.zig index d1e0dcc98..f5e1281fd 100644 --- a/src/browser/webapi/storage/Cookie.zig +++ b/src/browser/webapi/storage/Cookie.zig @@ -179,7 +179,7 @@ pub fn parse(allocator: Allocator, url: [:0]const u8, str: []const u8) !Cookie { var normalized_expires: ?f64 = null; if (max_age) |ma| { - normalized_expires = @floatFromInt(std.Io.Clock.now(.real, lp.io).toSeconds() + ma); + normalized_expires = @as(f64, @floatFromInt(lp.datetime.timestamp(.real))) + @as(f64, @floatFromInt(ma)); } else { // max age takes priority over expires if (expires) |expires_| { @@ -484,7 +484,7 @@ pub const Jar = struct { pub fn add( self: *Jar, cookie: Cookie, - request_time: i64, + request_time: u64, /// Checks if addition comes from HTTP request or JS context. comptime is_http: bool, ) !void { @@ -557,9 +557,9 @@ pub const Jar = struct { }); } - pub fn removeExpired(self: *Jar, request_time: ?i64) void { + pub fn removeExpired(self: *Jar, request_time: ?u64) void { if (self.cookies.items.len == 0) return; - const time = request_time orelse std.Io.Clock.now(.real, lp.io).toSeconds(); + const time = request_time orelse lp.datetime.timestamp(.real); var i: usize = self.cookies.items.len; while (i > 0) { i -= 1; @@ -572,7 +572,7 @@ pub const Jar = struct { pub const LookupOpts = struct { is_http: bool, - request_time: ?i64 = null, + request_time: ?u64 = null, is_navigation: bool = true, prefix: ?[]const u8 = null, origin_url: ?[:0]const u8 = null, @@ -608,7 +608,7 @@ pub const Jar = struct { return; }; - const now = std.Io.Clock.now(.real, lp.io).toSeconds(); + const now = lp.datetime.timestamp(.real); try self.add(c, now, true); } @@ -623,7 +623,7 @@ pub const Jar = struct { } }; -fn isCookieExpired(cookie: *const Cookie, now: i64) bool { +fn isCookieExpired(cookie: *const Cookie, now: u64) bool { const ce = cookie.expires orelse return false; return ce <= @as(f64, @floatFromInt(now)); } @@ -734,7 +734,7 @@ test "Jar: add" { } }.expect; - const now = std.Io.Clock.now(.real, lp.io).toSeconds(); + const now = lp.datetime.timestamp(.real); var jar = Jar.init(testing.allocator, null); defer jar.deinit(); @@ -766,7 +766,7 @@ test "Jar: add" { } test "Jar: non-HTTP add must not replace or duplicate an HttpOnly cookie" { - const now = std.Io.Clock.now(.real, lp.io).toSeconds(); + const now = lp.datetime.timestamp(.real); var jar = Jar.init(testing.allocator, null); defer jar.deinit(); @@ -788,7 +788,7 @@ test "Jar: add limit" { var jar = Jar.init(testing.allocator, null); defer jar.deinit(); - const now = std.Io.Clock.now(.real, lp.io).toSeconds(); + const now = lp.datetime.timestamp(.real); // add a too big cookie value. try testing.expectError(error.CookieSizeExceeded, jar.add(.{ @@ -845,7 +845,7 @@ test "Jar: forRequest" { } }.expect; - const now = std.Io.Clock.now(.real, lp.io).toSeconds(); + const now = lp.datetime.timestamp(.real); var jar = Jar.init(testing.allocator, null); defer jar.deinit(); @@ -997,7 +997,7 @@ test "Jar: forRequest SameSite=Strict on cross-site navigation" { defer jar.deinit(); const victim_url: [:0]const u8 = "http://victim.example/"; - try jar.add(try Cookie.parse(testing.allocator, victim_url, "sid=STRICT_COOKIE; Path=/; SameSite=Strict"), std.Io.Clock.now(.real, lp.io).toSeconds(), true); + try jar.add(try Cookie.parse(testing.allocator, victim_url, "sid=STRICT_COOKIE; Path=/; SameSite=Strict"), lp.datetime.timestamp(.real), true); // Same-site navigation: cookie included. try expectCookies("sid=STRICT_COOKIE", &jar, "http://victim.example/transfer", .{ @@ -1170,13 +1170,15 @@ test "Cookie: parse max-age" { try expectAttribute(.{ .expires = null }, null, "b;max-age=13.22"); try expectAttribute(.{ .expires = null }, null, "b;max-age=13abc"); - try expectAttribute(.{ .expires = std.Io.Clock.now(.real, lp.io).toSeconds() + 13 }, null, "b;max-age=13"); - try expectAttribute(.{ .expires = std.Io.Clock.now(.real, lp.io).toSeconds() + -22 }, null, "b;max-age=-22"); - try expectAttribute(.{ .expires = std.Io.Clock.now(.real, lp.io).toSeconds() + 4294967296 }, null, "b;max-age=4294967296"); - try expectAttribute(.{ .expires = std.Io.Clock.now(.real, lp.io).toSeconds() + -4294967296 }, null, "b;Max-Age= -4294967296"); - try expectAttribute(.{ .expires = std.Io.Clock.now(.real, lp.io).toSeconds() + 0 }, null, "b; Max-Age=0"); - try expectAttribute(.{ .expires = std.Io.Clock.now(.real, lp.io).toSeconds() + 500 }, null, "b; Max-Age = 500 ; Max-Age=invalid"); - try expectAttribute(.{ .expires = std.Io.Clock.now(.real, lp.io).toSeconds() + 1000 }, null, "b;max-age=600;max-age=0;max-age = 1000"); + // max-age can be negative, so the expected expiry has to be signed + const now: i64 = @intCast(lp.datetime.timestamp(.real)); + try expectAttribute(.{ .expires = now + 13 }, null, "b;max-age=13"); + try expectAttribute(.{ .expires = now + -22 }, null, "b;max-age=-22"); + try expectAttribute(.{ .expires = now + 4294967296 }, null, "b;max-age=4294967296"); + try expectAttribute(.{ .expires = now + -4294967296 }, null, "b;Max-Age= -4294967296"); + try expectAttribute(.{ .expires = now + 0 }, null, "b; Max-Age=0"); + try expectAttribute(.{ .expires = now + 500 }, null, "b; Max-Age = 500 ; Max-Age=invalid"); + try expectAttribute(.{ .expires = now + 1000 }, null, "b;max-age=600;max-age=0;max-age = 1000"); } test "Cookie: parse expires" { @@ -1188,7 +1190,7 @@ test "Cookie: parse expires" { try expectAttribute(.{ .expires = 1918798080 }, null, "b;expires=Wed, 21 Oct 2030 07:28:00 GMT"); try expectAttribute(.{ .expires = 1784275395 }, null, "b;expires=Fri, 17-Jul-2026 08:03:15 GMT"); // max-age has priority over expires - try expectAttribute(.{ .expires = std.Io.Clock.now(.real, lp.io).toSeconds() + 10 }, null, "b;Max-Age=10; expires=Wed, 21 Oct 2030 07:28:00 GMT"); + try expectAttribute(.{ .expires = lp.datetime.timestamp(.real) + 10 }, null, "b;Max-Age=10; expires=Wed, 21 Oct 2030 07:28:00 GMT"); } test "Cookie: parse all" { @@ -1206,7 +1208,7 @@ test "Cookie: parse all" { .http_only = true, .secure = true, .domain = ".lightpanda.io", - .expires = @floatFromInt(std.Io.Clock.now(.real, lp.io).toSeconds() + 30), + .expires = @floatFromInt(lp.datetime.timestamp(.real) + 30), }, "https://lightpanda.io/cms/users", "user-id=9000; HttpOnly; Max-Age=30; Secure; path=/; Domain=lightpanda.io"); try expectCookie(.{ @@ -1217,7 +1219,7 @@ test "Cookie: parse all" { .secure = false, .domain = ".localhost", .same_site = .lax, - .expires = @floatFromInt(std.Io.Clock.now(.real, lp.io).toSeconds() + 7200), + .expires = @floatFromInt(lp.datetime.timestamp(.real) + 7200), }, "http://localhost:8000/login", "app_session=123; Max-Age=7200; path=/; domain=localhost; httponly; samesite=lax"); } diff --git a/src/browser/webapi/storage/CookieStore.zig b/src/browser/webapi/storage/CookieStore.zig index 4bb67983b..54bed460d 100644 --- a/src/browser/webapi/storage/CookieStore.zig +++ b/src/browser/webapi/storage/CookieStore.zig @@ -431,12 +431,12 @@ fn storeCookie(exec: *const Execution, init_: CookieInit, is_delete: bool) !void } // convert to absolute time, so the rest of the code is shared for // maxAge and expires. - init.expires = (@as(f64, @floatFromInt(std.Io.Clock.now(.real, lp.io).toSeconds())) + max_age) * 1000.0; + init.expires = (@as(f64, @floatFromInt(lp.datetime.timestamp(.real))) + max_age) * 1000.0; } if (init.expires) |ms| { // 400 day expiry limit, per spec. - const cap_ms = (@as(f64, @floatFromInt(std.Io.Clock.now(.real, lp.io).toSeconds())) + 400 * std.time.s_per_day) * 1000.0; + const cap_ms = (@as(f64, @floatFromInt(lp.datetime.timestamp(.real))) + 400 * std.time.s_per_day) * 1000.0; init.expires = @min(ms, cap_ms); } @@ -557,7 +557,7 @@ fn storeCookie(exec: *const Execution, init_: CookieInit, is_delete: bool) !void }; // CookieStore is a script API, so is_http = false. - try session.cookie_jar.add(cookie, std.Io.Clock.now(.real, lp.io).toSeconds(), false); + try session.cookie_jar.add(cookie, lp.datetime.timestamp(.real), false); } // Control characters (U+0000–U+001F and U+007F DEL) and `;` cannot appear in diff --git a/src/cdp/domains/network.zig b/src/cdp/domains/network.zig index 508f29c80..ee832ed95 100644 --- a/src/cdp/domains/network.zig +++ b/src/cdp/domains/network.zig @@ -27,7 +27,6 @@ const Config = @import("../../Config.zig"); const URL = @import("../../browser/URL.zig"); const Mime = @import("../../browser/Mime.zig"); const Notification = @import("../../Notification.zig"); -const timestamp = @import("../../datetime.zig").timestamp; const HttpClient = @import("../../network/HttpClient.zig"); const Cache = @import("../../network/cache/Cache.zig"); @@ -328,7 +327,7 @@ pub fn httpRequestFail(bc: *CDP.BrowserContext, msg: *const Notification.Request // We're missing a bunch of fields, but, for now, this seems like enough try bc.cdp.sendEvent("Network.loadingFailed", .{ .requestId = &id.toRequestId(msg.transfer), - .timestamp = timestamp(.monotonic), + .timestamp = lp.datetime.timestamp(.boot), // Seems to be what chrome answers with. I assume it depends on the type of error? .type = "Ping", .errorText = msg.err, @@ -365,8 +364,8 @@ pub fn httpRequestStart(bc: *CDP.BrowserContext, msg: *const Notification.Reques .initiator = .{ .type = "other" }, .redirectHasExtraInfo = false, // TODO change after adding Network.requestWillBeSentExtraInfo .hasUserGesture = false, - .timestamp = timestamp(.monotonic), - .wallTime = timestamp(.clock), + .timestamp = lp.datetime.timestamp(.boot), + .wallTime = lp.datetime.timestamp(.real), }, .{ .session_id = session_id }); } @@ -383,7 +382,7 @@ pub fn httpResponseHeaderDone(arena: Allocator, bc: *CDP.BrowserContext, msg: *c .frameId = &id.toFrameId(req.document_frame_id orelse req.frame_id), .requestId = &id.toRequestId(transfer), .loaderId = &id.toLoaderId(req.loader_id), - .timestamp = timestamp(.monotonic), + .timestamp = lp.datetime.timestamp(.boot), .type = req.resource_type.string(), .response = ResponseWriter.init(arena, msg.transfer), .hasExtraInfo = false, // TODO change after adding Network.responseReceivedExtraInfo @@ -396,7 +395,7 @@ pub fn httpRequestDone(bc: *CDP.BrowserContext, msg: *const Notification.Request const session_id = bc.session_id orelse return; try bc.cdp.sendEvent("Network.loadingFinished", .{ .requestId = &id.toRequestId(msg.transfer), - .timestamp = timestamp(.monotonic), + .timestamp = lp.datetime.timestamp(.boot), .encodedDataLength = msg.content_length, }, .{ .session_id = session_id }); } diff --git a/src/cdp/domains/page.zig b/src/cdp/domains/page.zig index c187b658b..aa7b53a08 100644 --- a/src/cdp/domains/page.zig +++ b/src/cdp/domains/page.zig @@ -29,7 +29,6 @@ const CDP = @import("../CDP.zig"); const js = @import("../../browser/js/js.zig"); const URL = @import("../../browser/URL.zig"); const Frame = @import("../../browser/Frame.zig"); -const timestampF = @import("../../datetime.zig").timestamp; const Notification = @import("../../Notification.zig"); const log = lp.log; @@ -123,7 +122,7 @@ fn setLifecycleEventsEnabled(cmd: *CDP.Command) !void { const frame_id = &id.toFrameId(frame._frame_id); const loader_id = &id.toLoaderId(frame._loader_id); - const now = timestampF(.monotonic); + const now = lp.datetime.timestamp(.boot); try sendPageLifecycle(bc, "DOMContentLoaded", now, frame_id, loader_id); try sendPageLifecycle(bc, "load", now, frame_id, loader_id); diff --git a/src/cdp/domains/storage.zig b/src/cdp/domains/storage.zig index aabd5d9d8..c21faa3bd 100644 --- a/src/cdp/domains/storage.zig +++ b/src/cdp/domains/storage.zig @@ -171,7 +171,7 @@ pub fn setCdpCookie(cookie_jar: *CookieJar, param: CdpCookie) !void { }, }; }; - try cookie_jar.add(cookie, std.Io.Clock.now(.real, lp.io).toSeconds(), true); + try cookie_jar.add(cookie, lp.datetime.timestamp(.real), true); } pub const CookieWriter = struct { diff --git a/src/cookies.zig b/src/cookies.zig index 33b43d321..ef493b52d 100644 --- a/src/cookies.zig +++ b/src/cookies.zig @@ -51,7 +51,7 @@ fn _loadFromFile(session: *Session, path: []const u8) !void { }; const jar = &session.cookie_jar; - const now = std.Io.Clock.now(.real, lp.io).toSeconds(); + const now = lp.datetime.timestamp(.real); var loaded: usize = 0; for (json_cookies) |jc| { diff --git a/src/datetime.zig b/src/datetime.zig index 3e8aad559..52526719b 100644 --- a/src/datetime.zig +++ b/src/datetime.zig @@ -291,7 +291,7 @@ pub const DateTime = struct { pub fn now() DateTime { return .{ - .micros = @intCast(microTimestamp(.clock)), + .micros = @intCast(microTimestamp(.real)), }; } @@ -522,30 +522,16 @@ pub const DateTime = struct { } }; -pub const TimestampMode = enum { - clock, - monotonic, - - // .boot intends to keep counting across system suspend, matching the - // old CLOCK_BOOTTIME / UPTIME_RAW choice. - fn ioClock(comptime mode: TimestampMode) std.Io.Clock { - return switch (mode) { - .clock => .real, - .monotonic => .boot, - }; - } -}; - -pub fn timestamp(comptime mode: TimestampMode) u64 { - return @intCast(mode.ioClock().now(lp.io).toSeconds()); +pub fn timestamp(comptime clock: std.Io.Clock) u64 { + return @intCast(clock.now(lp.io).toSeconds()); } -pub fn milliTimestamp(comptime mode: TimestampMode) u64 { - return @intCast(mode.ioClock().now(lp.io).toMilliseconds()); +pub fn milliTimestamp(comptime clock: std.Io.Clock) u64 { + return @intCast(clock.now(lp.io).toMilliseconds()); } -pub fn microTimestamp(comptime mode: TimestampMode) u64 { - return @intCast(mode.ioClock().now(lp.io).toMicroseconds()); +pub fn microTimestamp(comptime clock: std.Io.Clock) u64 { + return @intCast(clock.now(lp.io).toMicroseconds()); } fn writeDate(into: []u8, date: Date) u8 { @@ -1239,7 +1225,7 @@ test "DateTime: initUTC" { test "DateTime: now" { const dt = DateTime.now(); - try testing.expectDelta(@as(i64, @intCast(microTimestamp(.clock))), dt.micros, 1000); + try testing.expectDelta(@as(i64, @intCast(microTimestamp(.real))), dt.micros, 1000); } test "DateTime: date" { diff --git a/src/log.zig b/src/log.zig index 87fe23573..eb7aebb41 100644 --- a/src/log.zig +++ b/src/log.zig @@ -155,7 +155,7 @@ pub fn log(scope: Scope, level: Level, msg: []const u8, data: anytype) void { var buf: [4096]u8 = undefined; var w: std.Io.Writer = .fixed(&buf); logTo(scope, level, msg, data, &w) catch |log_err| { - std.debug.print("$time={d} $level=fatal $scope={s} $msg=\"log err\" err={s} log_msg=\"{s}\"\n", .{ timestamp(.clock), @tagName(scope), @errorName(log_err), msg }); + std.debug.print("$time={d} $level=fatal $scope={s} $msg=\"log err\" err={s} log_msg=\"{s}\"\n", .{ timestamp(.real), @tagName(scope), @errorName(log_err), msg }); return; }; s(w.buffered()); @@ -167,7 +167,7 @@ pub fn log(scope: Scope, level: Level, msg: []const u8, data: anytype) void { defer std.debug.unlockStderr(); logTo(scope, level, msg, data, &stderr.file_writer.interface) catch |log_err| { - std.debug.print("$time={d} $level=fatal $scope={s} $msg=\"log err\" err={s} log_msg=\"{s}\"\n", .{ timestamp(.clock), @errorName(log_err), @tagName(scope), msg }); + std.debug.print("$time={d} $level=fatal $scope={s} $msg=\"log err\" err={s} log_msg=\"{s}\"\n", .{ timestamp(.real), @errorName(log_err), @tagName(scope), msg }); }; } @@ -223,7 +223,7 @@ fn logLogfmt(scope: Scope, level: Level, msg: []const u8, kvs: []const KV, write fn logLogFmtPrefix(scope: Scope, level: Level, msg: []const u8, writer: *std.Io.Writer) !void { try writer.writeAll("$time="); - try writer.print("{d}", .{timestamp(.clock)}); + try writer.print("{d}", .{timestamp(.real)}); try writer.writeAll(" $scope="); try writer.writeAll(@tagName(scope)); @@ -494,7 +494,7 @@ pub const LogFormatWriter = struct { var first_log: std.atomic.Value(u64) = .init(0); fn elapsed() struct { time: f64, unit: []const u8 } { - const now = timestamp(.monotonic); + const now = timestamp(.boot); var first = first_log.load(.monotonic); if (first == 0) { @@ -509,11 +509,11 @@ fn elapsed() struct { time: f64, unit: []const u8 } { } const datetime = @import("datetime.zig"); -fn timestamp(comptime mode: datetime.TimestampMode) u64 { +fn timestamp(comptime clock: std.Io.Clock) u64 { if (IS_TEST) { return 1739795092929; } - return datetime.milliTimestamp(mode); + return datetime.milliTimestamp(clock); } const testing = @import("testing.zig"); diff --git a/src/network/HttpClient.zig b/src/network/HttpClient.zig index bca67a13b..6d9d856ba 100644 --- a/src/network/HttpClient.zig +++ b/src/network/HttpClient.zig @@ -23,7 +23,6 @@ const builtin = @import("builtin"); const Inbox = @import("../Inbox.zig"); const ArenaPool = @import("../ArenaPool.zig"); const Notification = @import("../Notification.zig"); -const timestamp = @import("../datetime.zig").timestamp; const CDP = @import("../cdp/CDP.zig"); const Watchdog = @import("../Watchdog.zig"); @@ -593,7 +592,7 @@ pub fn newRequest(self: *Client, req: Request, owner: ?*Owner) anyerror!*Transfe .client = self, .arena = arena, .id = self.incrReqId(), - .start_time = timestamp(.monotonic), + .start_time = lp.datetime.timestamp(.boot), // owner is set AFTER we've actually appended to the owner list, // so transfer.deinit's `if (self.owner)` branch only fires when // we're truly linked. Otherwise we'd try to remove a node from @@ -928,7 +927,7 @@ fn cacheLookup(self: *Client, transfer: *Transfer) !bool { const cached = cache.get(arena, .{ .url = req.url, - .timestamp = std.Io.Clock.now(.real, lp.io).toSeconds(), + .timestamp = lp.datetime.timestamp(.real), .request_headers = req_headers.items, }) orelse { lp.metrics.http_cache.incr(.miss); @@ -985,7 +984,7 @@ fn cacheRevalidated(self: *Client, transfer: *Transfer) !bool { cache.renew(transfer.arena, .{ .url = transfer._cache_key, - .timestamp = std.Io.Clock.now(.real, lp.io).toSeconds(), + .timestamp = lp.datetime.timestamp(.real), .headers = transfer.res.headers, }) catch |err| { log.warn(.cache, "renew failed", .{ .err = err }); @@ -1023,7 +1022,7 @@ fn cacheStore(self: *Client, transfer: *Transfer) void { const vary = findHeader(headers, "vary"); const maybe_cm = Cache.tryCache( arena, - std.Io.Clock.now(.real, lp.io).toSeconds(), + lp.datetime.timestamp(.real), transfer._cache_key, rh.status, rh.contentType(), diff --git a/src/network/WebBotAuth.zig b/src/network/WebBotAuth.zig index 4d6016d54..68dd20858 100644 --- a/src/network/WebBotAuth.zig +++ b/src/network/WebBotAuth.zig @@ -94,7 +94,7 @@ pub fn signRequest( headers: *Http.Headers, authority: []const u8, ) !void { - const now = std.Io.Clock.now(.real, lp.io).toSeconds(); + const now = lp.datetime.timestamp(.real); const expires = now + 60; // build the signature-input value (without the sig1= label) diff --git a/src/network/cache/Cache.zig b/src/network/cache/Cache.zig index e46df8863..ebe95ca19 100644 --- a/src/network/cache/Cache.zig +++ b/src/network/cache/Cache.zig @@ -129,7 +129,7 @@ pub const CachedMetadata = struct { content_type: []const u8, status: u16, - stored_at: i64, + stored_at: u64, age_at_store: u64, cache_control: CacheControl, @@ -164,10 +164,11 @@ pub const CachedMetadata = struct { try writer.print("]", .{}); } - pub fn isStale(self: CachedMetadata, timestamp: i64) bool { + pub fn isStale(self: CachedMetadata, timestamp: u64) bool { if (self.cache_control.must_revalidate) return true; - const age = (timestamp - self.stored_at) + @as(i64, @intCast(self.age_at_store)); - return age >= @as(i64, @intCast(self.cache_control.max_age)); + // saturating: a backwards wall-clock jump leaves the entry fresh + const age = (timestamp -| self.stored_at) + self.age_at_store; + return age >= self.cache_control.max_age; } pub fn hasValidators(self: CachedMetadata) bool { @@ -197,13 +198,13 @@ pub const CachedMetadata = struct { pub const CacheRequest = struct { url: []const u8, - timestamp: i64, + timestamp: u64, request_headers: []const Http.Header, }; pub const RenewResponse = struct { url: []const u8, - timestamp: i64, + timestamp: u64, headers: []const Http.Header, }; @@ -246,7 +247,7 @@ pub const CachedResponse = struct { pub fn tryCache( arena: std.mem.Allocator, - timestamp: i64, + timestamp: u64, url: [:0]const u8, status: u16, content_type: ?[]const u8, diff --git a/src/network/cache/FsCache.zig b/src/network/cache/FsCache.zig index e10dc44e4..58d056716 100644 --- a/src/network/cache/FsCache.zig +++ b/src/network/cache/FsCache.zig @@ -372,7 +372,7 @@ test "FsCache: basic put and get" { var arena = std.heap.ArenaAllocator.init(testing.allocator); defer arena.deinit(); - const now = std.Io.Clock.now(.real, lp.io).toSeconds(); + const now = lp.datetime.timestamp(.real); const meta = CachedMetadata{ .url = "https://example.com", .content_type = "text/html", @@ -595,7 +595,7 @@ test "FsCache: vary hit and miss" { var arena = std.heap.ArenaAllocator.init(testing.allocator); defer arena.deinit(); - const now = std.Io.Clock.now(.real, lp.io).toSeconds(); + const now = lp.datetime.timestamp(.real); const meta = CachedMetadata{ .url = "https://example.com", .content_type = "text/html", @@ -656,7 +656,7 @@ test "FsCache: vary multiple headers" { var arena = std.heap.ArenaAllocator.init(testing.allocator); defer arena.deinit(); - const now = std.Io.Clock.now(.real, lp.io).toSeconds(); + const now = lp.datetime.timestamp(.real); const meta = CachedMetadata{ .url = "https://example.com", .content_type = "text/html", @@ -705,7 +705,7 @@ test "FsCache: clear removes all entries" { var arena = std.heap.ArenaAllocator.init(testing.allocator); defer arena.deinit(); - const now = std.Io.Clock.now(.real, lp.io).toSeconds(); + const now = lp.datetime.timestamp(.real); const base_meta_a = CachedMetadata{ .url = "https://example.com/a", .status = 200, @@ -786,7 +786,7 @@ test "FsCache: put after clear works" { var arena = std.heap.ArenaAllocator.init(testing.allocator); defer arena.deinit(); - const now = std.Io.Clock.now(.real, lp.io).toSeconds(); + const now = lp.datetime.timestamp(.real); const meta = CachedMetadata{ .url = "https://example.com", .content_type = "text/html", @@ -847,7 +847,7 @@ test "FsCache: evict removes entry" { var arena = std.heap.ArenaAllocator.init(testing.allocator); defer arena.deinit(); - const now = std.Io.Clock.now(.real, lp.io).toSeconds(); + const now = lp.datetime.timestamp(.real); const meta = CachedMetadata{ .url = "https://example.com", .content_type = "text/html", @@ -895,7 +895,7 @@ test "FsCache: renew refreshes expiry" { var arena = std.heap.ArenaAllocator.init(testing.allocator); defer arena.deinit(); - const now: i64 = 5000; + const now: u64 = 5000; const max_age: u64 = 1000; const meta = CachedMetadata{ @@ -958,7 +958,7 @@ test "FsCache: renew preserves body" { var arena = std.heap.ArenaAllocator.init(testing.allocator); defer arena.deinit(); - const now = std.Io.Clock.now(.real, lp.io).toSeconds(); + const now = lp.datetime.timestamp(.real); const meta = CachedMetadata{ .url = "https://example.com", .content_type = "text/html",