Merge pull request #1901 from lightpanda-io/goodbye_origin
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / wba-demo-scripts (push) Has been cancelled
e2e-test / wba-test (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 fmt (push) Has been cancelled
zig-test / zig test using v8 in debug mode (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled

Remove Origins
This commit is contained in:
Karl Seguin
2026-03-21 07:19:47 +08:00
committed by GitHub
17 changed files with 376 additions and 331 deletions

View File

@@ -17,12 +17,15 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const builtin = @import("builtin");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator; const ArenaAllocator = std.heap.ArenaAllocator;
const ArenaPool = @This(); const ArenaPool = @This();
const IS_DEBUG = builtin.mode == .Debug;
allocator: Allocator, allocator: Allocator,
retain_bytes: usize, retain_bytes: usize,
free_list_len: u16 = 0, free_list_len: u16 = 0,
@@ -30,10 +33,17 @@ free_list: ?*Entry = null,
free_list_max: u16, free_list_max: u16,
entry_pool: std.heap.MemoryPool(Entry), entry_pool: std.heap.MemoryPool(Entry),
mutex: std.Thread.Mutex = .{}, mutex: std.Thread.Mutex = .{},
// Debug mode: track acquire/release counts per debug name to detect leaks and double-frees
_leak_track: if (IS_DEBUG) std.StringHashMapUnmanaged(isize) else void = if (IS_DEBUG) .empty else {},
const Entry = struct { const Entry = struct {
next: ?*Entry, next: ?*Entry,
arena: ArenaAllocator, arena: ArenaAllocator,
debug: if (IS_DEBUG) []const u8 else void = if (IS_DEBUG) "" else {},
};
pub const DebugInfo = struct {
debug: []const u8 = "",
}; };
pub fn init(allocator: Allocator, free_list_max: u16, retain_bytes: usize) ArenaPool { pub fn init(allocator: Allocator, free_list_max: u16, retain_bytes: usize) ArenaPool {
@@ -42,10 +52,26 @@ pub fn init(allocator: Allocator, free_list_max: u16, retain_bytes: usize) Arena
.free_list_max = free_list_max, .free_list_max = free_list_max,
.retain_bytes = retain_bytes, .retain_bytes = retain_bytes,
.entry_pool = .init(allocator), .entry_pool = .init(allocator),
._leak_track = if (IS_DEBUG) .empty else {},
}; };
} }
pub fn deinit(self: *ArenaPool) void { pub fn deinit(self: *ArenaPool) void {
if (IS_DEBUG) {
var has_leaks = false;
var it = self._leak_track.iterator();
while (it.next()) |kv| {
if (kv.value_ptr.* != 0) {
std.debug.print("ArenaPool leak detected: '{s}' count={d}\n", .{ kv.key_ptr.*, kv.value_ptr.* });
has_leaks = true;
}
}
if (has_leaks) {
@panic("ArenaPool: leaked arenas detected");
}
self._leak_track.deinit(self.allocator);
}
var entry = self.free_list; var entry = self.free_list;
while (entry) |e| { while (entry) |e| {
entry = e.next; entry = e.next;
@@ -54,13 +80,21 @@ pub fn deinit(self: *ArenaPool) void {
self.entry_pool.deinit(); self.entry_pool.deinit();
} }
pub fn acquire(self: *ArenaPool) !Allocator { pub fn acquire(self: *ArenaPool, dbg: DebugInfo) !Allocator {
self.mutex.lock(); self.mutex.lock();
defer self.mutex.unlock(); defer self.mutex.unlock();
if (self.free_list) |entry| { if (self.free_list) |entry| {
self.free_list = entry.next; self.free_list = entry.next;
self.free_list_len -= 1; self.free_list_len -= 1;
if (IS_DEBUG) {
entry.debug = dbg.debug;
const gop = try self._leak_track.getOrPut(self.allocator, dbg.debug);
if (!gop.found_existing) {
gop.value_ptr.* = 0;
}
gop.value_ptr.* += 1;
}
return entry.arena.allocator(); return entry.arena.allocator();
} }
@@ -68,8 +102,16 @@ pub fn acquire(self: *ArenaPool) !Allocator {
entry.* = .{ entry.* = .{
.next = null, .next = null,
.arena = ArenaAllocator.init(self.allocator), .arena = ArenaAllocator.init(self.allocator),
.debug = if (IS_DEBUG) dbg.debug else {},
}; };
if (IS_DEBUG) {
const gop = try self._leak_track.getOrPut(self.allocator, dbg.debug);
if (!gop.found_existing) {
gop.value_ptr.* = 0;
}
gop.value_ptr.* += 1;
}
return entry.arena.allocator(); return entry.arena.allocator();
} }
@@ -83,6 +125,19 @@ pub fn release(self: *ArenaPool, allocator: Allocator) void {
self.mutex.lock(); self.mutex.lock();
defer self.mutex.unlock(); defer self.mutex.unlock();
if (IS_DEBUG) {
if (self._leak_track.getPtr(entry.debug)) |count| {
count.* -= 1;
if (count.* < 0) {
std.debug.print("ArenaPool double-free detected: '{s}'\n", .{entry.debug});
@panic("ArenaPool: double-free detected");
}
} else {
std.debug.print("ArenaPool release of untracked arena: '{s}'\n", .{entry.debug});
@panic("ArenaPool: release of untracked arena");
}
}
const free_list_len = self.free_list_len; const free_list_len = self.free_list_len;
if (free_list_len == self.free_list_max) { if (free_list_len == self.free_list_max) {
arena.deinit(); arena.deinit();
@@ -106,7 +161,7 @@ test "arena pool - basic acquire and use" {
var pool = ArenaPool.init(testing.allocator, 512, 1024 * 16); var pool = ArenaPool.init(testing.allocator, 512, 1024 * 16);
defer pool.deinit(); defer pool.deinit();
const alloc = try pool.acquire(); const alloc = try pool.acquire(.{ .debug = "test" });
const buf = try alloc.alloc(u8, 64); const buf = try alloc.alloc(u8, 64);
@memset(buf, 0xAB); @memset(buf, 0xAB);
try testing.expectEqual(@as(u8, 0xAB), buf[0]); try testing.expectEqual(@as(u8, 0xAB), buf[0]);
@@ -118,14 +173,14 @@ test "arena pool - reuse entry after release" {
var pool = ArenaPool.init(testing.allocator, 512, 1024 * 16); var pool = ArenaPool.init(testing.allocator, 512, 1024 * 16);
defer pool.deinit(); defer pool.deinit();
const alloc1 = try pool.acquire(); const alloc1 = try pool.acquire(.{ .debug = "test" });
try testing.expectEqual(@as(u16, 0), pool.free_list_len); try testing.expectEqual(@as(u16, 0), pool.free_list_len);
pool.release(alloc1); pool.release(alloc1);
try testing.expectEqual(@as(u16, 1), pool.free_list_len); try testing.expectEqual(@as(u16, 1), pool.free_list_len);
// The same entry should be returned from the free list. // The same entry should be returned from the free list.
const alloc2 = try pool.acquire(); const alloc2 = try pool.acquire(.{ .debug = "test" });
try testing.expectEqual(@as(u16, 0), pool.free_list_len); try testing.expectEqual(@as(u16, 0), pool.free_list_len);
try testing.expectEqual(alloc1.ptr, alloc2.ptr); try testing.expectEqual(alloc1.ptr, alloc2.ptr);
@@ -136,9 +191,9 @@ test "arena pool - multiple concurrent arenas" {
var pool = ArenaPool.init(testing.allocator, 512, 1024 * 16); var pool = ArenaPool.init(testing.allocator, 512, 1024 * 16);
defer pool.deinit(); defer pool.deinit();
const a1 = try pool.acquire(); const a1 = try pool.acquire(.{ .debug = "test1" });
const a2 = try pool.acquire(); const a2 = try pool.acquire(.{ .debug = "test2" });
const a3 = try pool.acquire(); const a3 = try pool.acquire(.{ .debug = "test3" });
// All three must be distinct arenas. // All three must be distinct arenas.
try testing.expect(a1.ptr != a2.ptr); try testing.expect(a1.ptr != a2.ptr);
@@ -161,8 +216,8 @@ test "arena pool - free list respects max limit" {
var pool = ArenaPool.init(testing.allocator, 1, 1024 * 16); var pool = ArenaPool.init(testing.allocator, 1, 1024 * 16);
defer pool.deinit(); defer pool.deinit();
const a1 = try pool.acquire(); const a1 = try pool.acquire(.{ .debug = "test1" });
const a2 = try pool.acquire(); const a2 = try pool.acquire(.{ .debug = "test2" });
pool.release(a1); pool.release(a1);
try testing.expectEqual(@as(u16, 1), pool.free_list_len); try testing.expectEqual(@as(u16, 1), pool.free_list_len);
@@ -176,7 +231,7 @@ test "arena pool - reset clears memory without releasing" {
var pool = ArenaPool.init(testing.allocator, 512, 1024 * 16); var pool = ArenaPool.init(testing.allocator, 512, 1024 * 16);
defer pool.deinit(); defer pool.deinit();
const alloc = try pool.acquire(); const alloc = try pool.acquire(.{ .debug = "test" });
const buf = try alloc.alloc(u8, 128); const buf = try alloc.alloc(u8, 128);
@memset(buf, 0xFF); @memset(buf, 0xFF);
@@ -200,8 +255,8 @@ test "arena pool - deinit with entries in free list" {
// detected by the test allocator). // detected by the test allocator).
var pool = ArenaPool.init(testing.allocator, 512, 1024 * 16); var pool = ArenaPool.init(testing.allocator, 512, 1024 * 16);
const a1 = try pool.acquire(); const a1 = try pool.acquire(.{ .debug = "test1" });
const a2 = try pool.acquire(); const a2 = try pool.acquire(.{ .debug = "test2" });
_ = try a1.alloc(u8, 256); _ = try a1.alloc(u8, 256);
_ = try a2.alloc(u8, 512); _ = try a2.alloc(u8, 512);
pool.release(a1); pool.release(a1);

View File

@@ -302,7 +302,11 @@ pub fn init(self: *Page, frame_id: u32, session: *Session, parent: ?*Page) !void
self._script_manager = ScriptManager.init(browser.allocator, browser.http_client, self); self._script_manager = ScriptManager.init(browser.allocator, browser.http_client, self);
errdefer self._script_manager.deinit(); errdefer self._script_manager.deinit();
self.js = try browser.env.createContext(self); self.js = try browser.env.createContext(self, .{
.identity = &session.identity,
.identity_arena = session.page_arena,
.call_arena = self.call_arena,
});
errdefer self.js.deinit(); errdefer self.js.deinit();
document._page = self; document._page = self;

View File

@@ -24,6 +24,7 @@ const log = @import("../log.zig");
const App = @import("../App.zig"); const App = @import("../App.zig");
const js = @import("js/js.zig"); const js = @import("js/js.zig");
const v8 = js.v8;
const storage = @import("webapi/storage/storage.zig"); const storage = @import("webapi/storage/storage.zig");
const Navigation = @import("webapi/navigation/Navigation.zig"); const Navigation = @import("webapi/navigation/Navigation.zig");
const History = @import("webapi/History.zig"); const History = @import("webapi/History.zig");
@@ -65,17 +66,14 @@ page_arena: Allocator,
// Origin map for same-origin context sharing. Scoped to the root page lifetime. // Origin map for same-origin context sharing. Scoped to the root page lifetime.
origins: std.StringHashMapUnmanaged(*js.Origin) = .empty, origins: std.StringHashMapUnmanaged(*js.Origin) = .empty,
// Identity tracking for the main world. All main world contexts share this,
// ensuring object identity works across same-origin frames.
identity: js.Identity = .{},
// Shared resources for all pages in this session. // Shared resources for all pages in this session.
// These live for the duration of the page tree (root + frames). // These live for the duration of the page tree (root + frames).
arena_pool: *ArenaPool, arena_pool: *ArenaPool,
// In Debug, we use this to see if anything fails to release an arena back to
// the pool.
_arena_pool_leak_track: if (IS_DEBUG) std.AutoHashMapUnmanaged(usize, struct {
owner: []const u8,
count: usize,
}) else void = if (IS_DEBUG) .empty else {},
page: ?Page, page: ?Page,
queued_navigation: std.ArrayList(*Page), queued_navigation: std.ArrayList(*Page),
@@ -84,17 +82,17 @@ queued_navigation: std.ArrayList(*Page),
// about:blank navigations (which may add to queued_navigation). // about:blank navigations (which may add to queued_navigation).
queued_queued_navigation: std.ArrayList(*Page), queued_queued_navigation: std.ArrayList(*Page),
page_id_gen: u32, page_id_gen: u32 = 0,
frame_id_gen: u32, frame_id_gen: u32 = 0,
pub fn init(self: *Session, browser: *Browser, notification: *Notification) !void { pub fn init(self: *Session, browser: *Browser, notification: *Notification) !void {
const allocator = browser.app.allocator; const allocator = browser.app.allocator;
const arena_pool = browser.arena_pool; const arena_pool = browser.arena_pool;
const arena = try arena_pool.acquire(); const arena = try arena_pool.acquire(.{ .debug = "Session" });
errdefer arena_pool.release(arena); errdefer arena_pool.release(arena);
const page_arena = try arena_pool.acquire(); const page_arena = try arena_pool.acquire(.{ .debug = "Session.page_arena" });
errdefer arena_pool.release(page_arena); errdefer arena_pool.release(page_arena);
self.* = .{ self.* = .{
@@ -104,8 +102,6 @@ pub fn init(self: *Session, browser: *Browser, notification: *Notification) !voi
.page_arena = page_arena, .page_arena = page_arena,
.factory = Factory.init(page_arena), .factory = Factory.init(page_arena),
.history = .{}, .history = .{},
.page_id_gen = 0,
.frame_id_gen = 0,
// The prototype (EventTarget) for Navigation is created when a Page is created. // The prototype (EventTarget) for Navigation is created when a Page is created.
.navigation = .{ ._proto = undefined }, .navigation = .{ ._proto = undefined },
.storage_shed = .{}, .storage_shed = .{},
@@ -171,32 +167,11 @@ pub const GetArenaOpts = struct {
}; };
pub fn getArena(self: *Session, opts: GetArenaOpts) !Allocator { pub fn getArena(self: *Session, opts: GetArenaOpts) !Allocator {
const allocator = try self.arena_pool.acquire(); return self.arena_pool.acquire(.{ .debug = opts.debug });
if (comptime IS_DEBUG) {
// Use session's arena (not page_arena) since page_arena gets reset between pages
const gop = try self._arena_pool_leak_track.getOrPut(self.arena, @intFromPtr(allocator.ptr));
if (gop.found_existing and gop.value_ptr.count != 0) {
log.err(.bug, "ArenaPool Double Use", .{ .owner = gop.value_ptr.*.owner });
@panic("ArenaPool Double Use");
}
gop.value_ptr.* = .{ .owner = opts.debug, .count = 1 };
}
return allocator;
} }
pub fn releaseArena(self: *Session, allocator: Allocator) void { pub fn releaseArena(self: *Session, allocator: Allocator) void {
if (comptime IS_DEBUG) { self.arena_pool.release(allocator);
const found = self._arena_pool_leak_track.getPtr(@intFromPtr(allocator.ptr)).?;
if (found.count != 1) {
log.err(.bug, "ArenaPool Double Free", .{ .owner = found.owner, .count = found.count });
if (comptime builtin.is_test) {
@panic("ArenaPool Double Free");
}
return;
}
found.count = 0;
}
return self.arena_pool.release(allocator);
} }
pub fn getOrCreateOrigin(self: *Session, key_: ?[]const u8) !*js.Origin { pub fn getOrCreateOrigin(self: *Session, key_: ?[]const u8) !*js.Origin {
@@ -237,18 +212,9 @@ pub fn releaseOrigin(self: *Session, origin: *js.Origin) void {
/// Reset page_arena and factory for a clean slate. /// Reset page_arena and factory for a clean slate.
/// Called when root page is removed. /// Called when root page is removed.
fn resetPageResources(self: *Session) void { fn resetPageResources(self: *Session) void {
// Check for arena leaks before releasing self.identity.deinit();
if (comptime IS_DEBUG) { self.identity = .{};
var it = self._arena_pool_leak_track.valueIterator();
while (it.next()) |value_ptr| {
if (value_ptr.count > 0) {
log.err(.bug, "ArenaPool Leak", .{ .owner = value_ptr.owner });
}
}
self._arena_pool_leak_track.clearRetainingCapacity();
}
// All origins should have been released when contexts were destroyed
if (comptime IS_DEBUG) { if (comptime IS_DEBUG) {
std.debug.assert(self.origins.count() == 0); std.debug.assert(self.origins.count() == 0);
} }
@@ -259,10 +225,9 @@ fn resetPageResources(self: *Session) void {
while (it.next()) |value| { while (it.next()) |value| {
value.*.deinit(app); value.*.deinit(app);
} }
self.origins.clearRetainingCapacity(); self.origins = .empty;
} }
// Release old page_arena and acquire fresh one
self.frame_id_gen = 0; self.frame_id_gen = 0;
self.arena_pool.reset(self.page_arena, 64 * 1024); self.arena_pool.reset(self.page_arena, 64 * 1024);
self.factory = Factory.init(self.page_arena); self.factory = Factory.init(self.page_arena);
@@ -646,16 +611,6 @@ fn processRootQueuedNavigation(self: *Session) !void {
defer self.arena_pool.release(qn.arena); defer self.arena_pool.release(qn.arena);
// HACK
// Mark as released in tracking BEFORE removePage clears the map.
// We can't call releaseArena() because that would also return the arena
// to the pool, making the memory invalid before we use qn.url/qn.opts.
if (comptime IS_DEBUG) {
if (self._arena_pool_leak_track.getPtr(@intFromPtr(qn.arena.ptr))) |found| {
found.count = 0;
}
}
self.removePage(); self.removePage();
self.page = @as(Page, undefined); self.page = @as(Page, undefined);
@@ -686,3 +641,36 @@ pub fn nextPageId(self: *Session) u32 {
self.page_id_gen = id; self.page_id_gen = id;
return id; return id;
} }
// A type that has a finalizer can have its finalizer called one of two ways.
// The first is from V8 via the WeakCallback we give to weakRef. But that isn't
// guaranteed to fire, so we track this in finalizer_callbacks and call them on
// page reset.
pub const FinalizerCallback = struct {
arena: Allocator,
session: *Session,
ptr: *anyopaque,
global: v8.Global,
identity: *js.Identity,
zig_finalizer: *const fn (ptr: *anyopaque, session: *Session) void,
pub fn deinit(self: *FinalizerCallback) void {
self.zig_finalizer(self.ptr, self.session);
self.session.releaseArena(self.arena);
}
/// Release this item from the identity tracking maps (called after finalizer runs from V8)
pub fn releaseIdentity(self: *FinalizerCallback) void {
const session = self.session;
const id = @intFromPtr(self.ptr);
if (self.identity.identity_map.fetchRemove(id)) |kv| {
var global = kv.value;
v8.v8__Global__Reset(&global);
}
_ = self.identity.finalizer_callbacks.remove(id);
session.releaseArena(self.arena);
}
};

View File

@@ -63,7 +63,9 @@ templates: []*const v8.FunctionTemplate,
// Arena for the lifetime of the context // Arena for the lifetime of the context
arena: Allocator, arena: Allocator,
// The page.call_arena // The call_arena for this context. For main world contexts this is
// page.call_arena. For isolated world contexts this is a separate arena
// owned by the IsolatedWorld.
call_arena: Allocator, call_arena: Allocator,
// Because calls can be nested (i.e.a function calling a callback), // Because calls can be nested (i.e.a function calling a callback),
@@ -79,6 +81,16 @@ local: ?*const js.Local = null,
origin: *Origin, origin: *Origin,
// Identity tracking for this context. For main world contexts, this points to
// Session's Identity. For isolated world contexts (CDP inspector), this points
// to IsolatedWorld's Identity. This ensures same-origin frames share object
// identity while isolated worlds have separate identity tracking.
identity: *js.Identity,
// Allocator to use for identity map operations. For main world contexts this is
// session.page_arena, for isolated worlds it's the isolated world's arena.
identity_arena: Allocator,
// Unlike other v8 types, like functions or objects, modules are not shared // Unlike other v8 types, like functions or objects, modules are not shared
// across origins. // across origins.
global_modules: std.ArrayList(v8.Global) = .empty, global_modules: std.ArrayList(v8.Global) = .empty,
@@ -185,9 +197,8 @@ pub fn setOrigin(self: *Context, key: ?[]const u8) !void {
lp.assert(self.origin.rc == 1, "Ref opaque origin", .{ .rc = self.origin.rc }); lp.assert(self.origin.rc == 1, "Ref opaque origin", .{ .rc = self.origin.rc });
const origin = try self.session.getOrCreateOrigin(key); const origin = try self.session.getOrCreateOrigin(key);
errdefer self.session.releaseOrigin(origin);
try origin.takeover(self.origin);
self.session.releaseOrigin(self.origin);
self.origin = origin; self.origin = origin;
{ {
@@ -203,16 +214,16 @@ pub fn setOrigin(self: *Context, key: ?[]const u8) !void {
} }
pub fn trackGlobal(self: *Context, global: v8.Global) !void { pub fn trackGlobal(self: *Context, global: v8.Global) !void {
return self.origin.trackGlobal(global); return self.identity.globals.append(self.identity_arena, global);
} }
pub fn trackTemp(self: *Context, global: v8.Global) !void { pub fn trackTemp(self: *Context, global: v8.Global) !void {
return self.origin.trackTemp(global); return self.identity.temps.put(self.identity_arena, global.data_ptr, global);
} }
pub fn weakRef(self: *Context, obj: anytype) void { pub fn weakRef(self: *Context, obj: anytype) void {
const resolved = js.Local.resolveValue(obj); const resolved = js.Local.resolveValue(obj);
const fc = self.origin.finalizer_callbacks.get(@intFromPtr(resolved.ptr)) orelse { const fc = self.identity.finalizer_callbacks.get(@intFromPtr(resolved.ptr)) orelse {
if (comptime IS_DEBUG) { if (comptime IS_DEBUG) {
// should not be possible // should not be possible
std.debug.assert(false); std.debug.assert(false);
@@ -224,7 +235,7 @@ pub fn weakRef(self: *Context, obj: anytype) void {
pub fn safeWeakRef(self: *Context, obj: anytype) void { pub fn safeWeakRef(self: *Context, obj: anytype) void {
const resolved = js.Local.resolveValue(obj); const resolved = js.Local.resolveValue(obj);
const fc = self.origin.finalizer_callbacks.get(@intFromPtr(resolved.ptr)) orelse { const fc = self.identity.finalizer_callbacks.get(@intFromPtr(resolved.ptr)) orelse {
if (comptime IS_DEBUG) { if (comptime IS_DEBUG) {
// should not be possible // should not be possible
std.debug.assert(false); std.debug.assert(false);
@@ -237,7 +248,7 @@ pub fn safeWeakRef(self: *Context, obj: anytype) void {
pub fn strongRef(self: *Context, obj: anytype) void { pub fn strongRef(self: *Context, obj: anytype) void {
const resolved = js.Local.resolveValue(obj); const resolved = js.Local.resolveValue(obj);
const fc = self.origin.finalizer_callbacks.get(@intFromPtr(resolved.ptr)) orelse { const fc = self.identity.finalizer_callbacks.get(@intFromPtr(resolved.ptr)) orelse {
if (comptime IS_DEBUG) { if (comptime IS_DEBUG) {
// should not be possible // should not be possible
std.debug.assert(false); std.debug.assert(false);
@@ -247,6 +258,48 @@ pub fn strongRef(self: *Context, obj: anytype) void {
v8.v8__Global__ClearWeak(&fc.global); v8.v8__Global__ClearWeak(&fc.global);
} }
pub const IdentityResult = struct {
value_ptr: *v8.Global,
found_existing: bool,
};
pub fn addIdentity(self: *Context, ptr: usize) !IdentityResult {
const gop = try self.identity.identity_map.getOrPut(self.identity_arena, ptr);
return .{
.value_ptr = gop.value_ptr,
.found_existing = gop.found_existing,
};
}
pub fn releaseTemp(self: *Context, global: v8.Global) void {
if (self.identity.temps.fetchRemove(global.data_ptr)) |kv| {
var g = kv.value;
v8.v8__Global__Reset(&g);
}
}
pub fn createFinalizerCallback(
self: *Context,
global: v8.Global,
ptr: *anyopaque,
zig_finalizer: *const fn (ptr: *anyopaque, session: *Session) void,
) !*Session.FinalizerCallback {
const session = self.session;
const arena = try session.getArena(.{ .debug = "FinalizerCallback" });
errdefer session.releaseArena(arena);
const fc = try arena.create(Session.FinalizerCallback);
fc.* = .{
.arena = arena,
.session = session,
.ptr = ptr,
.global = global,
.zig_finalizer = zig_finalizer,
// Store identity pointer for cleanup when V8 GCs the object
.identity = self.identity,
};
return fc;
}
// Any operation on the context have to be made from a local. // Any operation on the context have to be made from a local.
pub fn localScope(self: *Context, ls: *js.Local.Scope) void { pub fn localScope(self: *Context, ls: *js.Local.Scope) void {
const isolate = self.isolate; const isolate = self.isolate;

View File

@@ -34,6 +34,7 @@ const Snapshot = @import("Snapshot.zig");
const Inspector = @import("Inspector.zig"); const Inspector = @import("Inspector.zig");
const Page = @import("../Page.zig"); const Page = @import("../Page.zig");
const Session = @import("../Session.zig");
const Window = @import("../webapi/Window.zig"); const Window = @import("../webapi/Window.zig");
const JsApis = bridge.JsApis; const JsApis = bridge.JsApis;
@@ -254,8 +255,15 @@ pub fn deinit(self: *Env) void {
allocator.destroy(self.isolate_params); allocator.destroy(self.isolate_params);
} }
pub fn createContext(self: *Env, page: *Page) !*Context { pub const ContextParams = struct {
const context_arena = try self.app.arena_pool.acquire(); identity: *js.Identity,
identity_arena: Allocator,
call_arena: Allocator,
debug_name: []const u8 = "Context",
};
pub fn createContext(self: *Env, page: *Page, params: ContextParams) !*Context {
const context_arena = try self.app.arena_pool.acquire(.{ .debug = params.debug_name });
errdefer self.app.arena_pool.release(context_arena); errdefer self.app.arena_pool.release(context_arena);
const isolate = self.isolate; const isolate = self.isolate;
@@ -300,33 +308,43 @@ pub fn createContext(self: *Env, page: *Page) !*Context {
v8.v8__Object__SetAlignedPointerInInternalField(global_obj, 0, tao); v8.v8__Object__SetAlignedPointerInInternalField(global_obj, 0, tao);
} }
// our window wrapped in a v8::Global
var global_global: v8.Global = undefined;
v8.v8__Global__New(isolate.handle, global_obj, &global_global);
const context_id = self.context_id; const context_id = self.context_id;
self.context_id = context_id + 1; self.context_id = context_id + 1;
const origin = try page._session.getOrCreateOrigin(null); const session = page._session;
errdefer page._session.releaseOrigin(origin); const origin = try session.getOrCreateOrigin(null);
errdefer session.releaseOrigin(origin);
const context = try context_arena.create(Context); const context = try context_arena.create(Context);
context.* = .{ context.* = .{
.env = self, .env = self,
.page = page, .page = page,
.session = page._session,
.origin = origin, .origin = origin,
.id = context_id, .id = context_id,
.session = session,
.isolate = isolate, .isolate = isolate,
.arena = context_arena, .arena = context_arena,
.handle = context_global, .handle = context_global,
.templates = self.templates, .templates = self.templates,
.call_arena = page.call_arena, .call_arena = params.call_arena,
.microtask_queue = microtask_queue, .microtask_queue = microtask_queue,
.script_manager = &page._script_manager, .script_manager = &page._script_manager,
.scheduler = .init(context_arena), .scheduler = .init(context_arena),
.identity = params.identity,
.identity_arena = params.identity_arena,
}; };
try context.origin.identity_map.putNoClobber(origin.arena, @intFromPtr(page.window), global_global);
{
// Multiple contexts can be created for the same Window (via CDP). We only
// need to register the first one.
const gop = try params.identity.identity_map.getOrPut(params.identity_arena, @intFromPtr(page.window));
if (gop.found_existing == false) {
// our window wrapped in a v8::Global
var global_global: v8.Global = undefined;
v8.v8__Global__New(isolate.handle, global_obj, &global_global);
gop.value_ptr.* = global_global;
}
}
// Store a pointer to our context inside the v8 context so that, given // Store a pointer to our context inside the v8 context so that, given
// a v8 context, we can get our context out // a v8 context, we can get our context out

View File

@@ -21,6 +21,7 @@ const js = @import("js.zig");
const v8 = js.v8; const v8 = js.v8;
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const Session = @import("../Session.zig");
const Function = @This(); const Function = @This();
@@ -210,10 +211,10 @@ fn _persist(self: *const Function, comptime is_global: bool) !(if (is_global) Gl
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global); v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
if (comptime is_global) { if (comptime is_global) {
try ctx.trackGlobal(global); try ctx.trackGlobal(global);
return .{ .handle = global, .origin = {} }; return .{ .handle = global, .temps = {} };
} }
try ctx.trackTemp(global); try ctx.trackTemp(global);
return .{ .handle = global, .origin = ctx.origin }; return .{ .handle = global, .temps = &ctx.identity.temps };
} }
pub fn tempWithThis(self: *const Function, value: anytype) !Temp { pub fn tempWithThis(self: *const Function, value: anytype) !Temp {
@@ -237,7 +238,7 @@ const GlobalType = enum(u8) {
fn G(comptime global_type: GlobalType) type { fn G(comptime global_type: GlobalType) type {
return struct { return struct {
handle: v8.Global, handle: v8.Global,
origin: if (global_type == .temp) *js.Origin else void, temps: if (global_type == .temp) *std.AutoHashMapUnmanaged(usize, v8.Global) else void,
const Self = @This(); const Self = @This();
@@ -257,7 +258,10 @@ fn G(comptime global_type: GlobalType) type {
} }
pub fn release(self: *const Self) void { pub fn release(self: *const Self) void {
self.origin.releaseTemp(self.handle); if (self.temps.fetchRemove(self.handle.data_ptr)) |kv| {
var g = kv.value;
v8.v8__Global__Reset(&g);
}
} }
}; };
} }

View File

@@ -0,0 +1,76 @@
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
// Identity manages the mapping between Zig instances and their v8::Object wrappers.
// This provides object identity semantics - the same Zig instance always maps to
// the same JS object within a given Identity scope.
//
// Main world contexts share a single Identity (on Session), ensuring that
// `window.top.document === top's document` works across same-origin frames.
//
// Isolated worlds (CDP inspector) have their own Identity, ensuring their
// v8::Global wrappers don't leak into the main world.
const std = @import("std");
const js = @import("js.zig");
const Session = @import("../Session.zig");
const v8 = js.v8;
const Allocator = std.mem.Allocator;
const Identity = @This();
// Maps Zig instance pointers to their v8::Global(Object) wrappers.
identity_map: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty,
// Tracked global v8 objects that need to be released on cleanup.
globals: std.ArrayList(v8.Global) = .empty,
// Temporary v8 globals that can be released early. Key is global.data_ptr.
temps: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty,
// Finalizer callbacks for weak references. Key is @intFromPtr of the Zig instance.
finalizer_callbacks: std.AutoHashMapUnmanaged(usize, *Session.FinalizerCallback) = .empty,
pub fn deinit(self: *Identity) void {
{
var it = self.finalizer_callbacks.valueIterator();
while (it.next()) |finalizer| {
finalizer.*.deinit();
}
}
{
var it = self.identity_map.valueIterator();
while (it.next()) |global| {
v8.v8__Global__Reset(global);
}
}
for (self.globals.items) |*global| {
v8.v8__Global__Reset(global);
}
{
var it = self.temps.valueIterator();
while (it.next()) |global| {
v8.v8__Global__Reset(global);
}
}
}

View File

@@ -202,20 +202,20 @@ pub fn compileAndRun(self: *const Local, src: []const u8, name: ?[]const u8) !js
// we can just grab it from the identity_map) // we can just grab it from the identity_map)
pub fn mapZigInstanceToJs(self: *const Local, js_obj_handle: ?*const v8.Object, value: anytype) !js.Object { pub fn mapZigInstanceToJs(self: *const Local, js_obj_handle: ?*const v8.Object, value: anytype) !js.Object {
const ctx = self.ctx; const ctx = self.ctx;
const origin_arena = ctx.origin.arena; const context_arena = ctx.arena;
const T = @TypeOf(value); const T = @TypeOf(value);
switch (@typeInfo(T)) { switch (@typeInfo(T)) {
.@"struct" => { .@"struct" => {
// Struct, has to be placed on the heap // Struct, has to be placed on the heap
const heap = try origin_arena.create(T); const heap = try context_arena.create(T);
heap.* = value; heap.* = value;
return self.mapZigInstanceToJs(js_obj_handle, heap); return self.mapZigInstanceToJs(js_obj_handle, heap);
}, },
.pointer => |ptr| { .pointer => |ptr| {
const resolved = resolveValue(value); const resolved = resolveValue(value);
const gop = try ctx.origin.addIdentity(@intFromPtr(resolved.ptr)); const gop = try ctx.addIdentity(@intFromPtr(resolved.ptr));
if (gop.found_existing) { if (gop.found_existing) {
// we've seen this instance before, return the same object // we've seen this instance before, return the same object
return (js.Object.Global{ .handle = gop.value_ptr.* }).local(self); return (js.Object.Global{ .handle = gop.value_ptr.* }).local(self);
@@ -244,7 +244,7 @@ pub fn mapZigInstanceToJs(self: *const Local, js_obj_handle: ?*const v8.Object,
// The TAO contains the pointer to our Zig instance as // The TAO contains the pointer to our Zig instance as
// well as any meta data we'll need to use it later. // well as any meta data we'll need to use it later.
// See the TaggedOpaque struct for more details. // See the TaggedOpaque struct for more details.
const tao = try origin_arena.create(TaggedOpaque); const tao = try context_arena.create(TaggedOpaque);
tao.* = .{ tao.* = .{
.value = resolved.ptr, .value = resolved.ptr,
.prototype_chain = resolved.prototype_chain.ptr, .prototype_chain = resolved.prototype_chain.ptr,
@@ -276,10 +276,10 @@ pub fn mapZigInstanceToJs(self: *const Local, js_obj_handle: ?*const v8.Object,
// Instead, we check if the base has finalizer. The assumption // Instead, we check if the base has finalizer. The assumption
// here is that if a resolve type has a finalizer, then the base // here is that if a resolve type has a finalizer, then the base
// should have a finalizer too. // should have a finalizer too.
const fc = try ctx.origin.createFinalizerCallback(ctx.session, gop.value_ptr.*, resolved.ptr, resolved.finalizer_from_zig.?); const fc = try ctx.createFinalizerCallback(gop.value_ptr.*, resolved.ptr, resolved.finalizer_from_zig.?);
{ {
errdefer fc.deinit(); errdefer fc.deinit();
try ctx.origin.finalizer_callbacks.put(ctx.origin.arena, @intFromPtr(resolved.ptr), fc); try ctx.identity.finalizer_callbacks.put(ctx.identity_arena, @intFromPtr(resolved.ptr), fc);
} }
conditionallyReference(value); conditionallyReference(value);

View File

@@ -16,19 +16,21 @@
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
// Origin represents the shared Zig<->JS bridge state for all contexts within // Origin represents the security token for contexts within the same origin.
// the same origin. Multiple contexts (frames) from the same origin share a // Multiple contexts (frames) from the same origin share a single Origin,
// single Origin, ensuring that JS objects maintain their identity across frames. // which provides the V8 SecurityToken that allows cross-context access.
//
// Note: Identity tracking (mapping Zig instances to v8::Objects) is managed
// separately via js.Identity - Session has the main world Identity, and
// IsolatedWorlds have their own Identity instances.
const std = @import("std"); const std = @import("std");
const js = @import("js.zig"); const js = @import("js.zig");
const App = @import("../../App.zig"); const App = @import("../../App.zig");
const Session = @import("../Session.zig");
const v8 = js.v8; const v8 = js.v8;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const IS_DEBUG = @import("builtin").mode == .Debug;
const Origin = @This(); const Origin = @This();
@@ -38,40 +40,12 @@ arena: Allocator,
// The key, e.g. lightpanda.io:443 // The key, e.g. lightpanda.io:443
key: []const u8, key: []const u8,
// Security token - all contexts in this realm must use the same v8::Value instance // Security token - all contexts in this origin must use the same v8::Value instance
// as their security token for V8 to allow cross-context access // as their security token for V8 to allow cross-context access
security_token: v8.Global, security_token: v8.Global,
// Serves two purposes. Like `global_objects`, this is used to free
// every Global(Object) we've created during the lifetime of the realm.
// More importantly, it serves as an identity map - for a given Zig
// instance, we map it to the same Global(Object).
// The key is the @intFromPtr of the Zig value
identity_map: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty,
// Some web APIs have to manage opaque values. Ideally, they use an
// js.Object, but the js.Object has no lifetime guarantee beyond the
// current call. They can call .persist() on their js.Object to get
// a `Global(Object)`. We need to track these to free them.
// This used to be a map and acted like identity_map; the key was
// the @intFromPtr(js_obj.handle). But v8 can re-use address. Without
// a reliable way to know if an object has already been persisted,
// we now simply persist every time persist() is called.
globals: std.ArrayList(v8.Global) = .empty,
// Temp variants stored in HashMaps for O(1) early cleanup.
// Key is global.data_ptr.
temps: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty,
// Any type that is stored in the identity_map which has a finalizer declared
// will have its finalizer stored here. This is only used when shutting down
// if v8 hasn't called the finalizer directly itself.
finalizer_callbacks: std.AutoHashMapUnmanaged(usize, *FinalizerCallback) = .empty,
taken_over: std.ArrayList(*Origin),
pub fn init(app: *App, isolate: js.Isolate, key: []const u8) !*Origin { pub fn init(app: *App, isolate: js.Isolate, key: []const u8) !*Origin {
const arena = try app.arena_pool.acquire(); const arena = try app.arena_pool.acquire(.{ .debug = "Origin" });
errdefer app.arena_pool.release(arena); errdefer app.arena_pool.release(arena);
var hs: js.HandleScope = undefined; var hs: js.HandleScope = undefined;
@@ -88,175 +62,12 @@ pub fn init(app: *App, isolate: js.Isolate, key: []const u8) !*Origin {
.rc = 1, .rc = 1,
.arena = arena, .arena = arena,
.key = owned_key, .key = owned_key,
.temps = .empty,
.globals = .empty,
.taken_over = .empty,
.security_token = token_global, .security_token = token_global,
}; };
return self; return self;
} }
pub fn deinit(self: *Origin, app: *App) void { pub fn deinit(self: *Origin, app: *App) void {
for (self.taken_over.items) |o| {
o.deinit(app);
}
// Call finalizers before releasing anything
{
var it = self.finalizer_callbacks.valueIterator();
while (it.next()) |finalizer| {
finalizer.*.deinit();
}
}
v8.v8__Global__Reset(&self.security_token); v8.v8__Global__Reset(&self.security_token);
{
var it = self.identity_map.valueIterator();
while (it.next()) |global| {
v8.v8__Global__Reset(global);
}
}
for (self.globals.items) |*global| {
v8.v8__Global__Reset(global);
}
{
var it = self.temps.valueIterator();
while (it.next()) |global| {
v8.v8__Global__Reset(global);
}
}
app.arena_pool.release(self.arena); app.arena_pool.release(self.arena);
} }
pub fn trackGlobal(self: *Origin, global: v8.Global) !void {
return self.globals.append(self.arena, global);
}
pub const IdentityResult = struct {
value_ptr: *v8.Global,
found_existing: bool,
};
pub fn addIdentity(self: *Origin, ptr: usize) !IdentityResult {
const gop = try self.identity_map.getOrPut(self.arena, ptr);
return .{
.value_ptr = gop.value_ptr,
.found_existing = gop.found_existing,
};
}
pub fn trackTemp(self: *Origin, global: v8.Global) !void {
return self.temps.put(self.arena, global.data_ptr, global);
}
pub fn releaseTemp(self: *Origin, global: v8.Global) void {
if (self.temps.fetchRemove(global.data_ptr)) |kv| {
var g = kv.value;
v8.v8__Global__Reset(&g);
}
}
/// Release an item from the identity_map (called after finalizer runs from V8)
pub fn release(self: *Origin, item: *anyopaque) void {
var global = self.identity_map.fetchRemove(@intFromPtr(item)) orelse {
if (comptime IS_DEBUG) {
std.debug.assert(false);
}
return;
};
v8.v8__Global__Reset(&global.value);
// The item has been finalized, remove it from the finalizer callback so that
// we don't try to call it again on shutdown.
const kv = self.finalizer_callbacks.fetchRemove(@intFromPtr(item)) orelse {
if (comptime IS_DEBUG) {
std.debug.assert(false);
}
return;
};
const fc = kv.value;
fc.session.releaseArena(fc.arena);
}
pub fn createFinalizerCallback(
self: *Origin,
session: *Session,
global: v8.Global,
ptr: *anyopaque,
zig_finalizer: *const fn (ptr: *anyopaque, session: *Session) void,
) !*FinalizerCallback {
const arena = try session.getArena(.{ .debug = "FinalizerCallback" });
errdefer session.releaseArena(arena);
const fc = try arena.create(FinalizerCallback);
fc.* = .{
.arena = arena,
.origin = self,
.session = session,
.ptr = ptr,
.global = global,
.zig_finalizer = zig_finalizer,
};
return fc;
}
pub fn takeover(self: *Origin, original: *Origin) !void {
const arena = self.arena;
try self.globals.ensureUnusedCapacity(arena, original.globals.items.len);
for (original.globals.items) |obj| {
self.globals.appendAssumeCapacity(obj);
}
original.globals.clearRetainingCapacity();
{
try self.temps.ensureUnusedCapacity(arena, original.temps.count());
var it = original.temps.iterator();
while (it.next()) |kv| {
try self.temps.put(arena, kv.key_ptr.*, kv.value_ptr.*);
}
original.temps.clearRetainingCapacity();
}
{
try self.finalizer_callbacks.ensureUnusedCapacity(arena, original.finalizer_callbacks.count());
var it = original.finalizer_callbacks.iterator();
while (it.next()) |kv| {
kv.value_ptr.*.origin = self;
try self.finalizer_callbacks.put(arena, kv.key_ptr.*, kv.value_ptr.*);
}
original.finalizer_callbacks.clearRetainingCapacity();
}
{
try self.identity_map.ensureUnusedCapacity(arena, original.identity_map.count());
var it = original.identity_map.iterator();
while (it.next()) |kv| {
try self.identity_map.put(arena, kv.key_ptr.*, kv.value_ptr.*);
}
original.identity_map.clearRetainingCapacity();
}
try self.taken_over.append(self.arena, original);
}
// A type that has a finalizer can have its finalizer called one of two ways.
// The first is from V8 via the WeakCallback we give to weakRef. But that isn't
// guaranteed to fire, so we track this in finalizer_callbacks and call them on
// origin shutdown.
pub const FinalizerCallback = struct {
arena: Allocator,
origin: *Origin,
session: *Session,
ptr: *anyopaque,
global: v8.Global,
zig_finalizer: *const fn (ptr: *anyopaque, session: *Session) void,
pub fn deinit(self: *FinalizerCallback) void {
self.zig_finalizer(self.ptr, self.session);
self.session.releaseArena(self.arena);
}
};

View File

@@ -16,9 +16,12 @@
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const js = @import("js.zig"); const js = @import("js.zig");
const v8 = js.v8; const v8 = js.v8;
const Session = @import("../Session.zig");
const Promise = @This(); const Promise = @This();
local: *const js.Local, local: *const js.Local,
@@ -63,10 +66,10 @@ fn _persist(self: *const Promise, comptime is_global: bool) !(if (is_global) Glo
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global); v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
if (comptime is_global) { if (comptime is_global) {
try ctx.trackGlobal(global); try ctx.trackGlobal(global);
return .{ .handle = global, .origin = {} }; return .{ .handle = global, .temps = {} };
} }
try ctx.trackTemp(global); try ctx.trackTemp(global);
return .{ .handle = global, .origin = ctx.origin }; return .{ .handle = global, .temps = &ctx.identity.temps };
} }
pub const Temp = G(.temp); pub const Temp = G(.temp);
@@ -80,7 +83,7 @@ const GlobalType = enum(u8) {
fn G(comptime global_type: GlobalType) type { fn G(comptime global_type: GlobalType) type {
return struct { return struct {
handle: v8.Global, handle: v8.Global,
origin: if (global_type == .temp) *js.Origin else void, temps: if (global_type == .temp) *std.AutoHashMapUnmanaged(usize, v8.Global) else void,
const Self = @This(); const Self = @This();
@@ -96,7 +99,10 @@ fn G(comptime global_type: GlobalType) type {
} }
pub fn release(self: *const Self) void { pub fn release(self: *const Self) void {
self.origin.releaseTemp(self.handle); if (self.temps.fetchRemove(self.handle.data_ptr)) |kv| {
var g = kv.value;
v8.v8__Global__Reset(&g);
}
} }
}; };
} }

View File

@@ -56,7 +56,7 @@ fn _toSlice(self: String, comptime null_terminate: bool, allocator: Allocator) !
pub fn toSSO(self: String, comptime global: bool) !(if (global) SSO.Global else SSO) { pub fn toSSO(self: String, comptime global: bool) !(if (global) SSO.Global else SSO) {
if (comptime global) { if (comptime global) {
return .{ .str = try self.toSSOWithAlloc(self.local.ctx.origin.arena) }; return .{ .str = try self.toSSOWithAlloc(self.local.ctx.session.page_arena) };
} }
return self.toSSOWithAlloc(self.local.call_arena); return self.toSSOWithAlloc(self.local.call_arena);
} }

View File

@@ -25,6 +25,7 @@ const v8 = js.v8;
const IS_DEBUG = @import("builtin").mode == .Debug; const IS_DEBUG = @import("builtin").mode == .Debug;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const Session = @import("../Session.zig");
const Value = @This(); const Value = @This();
@@ -300,10 +301,10 @@ fn _persist(self: *const Value, comptime is_global: bool) !(if (is_global) Globa
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global); v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
if (comptime is_global) { if (comptime is_global) {
try ctx.trackGlobal(global); try ctx.trackGlobal(global);
return .{ .handle = global, .origin = {} }; return .{ .handle = global, .temps = {} };
} }
try ctx.trackTemp(global); try ctx.trackTemp(global);
return .{ .handle = global, .origin = ctx.origin }; return .{ .handle = global, .temps = &ctx.identity.temps };
} }
pub fn toZig(self: Value, comptime T: type) !T { pub fn toZig(self: Value, comptime T: type) !T {
@@ -361,7 +362,7 @@ const GlobalType = enum(u8) {
fn G(comptime global_type: GlobalType) type { fn G(comptime global_type: GlobalType) type {
return struct { return struct {
handle: v8.Global, handle: v8.Global,
origin: if (global_type == .temp) *js.Origin else void, temps: if (global_type == .temp) *std.AutoHashMapUnmanaged(usize, v8.Global) else void,
const Self = @This(); const Self = @This();
@@ -381,7 +382,10 @@ fn G(comptime global_type: GlobalType) type {
} }
pub fn release(self: *const Self) void { pub fn release(self: *const Self) void {
self.origin.releaseTemp(self.handle); if (self.temps.fetchRemove(self.handle.data_ptr)) |kv| {
var g = kv.value;
v8.v8__Global__Reset(&g);
}
} }
}; };
} }

View File

@@ -27,7 +27,6 @@ const v8 = js.v8;
const Caller = @import("Caller.zig"); const Caller = @import("Caller.zig");
const Context = @import("Context.zig"); const Context = @import("Context.zig");
const Origin = @import("Origin.zig");
const IS_DEBUG = @import("builtin").mode == .Debug; const IS_DEBUG = @import("builtin").mode == .Debug;
@@ -117,13 +116,12 @@ pub fn Builder(comptime T: type) type {
.from_v8 = struct { .from_v8 = struct {
fn wrap(handle: ?*const v8.WeakCallbackInfo) callconv(.c) void { fn wrap(handle: ?*const v8.WeakCallbackInfo) callconv(.c) void {
const ptr = v8.v8__WeakCallbackInfo__GetParameter(handle.?).?; const ptr = v8.v8__WeakCallbackInfo__GetParameter(handle.?).?;
const fc: *Origin.FinalizerCallback = @ptrCast(@alignCast(ptr)); const fc: *Session.FinalizerCallback = @ptrCast(@alignCast(ptr));
const origin = fc.origin;
const value_ptr = fc.ptr; const value_ptr = fc.ptr;
if (origin.finalizer_callbacks.contains(@intFromPtr(value_ptr))) { if (fc.identity.finalizer_callbacks.contains(@intFromPtr(value_ptr))) {
func(@ptrCast(@alignCast(value_ptr)), false, fc.session); func(@ptrCast(@alignCast(value_ptr)), false, fc.session);
origin.release(value_ptr); fc.releaseIdentity();
} else { } else {
// A bit weird, but v8 _requires_ that we release it // A bit weird, but v8 _requires_ that we release it
// If we don't. We'll 100% crash. // If we don't. We'll 100% crash.

View File

@@ -25,6 +25,7 @@ pub const Env = @import("Env.zig");
pub const bridge = @import("bridge.zig"); pub const bridge = @import("bridge.zig");
pub const Caller = @import("Caller.zig"); pub const Caller = @import("Caller.zig");
pub const Origin = @import("Origin.zig"); pub const Origin = @import("Origin.zig");
pub const Identity = @import("Identity.zig");
pub const Context = @import("Context.zig"); pub const Context = @import("Context.zig");
pub const Local = @import("Local.zig"); pub const Local = @import("Local.zig");
pub const Inspector = @import("Inspector.zig"); pub const Inspector = @import("Inspector.zig");

View File

@@ -93,12 +93,12 @@ pub fn init(callback: js.Function.Temp, options: ?ObserverInit, page: *Page) !*I
} }
pub fn deinit(self: *IntersectionObserver, shutdown: bool, session: *Session) void { pub fn deinit(self: *IntersectionObserver, shutdown: bool, session: *Session) void {
if (shutdown) {
self._callback.release(); self._callback.release();
session.releaseArena(self._arena); if ((comptime IS_DEBUG) and !shutdown) {
} else if (comptime IS_DEBUG) { std.debug.assert(self._observing.items.len == 0);
std.debug.assert(false);
} }
session.releaseArena(self._arena);
} }
pub fn observe(self: *IntersectionObserver, target: *Element, page: *Page) !void { pub fn observe(self: *IntersectionObserver, target: *Element, page: *Page) !void {
@@ -111,6 +111,7 @@ pub fn observe(self: *IntersectionObserver, target: *Element, page: *Page) !void
// Register with page if this is our first observation // Register with page if this is our first observation
if (self._observing.items.len == 0) { if (self._observing.items.len == 0) {
page.js.strongRef(self);
try page.registerIntersectionObserver(self); try page.registerIntersectionObserver(self);
} }
@@ -145,18 +146,22 @@ pub fn unobserve(self: *IntersectionObserver, target: *Element, page: *Page) voi
break; break;
} }
} }
if (self._observing.items.len == 0) {
page.js.safeWeakRef(self);
}
} }
pub fn disconnect(self: *IntersectionObserver, page: *Page) void { pub fn disconnect(self: *IntersectionObserver, page: *Page) void {
page.unregisterIntersectionObserver(self);
self._observing.clearRetainingCapacity();
self._previous_states.clearRetainingCapacity(); self._previous_states.clearRetainingCapacity();
for (self._pending_entries.items) |entry| { for (self._pending_entries.items) |entry| {
entry.deinit(false, page._session); entry.deinit(false, page._session);
} }
self._pending_entries.clearRetainingCapacity(); self._pending_entries.clearRetainingCapacity();
page.js.safeWeakRef(self);
self._observing.clearRetainingCapacity();
page.unregisterIntersectionObserver(self);
} }
pub fn takeRecords(self: *IntersectionObserver, page: *Page) ![]*IntersectionObserverEntry { pub fn takeRecords(self: *IntersectionObserver, page: *Page) ![]*IntersectionObserverEntry {
@@ -358,6 +363,7 @@ pub const JsApi = struct {
pub const name = "IntersectionObserver"; pub const name = "IntersectionObserver";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(IntersectionObserver.deinit); pub const finalizer = bridge.finalizer(IntersectionObserver.deinit);
}; };

View File

@@ -86,12 +86,12 @@ pub fn init(callback: js.Function.Temp, page: *Page) !*MutationObserver {
} }
pub fn deinit(self: *MutationObserver, shutdown: bool, session: *Session) void { pub fn deinit(self: *MutationObserver, shutdown: bool, session: *Session) void {
if (shutdown) {
self._callback.release(); self._callback.release();
session.releaseArena(self._arena); if ((comptime IS_DEBUG) and !shutdown) {
} else if (comptime IS_DEBUG) { std.debug.assert(self._observing.items.len == 0);
std.debug.assert(false);
} }
session.releaseArena(self._arena);
} }
pub fn observe(self: *MutationObserver, target: *Node, options: ObserveOptions, page: *Page) !void { pub fn observe(self: *MutationObserver, target: *Node, options: ObserveOptions, page: *Page) !void {
@@ -158,6 +158,7 @@ pub fn observe(self: *MutationObserver, target: *Node, options: ObserveOptions,
// Register with page if this is our first observation // Register with page if this is our first observation
if (self._observing.items.len == 0) { if (self._observing.items.len == 0) {
page.js.strongRef(self);
try page.registerMutationObserver(self); try page.registerMutationObserver(self);
} }
@@ -168,13 +169,13 @@ pub fn observe(self: *MutationObserver, target: *Node, options: ObserveOptions,
} }
pub fn disconnect(self: *MutationObserver, page: *Page) void { pub fn disconnect(self: *MutationObserver, page: *Page) void {
page.unregisterMutationObserver(self);
self._observing.clearRetainingCapacity();
for (self._pending_records.items) |record| { for (self._pending_records.items) |record| {
record.deinit(false, page._session); record.deinit(false, page._session);
} }
self._pending_records.clearRetainingCapacity(); self._pending_records.clearRetainingCapacity();
page.js.safeWeakRef(self);
self._observing.clearRetainingCapacity();
page.unregisterMutationObserver(self);
} }
pub fn takeRecords(self: *MutationObserver, page: *Page) ![]*MutationRecord { pub fn takeRecords(self: *MutationObserver, page: *Page) ![]*MutationRecord {
@@ -440,6 +441,7 @@ pub const JsApi = struct {
pub const name = "MutationObserver"; pub const name = "MutationObserver";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(MutationObserver.deinit); pub const finalizer = bridge.finalizer(MutationObserver.deinit);
}; };

View File

@@ -489,12 +489,16 @@ pub fn BrowserContext(comptime CDP_T: type) type {
pub fn createIsolatedWorld(self: *Self, world_name: []const u8, grant_universal_access: bool) !*IsolatedWorld { pub fn createIsolatedWorld(self: *Self, world_name: []const u8, grant_universal_access: bool) !*IsolatedWorld {
const browser = &self.cdp.browser; const browser = &self.cdp.browser;
const arena = try browser.arena_pool.acquire(); const arena = try browser.arena_pool.acquire(.{ .debug = "IsolatedWorld" });
errdefer browser.arena_pool.release(arena); errdefer browser.arena_pool.release(arena);
const call_arena = try browser.arena_pool.acquire(.{ .debug = "IsolatedWorld.call_arena" });
errdefer browser.arena_pool.release(call_arena);
const world = try arena.create(IsolatedWorld); const world = try arena.create(IsolatedWorld);
world.* = .{ world.* = .{
.arena = arena, .arena = arena,
.call_arena = call_arena,
.context = null, .context = null,
.browser = browser, .browser = browser,
.name = try arena.dupe(u8, world_name), .name = try arena.dupe(u8, world_name),
@@ -745,13 +749,20 @@ pub fn BrowserContext(comptime CDP_T: type) type {
/// An object id is unique across all contexts, different object ids can refer to the same Node in different contexts. /// An object id is unique across all contexts, different object ids can refer to the same Node in different contexts.
const IsolatedWorld = struct { const IsolatedWorld = struct {
arena: Allocator, arena: Allocator,
call_arena: Allocator,
browser: *Browser, browser: *Browser,
name: []const u8, name: []const u8,
context: ?*js.Context = null, context: ?*js.Context = null,
grant_universal_access: bool, grant_universal_access: bool,
// Identity tracking for this isolated world (separate from main world).
// This ensures CDP inspector contexts don't share v8::Globals with main world.
identity: js.Identity = .{},
pub fn deinit(self: *IsolatedWorld) void { pub fn deinit(self: *IsolatedWorld) void {
self.removeContext() catch {}; self.removeContext() catch {};
self.identity.deinit();
self.browser.arena_pool.release(self.call_arena);
self.browser.arena_pool.release(self.arena); self.browser.arena_pool.release(self.arena);
} }
@@ -759,6 +770,8 @@ const IsolatedWorld = struct {
const ctx = self.context orelse return error.NoIsolatedContextToRemove; const ctx = self.context orelse return error.NoIsolatedContextToRemove;
self.browser.env.destroyContext(ctx); self.browser.env.destroyContext(ctx);
self.context = null; self.context = null;
self.identity.deinit();
self.identity = .{};
} }
// The isolate world must share at least some of the state with the related page, specifically the DocumentHTML // The isolate world must share at least some of the state with the related page, specifically the DocumentHTML
@@ -768,7 +781,13 @@ const IsolatedWorld = struct {
// Currently we have only 1 page/frame and thus also only 1 state in the isolate world. // Currently we have only 1 page/frame and thus also only 1 state in the isolate world.
pub fn createContext(self: *IsolatedWorld, page: *Page) !*js.Context { pub fn createContext(self: *IsolatedWorld, page: *Page) !*js.Context {
if (self.context == null) { if (self.context == null) {
self.context = try self.browser.env.createContext(page); const ctx = try self.browser.env.createContext(page, .{
.identity = &self.identity,
.identity_arena = self.arena,
.call_arena = self.call_arena,
.debug_name = "IsolatedContext",
});
self.context = ctx;
} else { } else {
log.warn(.cdp, "not implemented", .{ log.warn(.cdp, "not implemented", .{
.feature = "createContext: Not implemented second isolated context creation", .feature = "createContext: Not implemented second isolated context creation",