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;
}
// 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
// the page still exists
@@ -266,6 +270,8 @@ fn reset(self: *Page, comptime initializing: bool) !void {
const browser = self._session.browser;
if (comptime initializing == false) {
self.scheduler.deinit();
browser.env.destroyContext(self.js);
// 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
// memory usage as low as possible.
browser.env.memoryPressureNotification(.moderate);
self._script_manager.shutdown = true;
browser.http_client.abort();
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 {
name: []const u8 = "",
low_priority: bool = false,
finalizer: ?Finalizer = null,
};
pub fn add(self: *Scheduler, ctx: *anyopaque, cb: Callback, run_in_ms: u32, opts: AddOpts) !void {
if (comptime IS_DEBUG) {
@@ -63,6 +69,7 @@ pub fn add(self: *Scheduler, ctx: *anyopaque, cb: Callback, run_in_ms: u32, opts
.callback = cb,
.sequence = seq,
.name = opts.name,
.finalizer = opts.finalizer,
.run_at = milliTimestamp(.monotonic) + run_in_ms,
});
}
@@ -105,12 +112,23 @@ fn runQueue(self: *Scheduler, queue: *Queue) !?u64 {
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 {
run_at: u64,
sequence: u64,
ctx: *anyopaque,
name: []const u8,
callback: Callback,
finalizer: ?Finalizer,
};
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 Selection = @import("Selection.zig");
const Allocator = std.mem.Allocator;
const Window = @This();
_proto: *EventTarget,
@@ -353,18 +355,21 @@ pub fn postMessage(self: *Window, message: js.Value.Global, target_origin: ?[]co
_ = target_origin;
// postMessage queues a task (not a microtask), so use the scheduler
const origin = try self._location.getOrigin(page);
const callback = try page._factory.create(PostMessageCallback{
.window = self,
.message = message,
.origin = try page.arena.dupe(u8, origin),
.page = page,
});
errdefer page._factory.destroy(callback);
const arena = try page.getArena(.{ .debug = "Window.schedule" });
errdefer page.releaseArena(arena);
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, .{
.name = "postMessage",
.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;
}
const arena = try page.getArena(.{ .debug = "Window.schedule" });
errdefer page.releaseArena(arena);
const timer_id = self._timer_id +% 1;
self._timer_id = timer_id;
const params = opts.params;
var persisted_params: []js.Value.Temp = &.{};
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);
@@ -524,21 +532,23 @@ fn scheduleCallback(self: *Window, cb: js.Function.Temp, delay_ms: u32, opts: Sc
}
errdefer _ = self._timers.remove(timer_id);
const callback = try page._factory.create(ScheduleCallback{
const callback = try arena.create(ScheduleCallback);
callback.* = .{
.cb = cb,
.page = page,
.arena = arena,
.mode = opts.mode,
.name = opts.name,
.timer_id = timer_id,
.params = persisted_params,
.repeat_ms = if (opts.repeat) if (delay_ms == 0) 1 else delay_ms else null,
});
};
gop.value_ptr.* = callback;
errdefer page._factory.destroy(callback);
try page.scheduler.add(callback, ScheduleCallback.run, delay_ms, .{
.name = opts.name,
.low_priority = opts.low_priority,
.finalizer = ScheduleCallback.cancelled,
});
return timer_id;
@@ -556,13 +566,11 @@ const ScheduleCallback = struct {
cb: js.Function.Temp,
page: *Page,
params: []const js.Value.Temp,
removed: bool = false,
mode: Mode,
page: *Page,
arena: Allocator,
removed: bool = false,
params: []const js.Value.Temp,
const Mode = enum {
idle,
@@ -570,19 +578,26 @@ const ScheduleCallback = struct {
animation_frame,
};
fn cancelled(ctx: *anyopaque) void {
var self: *ScheduleCallback = @ptrCast(@alignCast(ctx));
self.deinit();
}
fn deinit(self: *ScheduleCallback) void {
self.page.js.release(self.cb);
for (self.params) |param| {
self.page.js.release(param);
}
self.page._factory.destroy(self);
self.page.releaseArena(self.arena);
}
fn run(ctx: *anyopaque) !?u32 {
const self: *ScheduleCallback = @ptrCast(@alignCast(ctx));
const page = self.page;
const window = page.window;
if (self.removed) {
_ = page.window._timers.remove(self.timer_id);
_ = window._timers.remove(self.timer_id);
self.deinit();
return null;
}
@@ -599,7 +614,7 @@ const ScheduleCallback = struct {
};
},
.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 });
};
},
@@ -614,35 +629,43 @@ const ScheduleCallback = struct {
return ms;
}
defer self.deinit();
_ = page.window._timers.remove(self.timer_id);
_ = window._timers.remove(self.timer_id);
return null;
}
};
const PostMessageCallback = struct {
window: *Window,
message: js.Value.Global,
origin: []const u8,
page: *Page,
arena: Allocator,
origin: []const u8,
message: js.Value.Global,
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 {
const self: *PostMessageCallback = @ptrCast(@alignCast(ctx));
defer self.deinit();
const page = self.page;
const window = page.window;
const message_event = try MessageEvent.initTrusted("message", .{
.data = self.message,
.origin = self.origin,
.source = self.window,
.source = window,
.bubbles = false,
.cancelable = false,
}, self.page);
}, page);
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;
}