Merge pull request #1468 from lightpanda-io/context_safety

Fixes a few context issues.
This commit is contained in:
Karl Seguin
2026-02-05 07:51:46 +08:00
committed by GitHub
4 changed files with 116 additions and 39 deletions

View File

@@ -243,15 +243,6 @@ pub fn deinit(self: *Page) void {
// stats.print(&stream) catch unreachable; // stats.print(&stream) catch unreachable;
} }
{
// some MicroTasks might be referencing the page, we need to drain it while
// the page still exists
var ls: JS.Local.Scope = undefined;
self.js.localScope(&ls);
defer ls.deinit();
ls.local.runMicrotasks();
}
const session = self._session; const session = self._session;
session.browser.env.destroyContext(self.js); session.browser.env.destroyContext(self.js);

View File

@@ -122,6 +122,10 @@ script_manager: ?*ScriptManager,
// Our macrotasks // Our macrotasks
scheduler: Scheduler, scheduler: Scheduler,
// Prevents us from enqueuing a microtask for this context while we're shutting
// down.
shutting_down: bool = false,
const ModuleEntry = struct { const ModuleEntry = struct {
// Can be null if we're asynchrously loading the module, in // Can be null if we're asynchrously loading the module, in
// which case resolver_promise cannot be null. // which case resolver_promise cannot be null.
@@ -154,12 +158,21 @@ pub fn fromIsolate(isolate: js.Isolate) *Context {
} }
pub fn deinit(self: *Context) void { pub fn deinit(self: *Context) void {
var page = self.page; defer self.env.app.arena_pool.release(self.arena);
const prev_context = page.js;
page.js = self;
defer page.js = prev_context;
// This can release JS objects var hs: js.HandleScope = undefined;
const entered = self.enter(&hs);
defer entered.exit();
// We might have microtasks in the isolate that refence this context. The
// only option we have is to run them. But a microtask could queue another
// microtask, so we set the shutting_down flag, so that any such microtask
// will be a noop (this isn't automatic, when v8 calls our microtask callback
// the first thing we'll check is if self.shutting_down == true).
self.shutting_down = true;
self.env.runMicrotasks();
// can release objects
self.scheduler.deinit(); self.scheduler.deinit();
{ {
@@ -214,13 +227,10 @@ pub fn deinit(self: *Context) void {
} }
if (self.entered) { if (self.entered) {
var ls: js.Local.Scope = undefined; v8.v8__Context__Exit(@ptrCast(v8.v8__Global__Get(&self.handle, self.isolate.handle)));
self.localScope(&ls);
defer ls.deinit();
v8.v8__Context__Exit(ls.local.handle);
} }
v8.v8__Global__Reset(&self.handle); v8.v8__Global__Reset(&self.handle);
self.env.app.arena_pool.release(self.arena);
} }
pub fn weakRef(self: *Context, obj: anytype) void { pub fn weakRef(self: *Context, obj: anytype) void {
@@ -880,41 +890,90 @@ fn resolveDynamicModule(self: *Context, state: *DynamicModuleResolveState, modul
}; };
} }
// Microtasks // Used to make temporarily enter and exit a context, updating and restoring
// page.js:
// var hs: js.HandleScope = undefined;
// const entered = ctx.enter(&hs);
// defer entered.exit();
pub fn enter(self: *Context, hs: *js.HandleScope) Entered {
const isolate = self.isolate;
js.HandleScope.init(hs, isolate);
const page = self.page;
const original = page.js;
page.js = self;
const handle: *const v8.Context = @ptrCast(v8.v8__Global__Get(&self.handle, isolate.handle));
v8.v8__Context__Enter(handle);
return .{.original = original, .handle = handle, .handle_scope = hs};
}
const Entered = struct {
// the context we should restore on the page
original: *Context,
// the handle of the entered context
handle: *const v8.Context,
handle_scope: *js.HandleScope,
pub fn exit(self: Entered) void {
self.original.page.js = self.original;
v8.v8__Context__Exit(self.handle);
self.handle_scope.deinit();
}
};
pub fn queueMutationDelivery(self: *Context) !void { pub fn queueMutationDelivery(self: *Context) !void {
self.isolate.enqueueMicrotask(struct { self.enqueueMicrotask(struct {
fn run(data: ?*anyopaque) callconv(.c) void { fn run(ctx: *Context) void {
const page: *Page = @ptrCast(@alignCast(data.?)); ctx.page.deliverMutations();
page.deliverMutations();
} }
}.run, self.page); }.run);
} }
pub fn queueIntersectionChecks(self: *Context) !void { pub fn queueIntersectionChecks(self: *Context) !void {
self.isolate.enqueueMicrotask(struct { self.enqueueMicrotask(struct {
fn run(data: ?*anyopaque) callconv(.c) void { fn run(ctx: *Context) void {
const page: *Page = @ptrCast(@alignCast(data.?)); ctx.page.performScheduledIntersectionChecks();
page.performScheduledIntersectionChecks();
} }
}.run, self.page); }.run);
} }
pub fn queueIntersectionDelivery(self: *Context) !void { pub fn queueIntersectionDelivery(self: *Context) !void {
self.isolate.enqueueMicrotask(struct { self.enqueueMicrotask(struct {
fn run(data: ?*anyopaque) callconv(.c) void { fn run(ctx: *Context) void {
const page: *Page = @ptrCast(@alignCast(data.?)); ctx.page.deliverIntersections();
page.deliverIntersections();
} }
}.run, self.page); }.run);
} }
pub fn queueSlotchangeDelivery(self: *Context) !void { pub fn queueSlotchangeDelivery(self: *Context) !void {
self.enqueueMicrotask(struct {
fn run(ctx: *Context) void {
ctx.page.deliverSlotchangeEvents();
}
}.run);
}
// Helper for executing a Microtask on this Context. In V8, microtasks aren't
// associated to a Context - they are just functions to execute in an Isolate.
// But for these Context microtasks, we want to (a) make sure the context isn't
// being shut down and (b) that it's entered.
fn enqueueMicrotask(self: *Context, callback: anytype) void {
self.isolate.enqueueMicrotask(struct { self.isolate.enqueueMicrotask(struct {
fn run(data: ?*anyopaque) callconv(.c) void { fn run(data: ?*anyopaque) callconv(.c) void {
const page: *Page = @ptrCast(@alignCast(data.?)); const ctx: *Context = @ptrCast(@alignCast(data.?));
page.deliverSlotchangeEvents(); if (ctx.shutting_down) {
return;
}
var hs: js.HandleScope = undefined;
const entered = ctx.enter(&hs);
defer entered.exit();
callback(ctx);
} }
}.run, self.page); }.run, self);
} }
pub fn queueMicrotaskFunc(self: *Context, cb: js.Function) void { pub fn queueMicrotaskFunc(self: *Context, cb: js.Function) void {

View File

@@ -18,6 +18,8 @@
const std = @import("std"); const std = @import("std");
const js = @import("js.zig"); const js = @import("js.zig");
const builtin = @import("builtin");
const v8 = js.v8; const v8 = js.v8;
const App = @import("../../App.zig"); const App = @import("../../App.zig");
@@ -35,7 +37,7 @@ const Window = @import("../webapi/Window.zig");
const JsApis = bridge.JsApis; const JsApis = bridge.JsApis;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const IS_DEBUG = @import("builtin").mode == .Debug; const IS_DEBUG = builtin.mode == .Debug;
// The Env maps to a V8 isolate, which represents a isolated sandbox for // The Env maps to a V8 isolate, which represents a isolated sandbox for
// executing JavaScript. The Env is where we'll define our V8 <-> Zig bindings, // executing JavaScript. The Env is where we'll define our V8 <-> Zig bindings,
@@ -284,6 +286,20 @@ pub fn runMicrotasks(self: *const Env) void {
pub fn runMacrotasks(self: *Env) !?u64 { pub fn runMacrotasks(self: *Env) !?u64 {
var ms_to_next_task: ?u64 = null; var ms_to_next_task: ?u64 = null;
for (self.contexts.items) |ctx| { for (self.contexts.items) |ctx| {
if (comptime builtin.is_test == false) {
// I hate this comptime check as much as you do. But we have tests
// which rely on short execution before shutdown. In real world, it's
// underterministic whether a timer will or won't run before the
// page shutsdown. But for tests, we need to run them to their end.
if (ctx.scheduler.hasReadyTasks() == false) {
continue;
}
}
var hs: js.HandleScope = undefined;
const entered = ctx.enter(&hs);
defer entered.exit();
const ms = (try ctx.scheduler.run()) orelse continue; const ms = (try ctx.scheduler.run()) orelse continue;
if (ms_to_next_task == null or ms < ms_to_next_task.?) { if (ms_to_next_task == null or ms < ms_to_next_task.?) {
ms_to_next_task = ms; ms_to_next_task = ms;

View File

@@ -74,11 +74,17 @@ pub fn add(self: *Scheduler, ctx: *anyopaque, cb: Callback, run_in_ms: u32, opts
}); });
} }
pub fn run(self: *Scheduler) !?u64 { pub fn run(self: *Scheduler) !?u64 {
_ = try self.runQueue(&self.low_priority); _ = try self.runQueue(&self.low_priority);
return self.runQueue(&self.high_priority); return self.runQueue(&self.high_priority);
} }
pub fn hasReadyTasks(self: *Scheduler) bool {
const now = milliTimestamp(.monotonic);
return queueuHasReadyTask(&self.low_priority, now) or queueuHasReadyTask(&self.high_priority, now);
}
fn runQueue(self: *Scheduler, queue: *Queue) !?u64 { fn runQueue(self: *Scheduler, queue: *Queue) !?u64 {
if (queue.count() == 0) { if (queue.count() == 0) {
return null; return null;
@@ -112,6 +118,11 @@ fn runQueue(self: *Scheduler, queue: *Queue) !?u64 {
return null; return null;
} }
fn queueuHasReadyTask(queue: *Queue, now: u64) bool {
const task = queue.peek() orelse return false;
return task.run_at <= now;
}
fn finalizeTasks(queue: *Queue) void { fn finalizeTasks(queue: *Queue) void {
var it = queue.iterator(); var it = queue.iterator();
while (it.next()) |t| { while (it.next()) |t| {