Allow [schedule] tasks to have finalizers

There's no guarantee that a task will ever be run. A page can be shutdown by
the user or timeout or an error. Scheduler cleanup relies on the underlying
page.arena. This forces all tasks to rely on the page.arena as they have no way
to clean themselves.

This commit allows tasks to register a finalizer which is guaranteed to be
called when the scheduler is shutdown.

The window ScheduleCallback, PostMessageCallback now use an arena from the
ArenaPool rather than the page.arena and use the task finalizer to ensure the
arena is released on shutdown.
This commit is contained in:
Karl Seguin
2026-01-30 17:23:03 +08:00
parent 34dda780d9
commit 5d96304332
3 changed files with 76 additions and 30 deletions

View File

@@ -234,6 +234,10 @@ pub fn deinit(self: *Page) void {
// stats.print(&stream) catch unreachable; // stats.print(&stream) catch unreachable;
} }
// This can release JS objects, so we need to do this while the js.Context
// is still around.
self.scheduler.deinit();
{ {
// some MicroTasks might be referencing the page, we need to drain it while // some MicroTasks might be referencing the page, we need to drain it while
// the page still exists // the page still exists
@@ -266,6 +270,8 @@ fn reset(self: *Page, comptime initializing: bool) !void {
const browser = self._session.browser; const browser = self._session.browser;
if (comptime initializing == false) { if (comptime initializing == false) {
self.scheduler.deinit();
browser.env.destroyContext(self.js); browser.env.destroyContext(self.js);
// removing a context can trigger finalizers, so we can only check for // removing a context can trigger finalizers, so we can only check for
@@ -281,7 +287,6 @@ fn reset(self: *Page, comptime initializing: bool) !void {
// We force a garbage collection between page navigations to keep v8 // We force a garbage collection between page navigations to keep v8
// memory usage as low as possible. // memory usage as low as possible.
browser.env.memoryPressureNotification(.moderate); browser.env.memoryPressureNotification(.moderate);
self._script_manager.shutdown = true; self._script_manager.shutdown = true;
browser.http_client.abort(); browser.http_client.abort();
self._script_manager.deinit(); self._script_manager.deinit();

View File

@@ -47,9 +47,15 @@ pub fn init(allocator: std.mem.Allocator) Scheduler {
}; };
} }
pub fn deinit(self: *Scheduler) void {
finalizeTasks(&self.low_priority);
finalizeTasks(&self.high_priority);
}
const AddOpts = struct { const AddOpts = struct {
name: []const u8 = "", name: []const u8 = "",
low_priority: bool = false, low_priority: bool = false,
finalizer: ?Finalizer = null,
}; };
pub fn add(self: *Scheduler, ctx: *anyopaque, cb: Callback, run_in_ms: u32, opts: AddOpts) !void { pub fn add(self: *Scheduler, ctx: *anyopaque, cb: Callback, run_in_ms: u32, opts: AddOpts) !void {
if (comptime IS_DEBUG) { if (comptime IS_DEBUG) {
@@ -63,6 +69,7 @@ pub fn add(self: *Scheduler, ctx: *anyopaque, cb: Callback, run_in_ms: u32, opts
.callback = cb, .callback = cb,
.sequence = seq, .sequence = seq,
.name = opts.name, .name = opts.name,
.finalizer = opts.finalizer,
.run_at = milliTimestamp(.monotonic) + run_in_ms, .run_at = milliTimestamp(.monotonic) + run_in_ms,
}); });
} }
@@ -105,12 +112,23 @@ fn runQueue(self: *Scheduler, queue: *Queue) !?u64 {
return null; return null;
} }
fn finalizeTasks(queue: *Queue) void {
var it = queue.iterator();
while (it.next()) |t| {
if (t.finalizer) |func| {
func(t.ctx);
}
}
}
const Task = struct { const Task = struct {
run_at: u64, run_at: u64,
sequence: u64, sequence: u64,
ctx: *anyopaque, ctx: *anyopaque,
name: []const u8, name: []const u8,
callback: Callback, callback: Callback,
finalizer: ?Finalizer,
}; };
const Callback = *const fn (ctx: *anyopaque) anyerror!?u32; const Callback = *const fn (ctx: *anyopaque) anyerror!?u32;
const Finalizer = *const fn (ctx: *anyopaque) void;

View File

@@ -44,6 +44,8 @@ const CSSStyleProperties = @import("css/CSSStyleProperties.zig");
const CustomElementRegistry = @import("CustomElementRegistry.zig"); const CustomElementRegistry = @import("CustomElementRegistry.zig");
const Selection = @import("Selection.zig"); const Selection = @import("Selection.zig");
const Allocator = std.mem.Allocator;
const Window = @This(); const Window = @This();
_proto: *EventTarget, _proto: *EventTarget,
@@ -353,18 +355,21 @@ pub fn postMessage(self: *Window, message: js.Value.Global, target_origin: ?[]co
_ = target_origin; _ = target_origin;
// postMessage queues a task (not a microtask), so use the scheduler // postMessage queues a task (not a microtask), so use the scheduler
const origin = try self._location.getOrigin(page); const arena = try page.getArena(.{ .debug = "Window.schedule" });
const callback = try page._factory.create(PostMessageCallback{ errdefer page.releaseArena(arena);
.window = self,
.message = message,
.origin = try page.arena.dupe(u8, origin),
.page = page,
});
errdefer page._factory.destroy(callback);
const origin = try self._location.getOrigin(page);
const callback = try arena.create(PostMessageCallback);
callback.* = .{
.page = page,
.arena = arena,
.message = message,
.origin = try arena.dupe(u8, origin),
};
try page.scheduler.add(callback, PostMessageCallback.run, 0, .{ try page.scheduler.add(callback, PostMessageCallback.run, 0, .{
.name = "postMessage", .name = "postMessage",
.low_priority = false, .low_priority = false,
.finalizer = PostMessageCallback.cancelled,
}); });
} }
@@ -508,13 +513,16 @@ fn scheduleCallback(self: *Window, cb: js.Function.Temp, delay_ms: u32, opts: Sc
return error.TooManyTimeout; return error.TooManyTimeout;
} }
const arena = try page.getArena(.{ .debug = "Window.schedule" });
errdefer page.releaseArena(arena);
const timer_id = self._timer_id +% 1; const timer_id = self._timer_id +% 1;
self._timer_id = timer_id; self._timer_id = timer_id;
const params = opts.params; const params = opts.params;
var persisted_params: []js.Value.Temp = &.{}; var persisted_params: []js.Value.Temp = &.{};
if (params.len > 0) { if (params.len > 0) {
persisted_params = try page.arena.dupe(js.Value.Temp, params); persisted_params = try arena.dupe(js.Value.Temp, params);
} }
const gop = try self._timers.getOrPut(page.arena, timer_id); const gop = try self._timers.getOrPut(page.arena, timer_id);
@@ -524,21 +532,23 @@ fn scheduleCallback(self: *Window, cb: js.Function.Temp, delay_ms: u32, opts: Sc
} }
errdefer _ = self._timers.remove(timer_id); errdefer _ = self._timers.remove(timer_id);
const callback = try page._factory.create(ScheduleCallback{ const callback = try arena.create(ScheduleCallback);
callback.* = .{
.cb = cb, .cb = cb,
.page = page, .page = page,
.arena = arena,
.mode = opts.mode, .mode = opts.mode,
.name = opts.name, .name = opts.name,
.timer_id = timer_id, .timer_id = timer_id,
.params = persisted_params, .params = persisted_params,
.repeat_ms = if (opts.repeat) if (delay_ms == 0) 1 else delay_ms else null, .repeat_ms = if (opts.repeat) if (delay_ms == 0) 1 else delay_ms else null,
}); };
gop.value_ptr.* = callback; gop.value_ptr.* = callback;
errdefer page._factory.destroy(callback);
try page.scheduler.add(callback, ScheduleCallback.run, delay_ms, .{ try page.scheduler.add(callback, ScheduleCallback.run, delay_ms, .{
.name = opts.name, .name = opts.name,
.low_priority = opts.low_priority, .low_priority = opts.low_priority,
.finalizer = ScheduleCallback.cancelled,
}); });
return timer_id; return timer_id;
@@ -556,13 +566,11 @@ const ScheduleCallback = struct {
cb: js.Function.Temp, cb: js.Function.Temp,
page: *Page,
params: []const js.Value.Temp,
removed: bool = false,
mode: Mode, mode: Mode,
page: *Page,
arena: Allocator,
removed: bool = false,
params: []const js.Value.Temp,
const Mode = enum { const Mode = enum {
idle, idle,
@@ -570,19 +578,26 @@ const ScheduleCallback = struct {
animation_frame, animation_frame,
}; };
fn cancelled(ctx: *anyopaque) void {
var self: *ScheduleCallback = @ptrCast(@alignCast(ctx));
self.deinit();
}
fn deinit(self: *ScheduleCallback) void { fn deinit(self: *ScheduleCallback) void {
self.page.js.release(self.cb); self.page.js.release(self.cb);
for (self.params) |param| { for (self.params) |param| {
self.page.js.release(param); self.page.js.release(param);
} }
self.page._factory.destroy(self); self.page.releaseArena(self.arena);
} }
fn run(ctx: *anyopaque) !?u32 { fn run(ctx: *anyopaque) !?u32 {
const self: *ScheduleCallback = @ptrCast(@alignCast(ctx)); const self: *ScheduleCallback = @ptrCast(@alignCast(ctx));
const page = self.page; const page = self.page;
const window = page.window;
if (self.removed) { if (self.removed) {
_ = page.window._timers.remove(self.timer_id); _ = window._timers.remove(self.timer_id);
self.deinit(); self.deinit();
return null; return null;
} }
@@ -599,7 +614,7 @@ const ScheduleCallback = struct {
}; };
}, },
.animation_frame => { .animation_frame => {
ls.toLocal(self.cb).call(void, .{page.window._performance.now()}) catch |err| { ls.toLocal(self.cb).call(void, .{window._performance.now()}) catch |err| {
log.warn(.js, "window.RAF", .{ .name = self.name, .err = err }); log.warn(.js, "window.RAF", .{ .name = self.name, .err = err });
}; };
}, },
@@ -614,35 +629,43 @@ const ScheduleCallback = struct {
return ms; return ms;
} }
defer self.deinit(); defer self.deinit();
_ = page.window._timers.remove(self.timer_id); _ = window._timers.remove(self.timer_id);
return null; return null;
} }
}; };
const PostMessageCallback = struct { const PostMessageCallback = struct {
window: *Window,
message: js.Value.Global,
origin: []const u8,
page: *Page, page: *Page,
arena: Allocator,
origin: []const u8,
message: js.Value.Global,
fn deinit(self: *PostMessageCallback) void { fn deinit(self: *PostMessageCallback) void {
self.page._factory.destroy(self); self.page.releaseArena(self.arena);
}
fn cancelled(ctx: *anyopaque) void {
const self: *PostMessageCallback = @ptrCast(@alignCast(ctx));
self.page.releaseArena(self.arena);
} }
fn run(ctx: *anyopaque) !?u32 { fn run(ctx: *anyopaque) !?u32 {
const self: *PostMessageCallback = @ptrCast(@alignCast(ctx)); const self: *PostMessageCallback = @ptrCast(@alignCast(ctx));
defer self.deinit(); defer self.deinit();
const page = self.page;
const window = page.window;
const message_event = try MessageEvent.initTrusted("message", .{ const message_event = try MessageEvent.initTrusted("message", .{
.data = self.message, .data = self.message,
.origin = self.origin, .origin = self.origin,
.source = self.window, .source = window,
.bubbles = false, .bubbles = false,
.cancelable = false, .cancelable = false,
}, self.page); }, page);
const event = message_event.asEvent(); const event = message_event.asEvent();
try self.page._event_manager.dispatch(self.window.asEventTarget(), event); try page._event_manager.dispatch(window.asEventTarget(), event);
return null; return null;
} }