Merge pull request #1445 from lightpanda-io/schedule_task_finalizer
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
e2e-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled

Allow [schedule] tasks to have finalizers
This commit is contained in:
Karl Seguin
2026-01-31 07:06:24 +08:00
committed by GitHub
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;
}