Ability to run tasks even in the "distant" future.

We previously ignored tasks scheduled more than 5 seconds away. These tasks are
now scheduled on the low priority queue. This means that they won't stop a
page.wait for returning, but they'll still [eventually] be run if page.wait is
called multiple times.

Practically, this means that they'll never be run in `fetch` mode, but they
might be run from CDP if the driver waits.

Make queue names consistent, primary => high_priority, secondary => low_priority
(the same names used by the page)
This commit is contained in:
Karl Seguin
2025-09-08 18:52:11 +08:00
parent bc82023878
commit 21e354d252
2 changed files with 19 additions and 26 deletions

View File

@@ -22,24 +22,24 @@ const Allocator = std.mem.Allocator;
const Scheduler = @This(); const Scheduler = @This();
primary: Queue, high_priority: Queue,
// For repeating tasks. We only want to run these if there are other things to // For repeating tasks. We only want to run these if there are other things to
// do. We don't, for example, want a window.setInterval or the page.runMicrotasks // do. We don't, for example, want a window.setInterval or the page.runMicrotasks
// to block the page.wait. // to block the page.wait.
secondary: Queue, low_priority: Queue,
// we expect allocator to be the page arena, hence we never call primary.deinit // we expect allocator to be the page arena, hence we never call high_priority.deinit
pub fn init(allocator: Allocator) Scheduler { pub fn init(allocator: Allocator) Scheduler {
return .{ return .{
.primary = Queue.init(allocator, {}), .high_priority = Queue.init(allocator, {}),
.secondary = Queue.init(allocator, {}), .low_priority = Queue.init(allocator, {}),
}; };
} }
pub fn reset(self: *Scheduler) void { pub fn reset(self: *Scheduler) void {
self.primary.clearRetainingCapacity(); self.high_priority.clearRetainingCapacity();
self.secondary.clearRetainingCapacity(); self.low_priority.clearRetainingCapacity();
} }
const AddOpts = struct { const AddOpts = struct {
@@ -47,13 +47,16 @@ const AddOpts = struct {
low_priority: bool = false, low_priority: bool = false,
}; };
pub fn add(self: *Scheduler, ctx: *anyopaque, func: Task.Func, ms: u32, opts: AddOpts) !void { pub fn add(self: *Scheduler, ctx: *anyopaque, func: Task.Func, ms: u32, opts: AddOpts) !void {
var low_priority = opts.low_priority;
if (ms > 5_000) { if (ms > 5_000) {
log.warn(.user_script, "long timeout ignored", .{ .delay = ms }); // we don't want tasks in the far future to block page.wait from
// ignore any task that we're almost certainly never going to run // completing. However, if page.wait is called multiple times (maybe
return; // a CDP driver is wait for something to happen), then we do want
// to [eventually] run these when their time is up.
low_priority = true;
} }
var q = if (opts.low_priority) &self.secondary else &self.primary; var q = if (low_priority) &self.low_priority else &self.high_priority;
return q.add(.{ return q.add(.{
.ms = std.time.milliTimestamp() + ms, .ms = std.time.milliTimestamp() + ms,
.ctx = ctx, .ctx = ctx,
@@ -63,11 +66,11 @@ pub fn add(self: *Scheduler, ctx: *anyopaque, func: Task.Func, ms: u32, opts: Ad
} }
pub fn runHighPriority(self: *Scheduler) !?i32 { pub fn runHighPriority(self: *Scheduler) !?i32 {
return self.runQueue(&self.primary); return self.runQueue(&self.high_priority);
} }
pub fn runLowPriority(self: *Scheduler) !?i32 { pub fn runLowPriority(self: *Scheduler) !?i32 {
return self.runQueue(&self.secondary); return self.runQueue(&self.low_priority);
} }
fn runQueue(self: *Scheduler, queue: *Queue) !?i32 { fn runQueue(self: *Scheduler, queue: *Queue) !?i32 {
@@ -94,7 +97,7 @@ fn runQueue(self: *Scheduler, queue: *Queue) !?i32 {
var copy = task; var copy = task;
copy.ms = now + repeat_delay; copy.ms = now + repeat_delay;
try self.secondary.add(copy); try self.low_priority.add(copy);
} }
_ = queue.remove(); _ = queue.remove();
next = queue.peek(); next = queue.peek();
@@ -144,11 +147,11 @@ test "Scheduler" {
try testing.expectEqualSlices(u32, &.{ 1, 1, 2 }, task.calls.items); try testing.expectEqualSlices(u32, &.{ 1, 1, 2 }, task.calls.items);
std.Thread.sleep(std.time.ns_per_ms * 5); std.Thread.sleep(std.time.ns_per_ms * 5);
// wont' run secondary // won't run low_priority
try testing.expectEqual(null, try s.runHighPriority()); try testing.expectEqual(null, try s.runHighPriority());
try testing.expectEqualSlices(u32, &.{ 1, 1, 2 }, task.calls.items); try testing.expectEqualSlices(u32, &.{ 1, 1, 2 }, task.calls.items);
//runs secondary //runs low_priority
try testing.expectDelta(2, try s.runLowPriority(), 1); try testing.expectDelta(2, try s.runLowPriority(), 1);
try testing.expectEqualSlices(u32, &.{ 1, 1, 2, 2 }, task.calls.items); try testing.expectEqualSlices(u32, &.{ 1, 1, 2, 2 }, task.calls.items);
} }

View File

@@ -277,16 +277,6 @@ pub const Window = struct {
}; };
fn createTimeout(self: *Window, cbk: Function, delay_: ?u32, page: *Page, opts: CreateTimeoutOpts) !u32 { fn createTimeout(self: *Window, cbk: Function, delay_: ?u32, page: *Page, opts: CreateTimeoutOpts) !u32 {
const delay = delay_ orelse 0; const delay = delay_ orelse 0;
if (delay > 5000) {
if (!@import("builtin").is_test) {
log.warn(.user_script, "long timeout ignored", .{ .delay = delay, .interval = opts.repeat });
}
// self.timer_id is u30, so the largest value we can generate is
// 1_073_741_824. Returning 2_000_000_000 makes sure that clients
// can call cancelTimer/cancelInterval without breaking anything.
return 2_000_000_000;
}
if (self.timers.count() > 512) { if (self.timers.count() > 512) {
return error.TooManyTimeout; return error.TooManyTimeout;
} }