diff --git a/src/backend/epoll.zig b/src/backend/epoll.zig index 25c0a93..d7a8831 100644 --- a/src/backend/epoll.zig +++ b/src/backend/epoll.zig @@ -462,13 +462,7 @@ pub const Loop = struct { // If we have a timer, we want to set the timeout to our next // timer value. If we have no timer, we wait forever. const t = self.timers.peek() orelse break :timeout -1; - - // Determine the time in milliseconds. - const ms_now = @as(u64, @intCast(self.cached_now.sec)) * std.time.ms_per_s + - @as(u64, @intCast(self.cached_now.nsec)) / std.time.ns_per_ms; - const ms_next = @as(u64, @intCast(t.next.sec)) * std.time.ms_per_s + - @as(u64, @intCast(t.next.nsec)) / std.time.ns_per_ms; - break :timeout @as(i32, @intCast(ms_next -| ms_now)); + break :timeout timeoutMs(self.cached_now, t.next); }; const n = epoll_helper.epoll_wait(self.fd, &events, timeout); @@ -924,6 +918,17 @@ pub const Loop = struct { } }; +/// Milliseconds for epoll_wait to wait until `next`, rounded up. Rounded down, +/// a timer due later in the current millisecond would get a timeout of 0, and +/// the loop would call epoll_wait without waiting, over and over, until it was +/// due. +fn timeoutMs(now: posix.timespec, next: posix.timespec) i32 { + const ns = (@as(i128, next.sec) - now.sec) * std.time.ns_per_s + (next.nsec - now.nsec); + if (ns <= 0) return 0; + const ms = @divFloor(ns - 1, std.time.ns_per_ms) + 1; + return @intCast(@min(ms, std.math.maxInt(i32))); +} + pub const Completion = struct { /// Operation to execute. This is only safe to read BEFORE the completion /// is queued. After being queued (with "add"), the operation may change. @@ -1517,6 +1522,28 @@ test "epoll: stop" { try testing.expect(!called); } +test "epoll: timeout to a timer rounds up to whole milliseconds" { + const testing = std.testing; + const us = std.time.ns_per_us; + const now: posix.timespec = .{ .sec = 10, .nsec = 200 * us }; + + // Later in the same millisecond: a 0 timeout polls until it is due. + try testing.expectEqual(1, timeoutMs(now, .{ .sec = 10, .nsec = 900 * us })); + try testing.expectEqual(1, timeoutMs(now, .{ .sec = 10, .nsec = 1200 * us })); + try testing.expectEqual(2, timeoutMs(now, .{ .sec = 10, .nsec = 1201 * us })); + try testing.expectEqual(1000, timeoutMs(now, .{ .sec = 11, .nsec = 200 * us })); + + // Due or overdue: no waiting. + try testing.expectEqual(0, timeoutMs(now, now)); + try testing.expectEqual(0, timeoutMs(now, .{ .sec = 9, .nsec = 0 })); + + // Far off, as the maximum timer is: capped, not overflowed. + try testing.expectEqual(std.math.maxInt(i32), timeoutMs(now, .{ + .sec = std.math.maxInt(isize), + .nsec = std.math.maxInt(isize), + })); +} + test "epoll: timer" { const testing = std.testing; diff --git a/src/backend/iocp.zig b/src/backend/iocp.zig index cc8506e..8df2892 100644 --- a/src/backend/iocp.zig +++ b/src/backend/iocp.zig @@ -320,13 +320,7 @@ pub const Loop = struct { // If we have a timer, we want to set the timeout to our next timer value. If we // have no timer, we wait forever. const t = self.timers.peek() orelse break :timeout null; - - // Determin the time in milliseconds. If the cast fails, we fallback to the maximum - // acceptable value. - const ms_now = self.cached_now / std.time.ns_per_ms; - const ms_next = t.next / std.time.ns_per_ms; - const ms = ms_next -| ms_now; - break :timeout std.math.cast(windows.DWORD, ms) orelse windows.INFINITE - 1; + break :timeout timeoutMs(self.cached_now, t.next); }; // Wait for changes IO completions. @@ -875,6 +869,16 @@ pub const Loop = struct { } }; +/// Milliseconds to wait for completions until `next`, rounded up. Rounded +/// down, a timer due later in the current millisecond would get a timeout of +/// 0, and the loop would poll without waiting, over and over, until it was +/// due. A wait too long for a DWORD falls back to the longest one short of +/// INFINITE. +fn timeoutMs(now: u64, next: u64) windows.DWORD { + const ms = std.math.divCeil(u64, next -| now, std.time.ns_per_ms) catch unreachable; + return std.math.cast(windows.DWORD, ms) orelse windows.INFINITE - 1; +} + /// Convenience to convert from windows.HANDLE to windows.ws2_32.SOCKET (which are the same thing). inline fn asSocket(h: windows.HANDLE) windows.ws2_32.SOCKET { return @as(windows.ws2_32.SOCKET, @ptrCast(h)); @@ -1550,6 +1554,23 @@ test "iocp: stop" { try testing.expect(!called); } +test "iocp: timeout to a timer rounds up to whole milliseconds" { + const testing = std.testing; + const us = std.time.ns_per_us; + const now: u64 = 10 * std.time.ns_per_s + 200 * us; + + // Later in the same millisecond: a 0 timeout polls until it is due. + try testing.expectEqual(1, timeoutMs(now, now + 700 * us)); + try testing.expectEqual(1, timeoutMs(now, now + 1000 * us)); + try testing.expectEqual(2, timeoutMs(now, now + 1001 * us)); + + // Due or overdue: no waiting. + try testing.expectEqual(0, timeoutMs(now, now)); + try testing.expectEqual(0, timeoutMs(now, now - 1)); + + try testing.expectEqual(windows.INFINITE - 1, timeoutMs(now, std.math.maxInt(u64))); +} + test "iocp: timer" { const testing = std.testing; diff --git a/src/backend/kqueue.zig b/src/backend/kqueue.zig index a78249d..b500187 100644 --- a/src/backend/kqueue.zig +++ b/src/backend/kqueue.zig @@ -471,18 +471,7 @@ pub const Loop = struct { // If we have a timer, we want to set the timeout to our next // timer value. If we have no timer, we wait forever. const t = self.timers.peek() orelse break :timeout null; - - // Determine the time in milliseconds. - const ms_now = @as(u64, @intCast(self.cached_now.sec)) * std.time.ms_per_s + - @as(u64, @intCast(self.cached_now.nsec)) / std.time.ns_per_ms; - const ms_next = @as(u64, @intCast(t.next.sec)) * std.time.ms_per_s + - @as(u64, @intCast(t.next.nsec)) / std.time.ns_per_ms; - const ms = ms_next -| ms_now; - - // Convert to s/ns for the timespec - const sec = ms / std.time.ms_per_s; - const nsec = (ms % std.time.ms_per_s) * std.time.ns_per_ms; - break :timeout .{ .sec = @intCast(sec), .nsec = @intCast(nsec) }; + break :timeout timeoutUntil(self.cached_now, t.next); }; // Wait for changes. Note that we ALWAYS attempt to get completions @@ -975,6 +964,20 @@ pub const Loop = struct { } }; +/// How long kevent waits for a timer due at `next`, to the nanosecond. In +/// whole milliseconds rounded down, a timer due later in the current +/// millisecond would get a zero timeout, and the loop would call kevent +/// without waiting, over and over, until it was due. +fn timeoutUntil(now: posix.timespec, next: posix.timespec) posix.timespec { + const ns = (@as(i128, next.sec) - now.sec) * std.time.ns_per_s + (next.nsec - now.nsec); + if (ns <= 0) return std.mem.zeroes(posix.timespec); + return .{ + // About 68 years: small enough for a kernel's 64-bit time types. + .sec = @intCast(@min(@divFloor(ns, std.time.ns_per_s), std.math.maxInt(i32))), + .nsec = @intCast(@mod(ns, std.time.ns_per_s)), + }; +} + /// A completion is a request to perform some work with the loop. pub const Completion = struct { /// Operation to execute. @@ -1995,6 +1998,24 @@ test "kqueue: stop" { try testing.expect(!called); } +test "kqueue: timeout to a timer is exact" { + const testing = std.testing; + const us = std.time.ns_per_us; + const now: posix.timespec = .{ .sec = 10, .nsec = 200 * us }; + + // Later in the same millisecond: a zero timeout polls until it is due. + try testing.expectEqual(posix.timespec{ .sec = 0, .nsec = 700 * us }, timeoutUntil(now, .{ .sec = 10, .nsec = 900 * us })); + try testing.expectEqual(posix.timespec{ .sec = 1, .nsec = 100 * us }, timeoutUntil(now, .{ .sec = 11, .nsec = 300 * us })); + + // Due or overdue: no waiting. + try testing.expectEqual(std.mem.zeroes(posix.timespec), timeoutUntil(now, now)); + try testing.expectEqual(std.mem.zeroes(posix.timespec), timeoutUntil(now, .{ .sec = 9, .nsec = 0 })); + + // Far off, as the maximum timer is: capped, not overflowed. + const far = timeoutUntil(now, .{ .sec = std.math.maxInt(isize), .nsec = std.math.maxInt(isize) }); + try testing.expectEqual(std.math.maxInt(i32), far.sec); +} + test "kqueue: timer" { const testing = std.testing;