lib/bench/src/timing.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const sys = @import("sys");
3
4 pub const clock_probe_reads: u32 = 256;
5
6 pub const ClockProbe = struct {
7 source: []const u8 = "monotonic_awake",
8 reads: u32,
9 positive_deltas: u32,
10 minimum_delta_ns: u64,
11 span_ns: u64,
12 };
13
14 pub fn nowInstant() sys.time.AwakeInstant {
15 return readInstant() catch @panic("awake clock unavailable");
16 }
17
18 pub fn readInstant() !sys.time.AwakeInstant {
19 return sys.time.awakeNow();
20 }
21
22 pub fn nowNs() i128 {
23 return @intCast(nowInstant().asNanoseconds());
24 }
25
26 pub fn probeClock() !ClockProbe {
27 std.debug.assert(clock_probe_reads >= 2);
28 var timestamps: [clock_probe_reads]sys.time.AwakeInstant = undefined;
29 for (×tamps) |*timestamp| timestamp.* = try readInstant();
30 return analyzeClockProbe(×tamps);
31 }
32
33 pub fn realNowNs() u64 {
34 return (sys.time.wallNow() catch @panic("wall clock unavailable")).asNanoseconds();
35 }
36
37 pub fn elapsedNs(start: i128) u64 {
38 const end = nowNs();
39 return elapsedNsBetween(start, end);
40 }
41
42 pub fn elapsedNsBetween(start: i128, end: i128) u64 {
43 if (end <= start) return 0;
44 return saturatingU64(end - start);
45 }
46
47 pub fn nsOrZero(value: ?i128) u64 {
48 return saturatingU64(value orelse return 0);
49 }
50
51 pub fn saturatingU64(value_int: anytype) u64 {
52 if (value_int <= 0) return 0;
53 if (value_int > std.math.maxInt(u64)) return std.math.maxInt(u64);
54 return @intCast(value_int);
55 }
56
57 pub fn saturatingI64(value: u64) i64 {
58 return @intCast(@min(value, std.math.maxInt(i64)));
59 }
60
61 fn analyzeClockProbe(timestamps: []const sys.time.AwakeInstant) !ClockProbe {
62 if (timestamps.len < 2) return error.InsufficientClockReads;
63 var minimum_delta: u64 = std.math.maxInt(u64);
64 var positive_deltas: u32 = 0;
65 var previous = timestamps[0];
66 for (timestamps[1..]) |current| {
67 const delta = current.elapsedSince(previous) catch return error.ClockRegressed;
68 if (!delta.isZero()) {
69 minimum_delta = @min(minimum_delta, delta.asNanoseconds());
70 positive_deltas += 1;
71 }
72 previous = current;
73 }
74 if (positive_deltas == 0) return error.ClockNotAdvancing;
75 return .{
76 .reads = @intCast(timestamps.len),
77 .positive_deltas = positive_deltas,
78 .minimum_delta_ns = minimum_delta,
79 .span_ns = (try timestamps[timestamps.len - 1].elapsedSince(timestamps[0])).asNanoseconds(),
80 };
81 }
82
83 test "clock probe analysis retains positive minimum delta" {
84 const Instant = sys.time.AwakeInstant;
85 const probe = try analyzeClockProbe(&.{
86 Instant.fromNanoseconds(100),
87 Instant.fromNanoseconds(100),
88 Instant.fromNanoseconds(103),
89 Instant.fromNanoseconds(108),
90 Instant.fromNanoseconds(108),
91 });
92
93 try std.testing.expectEqual(@as(u32, 5), probe.reads);
94 try std.testing.expectEqual(@as(u32, 2), probe.positive_deltas);
95 try std.testing.expectEqual(@as(u64, 3), probe.minimum_delta_ns);
96 try std.testing.expectEqual(@as(u64, 8), probe.span_ns);
97 }
98
99 test "clock probe analysis rejects invalid clocks" {
100 const Instant = sys.time.AwakeInstant;
101 try std.testing.expectError(
102 error.InsufficientClockReads,
103 analyzeClockProbe(&.{Instant.fromNanoseconds(1)}),
104 );
105 try std.testing.expectError(error.ClockNotAdvancing, analyzeClockProbe(&.{
106 Instant.fromNanoseconds(5),
107 Instant.fromNanoseconds(5),
108 Instant.fromNanoseconds(5),
109 }));
110 try std.testing.expectError(error.ClockRegressed, analyzeClockProbe(&.{
111 Instant.fromNanoseconds(5),
112 Instant.fromNanoseconds(4),
113 }));
114 }
115
116 test "host clock probe advances" {
117 if (!sys.time.supportsAwakeClock()) return error.SkipZigTest;
118 const probe = try probeClock();
119
120 try std.testing.expectEqual(clock_probe_reads, probe.reads);
121 try std.testing.expect(probe.positive_deltas > 0);
122 try std.testing.expect(probe.minimum_delta_ns > 0);
123 try std.testing.expect(probe.span_ns >= probe.minimum_delta_ns);
124 }