Merge pull request #3061 from lightpanda-io/consistent-clocks

chore: make our use of clock/timestamp more consistent
This commit is contained in:
Karl Seguin
2026-07-28 09:20:18 +08:00
committed by GitHub
21 changed files with 110 additions and 137 deletions

View File

@@ -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 {

View File

@@ -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;
}
}

View File

@@ -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!".

View File

@@ -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;
}

View File

@@ -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,

View File

@@ -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;
}

View File

@@ -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 };

View File

@@ -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;

View File

@@ -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();

View File

@@ -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");
}

View File

@@ -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+0000U+001F and U+007F DEL) and `;` cannot appear in

View File

@@ -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 });
}

View File

@@ -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);

View File

@@ -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 {

View File

@@ -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| {

View File

@@ -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" {

View File

@@ -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");

View File

@@ -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(),

View File

@@ -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)

View File

@@ -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,

View File

@@ -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",