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 f0dd75881..cdb864359 100644 --- a/src/browser/Frame.zig +++ b/src/browser/Frame.zig @@ -74,9 +74,6 @@ const AnimatedPreserveAspectRatio = @import("webapi/svg/AnimatedPreserveAspectRa const sys_url = @import("../sys/url.zig"); const HttpClient = @import("../network/HttpClient.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"); @@ -731,7 +728,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); @@ -746,7 +743,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) { @@ -829,7 +826,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, }); @@ -1091,7 +1088,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), }); } @@ -1201,7 +1198,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)) { @@ -1298,7 +1295,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), }); } @@ -1713,7 +1710,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 .{}, @@ -1830,7 +1827,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: { @@ -2346,7 +2343,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), }); } @@ -2356,7 +2353,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), }); } @@ -3062,13 +3059,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",