From 5d963043323d1e0b3a78831cffdddd6eff761732 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Fri, 30 Jan 2026 17:23:03 +0800 Subject: [PATCH] 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. --- src/browser/Page.zig | 7 ++- src/browser/Scheduler.zig | 18 ++++++++ src/browser/webapi/Window.zig | 81 ++++++++++++++++++++++------------- 3 files changed, 76 insertions(+), 30 deletions(-) diff --git a/src/browser/Page.zig b/src/browser/Page.zig index 3f8b4ba0..e270e27c 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -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(); diff --git a/src/browser/Scheduler.zig b/src/browser/Scheduler.zig index c3246124..1bd3e60a 100644 --- a/src/browser/Scheduler.zig +++ b/src/browser/Scheduler.zig @@ -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; diff --git a/src/browser/webapi/Window.zig b/src/browser/webapi/Window.zig index 8b275c86..9adf87a8 100644 --- a/src/browser/webapi/Window.zig +++ b/src/browser/webapi/Window.zig @@ -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; }