Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 34 additions & 7 deletions src/backend/epoll.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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;

Expand Down
35 changes: 28 additions & 7 deletions src/backend/iocp.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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;

Expand Down
45 changes: 33 additions & 12 deletions src/backend/kqueue.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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;

Expand Down