lib/sys/src/time.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 const builtin = @import("builtin");
  3 const capabilities = @import("capabilities.zig");
  4 
  5 pub const required_capabilities = capabilities.noLibc(&.{.time});
  6 pub const ClockError = error{
  7     UnsupportedPlatform,
  8     ClockUnavailable,
  9     ClockRegressed,
 10     ClockOverflow,
 11 };
 12 
 13 pub const ExternalClockId = enum(u32) {
 14     _,
 15 
 16     pub fn fromNative(value: u32) ExternalClockId {
 17         return @fromBackingInt(@intCast(value));
 18     }
 19 
 20     pub fn toNative(self: ExternalClockId) u32 {
 21         return @backingInt(self);
 22     }
 23 };
 24 
 25 pub const real_clock_id = ExternalClockId.fromNative(
 26     @intCast(@backingInt(std.os.linux.clockid_t.REALTIME)),
 27 );
 28 pub const awake_clock_id = ExternalClockId.fromNative(
 29     @intCast(@backingInt(std.os.linux.clockid_t.MONOTONIC)),
 30 );
 31 pub const boot_clock_id = ExternalClockId.fromNative(
 32     @intCast(@backingInt(std.os.linux.clockid_t.BOOTTIME)),
 33 );
 34 
 35 pub const Duration = enum(u64) {
 36     _,
 37 
 38     pub const zero: Duration = @fromBackingInt(@intCast(0));
 39 
 40     pub fn fromNanoseconds(value: u64) Duration {
 41         return @fromBackingInt(@intCast(value));
 42     }
 43 
 44     pub fn fromMilliseconds(value: u64) Duration {
 45         return @fromBackingInt(@intCast(value *| std.time.ns_per_ms));
 46     }
 47 
 48     pub fn asNanoseconds(self: Duration) u64 {
 49         return @backingInt(self);
 50     }
 51 
 52     pub fn asMillisecondsFloor(self: Duration) u64 {
 53         return self.asNanoseconds() / std.time.ns_per_ms;
 54     }
 55 
 56     pub fn asMillisecondsCeil(self: Duration) u64 {
 57         const nanoseconds = self.asNanoseconds();
 58         if (nanoseconds == 0) return 0;
 59         return 1 + (nanoseconds - 1) / std.time.ns_per_ms;
 60     }
 61 
 62     pub fn isZero(self: Duration) bool {
 63         return self == zero;
 64     }
 65 
 66     pub fn min(self: Duration, other: Duration) Duration {
 67         return if (self.asNanoseconds() <= other.asNanoseconds()) self else other;
 68     }
 69 
 70     pub fn saturatingMultiply(self: Duration, factor: u64) Duration {
 71         return .fromNanoseconds(self.asNanoseconds() *| factor);
 72     }
 73 };
 74 
 75 pub const AwakeInstant = enum(u64) {
 76     _,
 77 
 78     pub const zero: AwakeInstant = @fromBackingInt(@intCast(0));
 79 
 80     pub fn fromNanoseconds(value: u64) AwakeInstant {
 81         return @fromBackingInt(@intCast(value));
 82     }
 83 
 84     pub fn asNanoseconds(self: AwakeInstant) u64 {
 85         return @backingInt(self);
 86     }
 87 
 88     pub fn elapsedSince(self: AwakeInstant, earlier: AwakeInstant) ClockError!Duration {
 89         const current = self.asNanoseconds();
 90         const start = earlier.asNanoseconds();
 91         if (current < start) return error.ClockRegressed;
 92         return Duration.fromNanoseconds(current - start);
 93     }
 94 
 95     pub fn deadlineAfter(self: AwakeInstant, duration: Duration) AwakeInstant {
 96         return @fromBackingInt(@intCast(self.asNanoseconds() +| duration.asNanoseconds()));
 97     }
 98 
 99     pub fn reached(self: AwakeInstant, deadline: AwakeInstant) bool {
100         return self.asNanoseconds() >= deadline.asNanoseconds();
101     }
102 
103     pub fn isBefore(self: AwakeInstant, other: AwakeInstant) bool {
104         return self.asNanoseconds() < other.asNanoseconds();
105     }
106 
107     pub fn remainingUntil(self: AwakeInstant, deadline: AwakeInstant) Duration {
108         if (self.reached(deadline)) return .zero;
109         return .fromNanoseconds(deadline.asNanoseconds() - self.asNanoseconds());
110     }
111 };
112 
113 pub const BootInstant = enum(u64) {
114     _,
115 
116     pub const zero: BootInstant = @fromBackingInt(@intCast(0));
117 
118     pub fn fromNanoseconds(value: u64) BootInstant {
119         return @fromBackingInt(@intCast(value));
120     }
121 
122     pub fn asNanoseconds(self: BootInstant) u64 {
123         return @backingInt(self);
124     }
125 
126     pub fn elapsedSince(self: BootInstant, earlier: BootInstant) ClockError!Duration {
127         const current = self.asNanoseconds();
128         const start = earlier.asNanoseconds();
129         if (current < start) return error.ClockRegressed;
130         return Duration.fromNanoseconds(current - start);
131     }
132 
133     pub fn deadlineAfter(self: BootInstant, duration: Duration) BootInstant {
134         return @fromBackingInt(@intCast(self.asNanoseconds() +| duration.asNanoseconds()));
135     }
136 
137     pub fn reached(self: BootInstant, deadline: BootInstant) bool {
138         return self.asNanoseconds() >= deadline.asNanoseconds();
139     }
140 
141     pub fn isBefore(self: BootInstant, other: BootInstant) bool {
142         return self.asNanoseconds() < other.asNanoseconds();
143     }
144 
145     pub fn remainingUntil(self: BootInstant, deadline: BootInstant) Duration {
146         if (self.reached(deadline)) return .zero;
147         return .fromNanoseconds(deadline.asNanoseconds() - self.asNanoseconds());
148     }
149 };
150 
151 pub const WallTimestamp = enum(u64) {
152     _,
153 
154     pub const epoch: WallTimestamp = @fromBackingInt(@intCast(0));
155 
156     pub fn fromNanoseconds(value: u64) WallTimestamp {
157         return @fromBackingInt(@intCast(value));
158     }
159 
160     pub fn asNanoseconds(self: WallTimestamp) u64 {
161         return @backingInt(self);
162     }
163 
164     pub fn asMilliseconds(self: WallTimestamp) u64 {
165         return self.asNanoseconds() / std.time.ns_per_ms;
166     }
167 };
168 
169 pub const AwakeClock = struct {
170     context: ?*anyopaque = null,
171     read_fn: *const fn (?*anyopaque) ClockError!AwakeInstant = readSystemAwake,
172 
173     pub fn system() AwakeClock {
174         return .{};
175     }
176 
177     pub fn now(self: AwakeClock) ClockError!AwakeInstant {
178         return self.read_fn(self.context);
179     }
180 };
181 
182 pub const BootClock = struct {
183     context: ?*anyopaque = null,
184     read_fn: *const fn (?*anyopaque) ClockError!BootInstant = readSystemBoot,
185 
186     pub fn system() BootClock {
187         return .{};
188     }
189 
190     pub fn now(self: BootClock) ClockError!BootInstant {
191         return self.read_fn(self.context);
192     }
193 };
194 
195 pub const FakeClock = struct {
196     awake: AwakeInstant,
197     boot: BootInstant,
198     wall: WallTimestamp,
199     reads_unavailable: bool = false,
200 
201     pub fn init(
202         awake: AwakeInstant,
203         boot: BootInstant,
204         wall: WallTimestamp,
205     ) FakeClock {
206         return .{ .awake = awake, .boot = boot, .wall = wall };
207     }
208 
209     pub fn zero() FakeClock {
210         return init(.zero, .zero, .epoch);
211     }
212 
213     pub fn awakeClock(self: *FakeClock) AwakeClock {
214         return .{ .context = self, .read_fn = readFakeAwake };
215     }
216 
217     pub fn bootClock(self: *FakeClock) BootClock {
218         return .{ .context = self, .read_fn = readFakeBoot };
219     }
220 
221     pub fn advance(self: *FakeClock, duration: Duration) void {
222         const delta = duration.asNanoseconds();
223         self.awake = .fromNanoseconds(self.awake.asNanoseconds() +| delta);
224         self.boot = .fromNanoseconds(self.boot.asNanoseconds() +| delta);
225         self.wall = .fromNanoseconds(self.wall.asNanoseconds() +| delta);
226     }
227 
228     pub fn regress(self: *FakeClock, duration: Duration) void {
229         const delta = duration.asNanoseconds();
230         self.awake = .fromNanoseconds(self.awake.asNanoseconds() -| delta);
231         self.boot = .fromNanoseconds(self.boot.asNanoseconds() -| delta);
232         self.wall = .fromNanoseconds(self.wall.asNanoseconds() -| delta);
233     }
234 
235     pub fn suspendGap(self: *FakeClock, duration: Duration) void {
236         const delta = duration.asNanoseconds();
237         self.boot = .fromNanoseconds(self.boot.asNanoseconds() +| delta);
238         self.wall = .fromNanoseconds(self.wall.asNanoseconds() +| delta);
239     }
240 
241     pub fn overflow(self: *FakeClock) void {
242         self.awake = .fromNanoseconds(std.math.maxInt(u64));
243         self.boot = .fromNanoseconds(std.math.maxInt(u64));
244         self.wall = .fromNanoseconds(std.math.maxInt(u64));
245     }
246 
247     pub fn setWall(self: *FakeClock, wall: WallTimestamp) void {
248         self.wall = wall;
249     }
250 
251     pub fn setReadsUnavailable(self: *FakeClock, unavailable: bool) void {
252         self.reads_unavailable = unavailable;
253     }
254 };
255 
256 pub fn supportsAwakeClock() bool {
257     return switch (builtin.os.tag) {
258         .freestanding, .wasi => false,
259         else => true,
260     };
261 }
262 
263 pub fn supportsBootClock() bool {
264     return switch (builtin.os.tag) {
265         .freestanding, .wasi => false,
266         else => true,
267     };
268 }
269 
270 pub fn supportsRealClock() bool {
271     return switch (builtin.os.tag) {
272         .freestanding, .wasi => false,
273         else => true,
274     };
275 }
276 
277 pub fn awakeNow() ClockError!AwakeInstant {
278     const value = awakeNanoseconds() orelse return error.ClockUnavailable;
279     return .fromNanoseconds(try nonnegativeNanoseconds(value));
280 }
281 
282 pub fn bootNow() ClockError!BootInstant {
283     const value = bootNanoseconds() orelse return error.ClockUnavailable;
284     return .fromNanoseconds(try nonnegativeNanoseconds(value));
285 }
286 
287 pub fn wallNow() ClockError!WallTimestamp {
288     const value = realNanoseconds() orelse return error.ClockUnavailable;
289     return .fromNanoseconds(try nonnegativeNanoseconds(value));
290 }
291 
292 pub fn awakeNanoseconds() ?i128 {
293     if (comptime !supportsAwakeClock()) return null;
294     return switch (builtin.os.tag) {
295         .linux => linuxClockNanoseconds(.MONOTONIC),
296         else => std.Io.Timestamp.now(std.Options.debug_io, .awake).toNanoseconds(),
297     };
298 }
299 
300 /// CPU time spent by every thread of the process, or null where the host keeps no such clock.
301 pub fn processCpuNanoseconds() ?i128 {
302     return switch (builtin.os.tag) {
303         .linux => linuxClockNanoseconds(.PROCESS_CPUTIME_ID),
304         else => null,
305     };
306 }
307 
308 pub fn bootNanoseconds() ?i128 {
309     if (comptime !supportsBootClock()) return null;
310     return switch (builtin.os.tag) {
311         .linux => linuxClockNanoseconds(.BOOTTIME),
312         else => std.Io.Timestamp.now(std.Options.debug_io, .boot).toNanoseconds(),
313     };
314 }
315 
316 pub fn realNanoseconds() ?i128 {
317     if (comptime !supportsRealClock()) return null;
318     return switch (builtin.os.tag) {
319         .linux => linuxClockNanoseconds(.REALTIME),
320         else => std.Io.Timestamp.now(std.Options.debug_io, .real).toNanoseconds(),
321     };
322 }
323 
324 pub fn externalClockNanoseconds(clock_id: ExternalClockId) ClockError!i128 {
325     if (comptime builtin.os.tag != .linux) return error.UnsupportedPlatform;
326     const clock: std.os.linux.clockid_t = @fromBackingInt(@intCast(clock_id.toNative()));
327     return linuxClockNanoseconds(clock) orelse error.ClockUnavailable;
328 }
329 
330 pub fn nanoTimestamp() i128 {
331     return @intCast((awakeNow() catch @panic("awake clock unavailable")).asNanoseconds());
332 }
333 
334 pub fn realNanoTimestamp() i128 {
335     return @intCast((wallNow() catch @panic("wall clock unavailable")).asNanoseconds());
336 }
337 
338 pub fn realMilliTimestamp() i64 {
339     return @intCast(@divTrunc(realNanoTimestamp(), std.time.ns_per_ms));
340 }
341 
342 pub fn ioTimestampNanoseconds(timestamp_value: std.Io.Timestamp) u64 {
343     return @intCast(timestamp_value.nanoseconds);
344 }
345 
346 pub fn seconds() f64 {
347     return @as(f64, @floatFromInt(nanoTimestamp())) / @as(f64, @floatFromInt(std.time.ns_per_s));
348 }
349 
350 pub fn realSeconds() f64 {
351     return @as(f64, @floatFromInt(realNanoTimestamp())) /
352         @as(f64, @floatFromInt(std.time.ns_per_s));
353 }
354 
355 pub fn microTimestamp() i64 {
356     return @intCast(@divTrunc(nanoTimestamp(), std.time.ns_per_us));
357 }
358 
359 pub fn milliTimestamp() i64 {
360     return @intCast(@divTrunc(nanoTimestamp(), std.time.ns_per_ms));
361 }
362 
363 pub fn timestamp() i64 {
364     return @intCast(@divTrunc(nanoTimestamp(), std.time.ns_per_s));
365 }
366 
367 pub fn sleepNanoseconds(duration_ns: u64) void {
368     if (duration_ns == 0) return;
369     if (comptime builtin.os.tag == .freestanding or builtin.os.tag == .wasi) return;
370     switch (builtin.os.tag) {
371         .linux => linuxSleepNanoseconds(duration_ns),
372         else => std.Io.sleep(std.Options.debug_io, .fromNanoseconds(duration_ns), .awake) catch {},
373     }
374 }
375 
376 pub fn sleepMilliseconds(duration_ms: u64) void {
377     sleepNanoseconds(sleepNanosecondsForMilliseconds(duration_ms));
378 }
379 
380 pub fn sleepSeconds(duration_seconds: f64) void {
381     sleepNanoseconds(sleepNanosecondsForSeconds(duration_seconds));
382 }
383 
384 fn sleepNanosecondsForMilliseconds(duration_ms: u64) u64 {
385     if (duration_ms > std.math.maxInt(u64) / std.time.ns_per_ms) return std.math.maxInt(u64);
386     return duration_ms * std.time.ns_per_ms;
387 }
388 
389 fn sleepNanosecondsForSeconds(duration_seconds: f64) u64 {
390     if (!std.math.isFinite(duration_seconds) or duration_seconds <= 0) return 0;
391     const ns_per_s: f64 = @floatFromInt(std.time.ns_per_s);
392     const max_seconds = @as(f64, @floatFromInt(std.math.maxInt(u64))) / ns_per_s;
393     if (duration_seconds >= max_seconds) return std.math.maxInt(u64);
394     return @intFromFloat(@round(duration_seconds * ns_per_s));
395 }
396 
397 fn readSystemAwake(_: ?*anyopaque) ClockError!AwakeInstant {
398     return awakeNow();
399 }
400 
401 fn readSystemBoot(_: ?*anyopaque) ClockError!BootInstant {
402     return bootNow();
403 }
404 
405 fn readFakeAwake(context: ?*anyopaque) ClockError!AwakeInstant {
406     const clock: *FakeClock = @ptrCast(@alignCast(context orelse unreachable));
407     if (clock.reads_unavailable) return error.ClockUnavailable;
408     return clock.awake;
409 }
410 
411 fn readFakeBoot(context: ?*anyopaque) ClockError!BootInstant {
412     const clock: *FakeClock = @ptrCast(@alignCast(context orelse unreachable));
413     if (clock.reads_unavailable) return error.ClockUnavailable;
414     return clock.boot;
415 }
416 
417 fn nonnegativeNanoseconds(value: i128) ClockError!u64 {
418     if (value < 0 or value > std.math.maxInt(u64)) return error.ClockUnavailable;
419     return @intCast(value);
420 }
421 
422 fn linuxClockNanoseconds(clock: std.os.linux.clockid_t) ?i128 {
423     const linux = std.os.linux;
424     var ts: linux.timespec = undefined;
425     const rc = linux.clock_gettime(clock, &ts);
426     if (linux.errno(rc) != .SUCCESS) return null;
427     if (ts.sec < 0 or ts.nsec < 0) return null;
428     return @as(i128, @intCast(ts.sec)) * std.time.ns_per_s + @as(i128, @intCast(ts.nsec));
429 }
430 
431 fn linuxSleepNanoseconds(duration_ns: u64) void {
432     const linux = std.os.linux;
433     var remaining = linux.timespec{
434         .sec = @intCast(duration_ns / std.time.ns_per_s),
435         .nsec = @intCast(duration_ns % std.time.ns_per_s),
436     };
437     while (true) {
438         var next: linux.timespec = undefined;
439         const rc = linux.syscall2(.nanosleep, @intFromPtr(&remaining), @intFromPtr(&next));
440         switch (linux.errno(rc)) {
441             .SUCCESS => return,
442             .INTR => remaining = next,
443             else => return,
444         }
445     }
446 }
447 
448 test "timestamp helpers are callable on every target" {
449     if (supportsAwakeClock()) {
450         try std.testing.expect((try awakeNow()).asNanoseconds() > 0);
451         try std.testing.expect(nanoTimestamp() >= 0);
452         try std.testing.expect(seconds() >= 0);
453         try std.testing.expect(microTimestamp() >= 0);
454         try std.testing.expect(milliTimestamp() >= 0);
455         try std.testing.expect(timestamp() >= 0);
456     } else {
457         try std.testing.expectError(error.ClockUnavailable, awakeNow());
458     }
459     if (supportsBootClock()) {
460         try std.testing.expect((try bootNow()).asNanoseconds() > 0);
461     } else {
462         try std.testing.expectError(error.ClockUnavailable, bootNow());
463     }
464     if (supportsRealClock()) {
465         try std.testing.expect((try wallNow()).asNanoseconds() > 0);
466         try std.testing.expect(realNanoTimestamp() >= 0);
467         try std.testing.expect(realSeconds() >= 0);
468         try std.testing.expect(realMilliTimestamp() >= 0);
469     } else {
470         try std.testing.expectError(error.ClockUnavailable, wallNow());
471     }
472 }
473 
474 test "Linux awake clock read is monotone within the vDSO budget" {
475     if (comptime builtin.os.tag != .linux) return error.SkipZigTest;
476     const sample_count: usize = 64;
477     const median_budget_ns: u64 = 250;
478     var samples: [sample_count]u64 = undefined;
479     for (&samples) |*sample| {
480         const before = try awakeNow();
481         const after = try awakeNow();
482         try std.testing.expect(after.asNanoseconds() >= before.asNanoseconds());
483         sample.* = (try after.elapsedSince(before)).asNanoseconds();
484     }
485     std.mem.sort(u64, &samples, {}, std.sort.asc(u64));
486     try std.testing.expect(samples[samples.len / 2] <= median_budget_ns);
487 }
488 
489 test "instant subtraction accepts one clock type" {
490     comptime {
491         const awake_subtract: *const fn (
492             AwakeInstant,
493             AwakeInstant,
494         ) ClockError!Duration = AwakeInstant.elapsedSince;
495         const boot_subtract: *const fn (
496             BootInstant,
497             BootInstant,
498         ) ClockError!Duration = BootInstant.elapsedSince;
499         _ = awake_subtract;
500         _ = boot_subtract;
501         std.debug.assert(AwakeInstant != BootInstant);
502         std.debug.assert(AwakeInstant != WallTimestamp);
503     }
504 }
505 
506 test "instant subtraction reports regression" {
507     const earlier = AwakeInstant.fromNanoseconds(8);
508     const current = AwakeInstant.fromNanoseconds(5);
509     try std.testing.expectError(error.ClockRegressed, current.elapsedSince(earlier));
510 }
511 
512 test "deadline and duration arithmetic saturate" {
513     const duration = Duration.fromMilliseconds(std.math.maxInt(u64));
514     try std.testing.expectEqual(std.math.maxInt(u64), duration.asNanoseconds());
515     const start = AwakeInstant.fromNanoseconds(std.math.maxInt(u64) - 1);
516     const deadline = start.deadlineAfter(.fromNanoseconds(2));
517     try std.testing.expectEqual(std.math.maxInt(u64), deadline.asNanoseconds());
518     try std.testing.expectEqual(
519         @as(u64, 2),
520         Duration.fromNanoseconds(1_001_000).asMillisecondsCeil(),
521     );
522     try std.testing.expectEqual(
523         Duration.fromNanoseconds(5),
524         Duration.fromNanoseconds(8).min(.fromNanoseconds(5)),
525     );
526     try std.testing.expectEqual(
527         std.math.maxInt(u64),
528         Duration.fromNanoseconds(std.math.maxInt(u64)).saturatingMultiply(2).asNanoseconds(),
529     );
530 }
531 
532 test "fake clock separates awake suspension and wall jumps" {
533     var clock = FakeClock.zero();
534     clock.advance(.fromMilliseconds(2));
535     try std.testing.expectEqual(@as(u64, 2_000_000), clock.awake.asNanoseconds());
536     clock.suspendGap(.fromMilliseconds(3));
537     try std.testing.expectEqual(@as(u64, 2_000_000), clock.awake.asNanoseconds());
538     try std.testing.expectEqual(@as(u64, 5_000_000), clock.boot.asNanoseconds());
539     clock.setWall(.fromNanoseconds(9));
540     try std.testing.expectEqual(@as(u64, 9), clock.wall.asNanoseconds());
541     clock.regress(.fromNanoseconds(1));
542     try std.testing.expectEqual(@as(u64, 1_999_999), clock.awake.asNanoseconds());
543     clock.overflow();
544     try std.testing.expectEqual(std.math.maxInt(u64), clock.awake.asNanoseconds());
545 }
546 
547 test "fake typed clocks report unavailability" {
548     var fake = FakeClock.zero();
549     const awake_clock = fake.awakeClock();
550     const boot_clock = fake.bootClock();
551     try std.testing.expectEqual(AwakeInstant.zero, try awake_clock.now());
552     try std.testing.expectEqual(BootInstant.zero, try boot_clock.now());
553     fake.setReadsUnavailable(true);
554     try std.testing.expectError(error.ClockUnavailable, awake_clock.now());
555     try std.testing.expectError(error.ClockUnavailable, boot_clock.now());
556 }
557 
558 test "external clock ids route through the opaque sampled clock boundary" {
559     if (builtin.os.tag != .linux) {
560         try std.testing.expectError(
561             error.UnsupportedPlatform,
562             externalClockNanoseconds(awake_clock_id),
563         );
564         return;
565     }
566     try std.testing.expectEqual(
567         @as(u32, @intCast(@backingInt(std.os.linux.clockid_t.REALTIME))),
568         real_clock_id.toNative(),
569     );
570     try std.testing.expect(try externalClockNanoseconds(real_clock_id) > 0);
571     try std.testing.expect(try externalClockNanoseconds(awake_clock_id) > 0);
572     try std.testing.expect(try externalClockNanoseconds(boot_clock_id) > 0);
573     try std.testing.expectError(
574         error.ClockUnavailable,
575         externalClockNanoseconds(.fromNative(std.math.maxInt(u32))),
576     );
577 }
578 
579 test "io timestamp conversion preserves nanoseconds" {
580     try std.testing.expectEqual(@as(u64, 1234), ioTimestampNanoseconds(.{ .nanoseconds = 1234 }));
581 }
582 
583 test "sleep second conversion rejects invalid durations" {
584     try std.testing.expectEqual(@as(u64, 0), sleepNanosecondsForSeconds(-1));
585     try std.testing.expectEqual(@as(u64, 0), sleepNanosecondsForSeconds(0));
586     try std.testing.expectEqual(@as(u64, 0), sleepNanosecondsForSeconds(std.math.inf(f64)));
587 }
588 
589 test "sleep millisecond conversion saturates" {
590     try std.testing.expectEqual(@as(u64, 0), sleepNanosecondsForMilliseconds(0));
591     try std.testing.expectEqual(@as(u64, std.time.ns_per_ms), sleepNanosecondsForMilliseconds(1));
592     try std.testing.expectEqual(
593         std.math.maxInt(u64),
594         sleepNanosecondsForMilliseconds(std.math.maxInt(u64)),
595     );
596 }
597 
598 test "sleep second conversion rounds and saturates" {
599     try std.testing.expectEqual(@as(u64, 1), sleepNanosecondsForSeconds(0.000000001));
600     try std.testing.expectEqual(@as(u64, 1_500_000_000), sleepNanosecondsForSeconds(1.5));
601     try std.testing.expectEqual(
602         std.math.maxInt(u64),
603         sleepNanosecondsForSeconds(@as(f64, @floatFromInt(std.math.maxInt(u64)))),
604     );
605 }