Merge pull request #1459 from lightpanda-io/browser_context_arenas

Add a dedicated browser_context and page_arena to CDP.
This commit is contained in:
Karl Seguin
2026-02-04 07:51:35 +08:00
committed by GitHub
4 changed files with 51 additions and 26 deletions

View File

@@ -76,6 +76,13 @@ pub fn CDPT(comptime TypeProvider: type) type {
// Used for processing notifications within a browser context. // Used for processing notifications within a browser context.
notification_arena: std.heap.ArenaAllocator, notification_arena: std.heap.ArenaAllocator,
// Valid for 1 page navigation (what CDP calls a "renderer")
page_arena: std.heap.ArenaAllocator,
// Valid for the entire lifetime of the BrowserContext. Should minimize
// (or altogether elimiate) our use of this.
browser_context_arena: std.heap.ArenaAllocator,
const Self = @This(); const Self = @This();
pub fn init(app: *App, client: TypeProvider.Client) !Self { pub fn init(app: *App, client: TypeProvider.Client) !Self {
@@ -90,8 +97,10 @@ pub fn CDPT(comptime TypeProvider: type) type {
.browser = browser, .browser = browser,
.allocator = allocator, .allocator = allocator,
.browser_context = null, .browser_context = null,
.page_arena = std.heap.ArenaAllocator.init(allocator),
.message_arena = std.heap.ArenaAllocator.init(allocator), .message_arena = std.heap.ArenaAllocator.init(allocator),
.notification_arena = std.heap.ArenaAllocator.init(allocator), .notification_arena = std.heap.ArenaAllocator.init(allocator),
.browser_context_arena = std.heap.ArenaAllocator.init(allocator),
}; };
} }
@@ -100,8 +109,10 @@ pub fn CDPT(comptime TypeProvider: type) type {
bc.deinit(); bc.deinit();
} }
self.browser.deinit(); self.browser.deinit();
self.page_arena.deinit();
self.message_arena.deinit(); self.message_arena.deinit();
self.notification_arena.deinit(); self.notification_arena.deinit();
self.browser_context_arena.deinit();
} }
pub fn handleMessage(self: *Self, msg: []const u8) bool { pub fn handleMessage(self: *Self, msg: []const u8) bool {
@@ -323,9 +334,12 @@ pub fn BrowserContext(comptime CDP_T: type) type {
// RELATION TO SESSION_ID // RELATION TO SESSION_ID
session: *Session, session: *Session,
// Points to the session arena // Tied to the lifetime of the BrowserContext
arena: Allocator, arena: Allocator,
// Tied to the lifetime of 1 page rendered in the BrowserContext.
page_arena: Allocator,
// From the parent's notification_arena.allocator(). Most of the CDP // From the parent's notification_arena.allocator(). Most of the CDP
// code paths deal with a cmd which has its own arena (from the // code paths deal with a cmd which has its own arena (from the
// message_arena). But notifications happen outside of the typical CDP // message_arena). But notifications happen outside of the typical CDP
@@ -355,12 +369,12 @@ pub fn BrowserContext(comptime CDP_T: type) type {
node_search_list: Node.Search.List, node_search_list: Node.Search.List,
inspector_session: *js.Inspector.Session, inspector_session: *js.Inspector.Session,
isolated_worlds: std.ArrayListUnmanaged(IsolatedWorld), isolated_worlds: std.ArrayList(*IsolatedWorld),
http_proxy_changed: bool = false, http_proxy_changed: bool = false,
// Extra headers to add to all requests. // Extra headers to add to all requests.
extra_headers: std.ArrayListUnmanaged([*c]const u8) = .empty, extra_headers: std.ArrayList([*c]const u8) = .empty,
intercept_state: InterceptState, intercept_state: InterceptState,
@@ -373,7 +387,7 @@ pub fn BrowserContext(comptime CDP_T: type) type {
// ever streamed. So if CDP is the only thing that needs bodies in // ever streamed. So if CDP is the only thing that needs bodies in
// memory for an arbitrary amount of time, then that's where we're going // memory for an arbitrary amount of time, then that's where we're going
// to store the, // to store the,
captured_responses: std.AutoHashMapUnmanaged(usize, std.ArrayListUnmanaged(u8)), captured_responses: std.AutoHashMapUnmanaged(usize, std.ArrayList(u8)),
const Self = @This(); const Self = @This();
@@ -381,7 +395,6 @@ pub fn BrowserContext(comptime CDP_T: type) type {
const allocator = cdp.allocator; const allocator = cdp.allocator;
const session = try cdp.browser.newSession(); const session = try cdp.browser.newSession();
const arena = session.arena;
const browser = &cdp.browser; const browser = &cdp.browser;
const inspector_session = browser.env.inspector.?.startSession(self); const inspector_session = browser.env.inspector.?.startSession(self);
@@ -393,7 +406,6 @@ pub fn BrowserContext(comptime CDP_T: type) type {
self.* = .{ self.* = .{
.id = id, .id = id,
.cdp = cdp, .cdp = cdp,
.arena = arena,
.target_id = null, .target_id = null,
.session_id = null, .session_id = null,
.session = session, .session = session,
@@ -405,6 +417,8 @@ pub fn BrowserContext(comptime CDP_T: type) type {
.node_search_list = undefined, .node_search_list = undefined,
.isolated_worlds = .empty, .isolated_worlds = .empty,
.inspector_session = inspector_session, .inspector_session = inspector_session,
.page_arena = cdp.page_arena.allocator(),
.arena = cdp.browser_context_arena.allocator(),
.notification_arena = cdp.notification_arena.allocator(), .notification_arena = cdp.notification_arena.allocator(),
.intercept_state = try InterceptState.init(allocator), .intercept_state = try InterceptState.init(allocator),
.captured_responses = .empty, .captured_responses = .empty,
@@ -442,7 +456,7 @@ pub fn BrowserContext(comptime CDP_T: type) type {
transfer.abort(error.ClientDisconnect); transfer.abort(error.ClientDisconnect);
} }
for (self.isolated_worlds.items) |*world| { for (self.isolated_worlds.items) |world| {
world.deinit(); world.deinit();
} }
self.isolated_worlds.clearRetainingCapacity(); self.isolated_worlds.clearRetainingCapacity();
@@ -472,16 +486,21 @@ 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 owned_name = try self.arena.dupe(u8, world_name); const browser = &self.cdp.browser;
const world = try self.isolated_worlds.addOne(self.arena); const arena = try browser.arena_pool.acquire();
errdefer browser.arena_pool.release(arena);
const world = try arena.create(IsolatedWorld);
world.* = .{ world.* = .{
.arena = arena,
.context = null, .context = null,
.name = owned_name, .browser = browser,
.env = &self.cdp.browser.env, .name = try arena.dupe(u8, world_name),
.grant_universal_access = grant_universal_access, .grant_universal_access = grant_universal_access,
}; };
try self.isolated_worlds.append(self.arena, world);
return world; return world;
} }
@@ -634,7 +653,7 @@ pub fn BrowserContext(comptime CDP_T: type) type {
pub fn onHttpResponseData(ctx: *anyopaque, msg: *const Notification.ResponseData) !void { pub fn onHttpResponseData(ctx: *anyopaque, msg: *const Notification.ResponseData) !void {
const self: *Self = @ptrCast(@alignCast(ctx)); const self: *Self = @ptrCast(@alignCast(ctx));
const arena = self.arena; const arena = self.page_arena;
const id = msg.transfer.id; const id = msg.transfer.id;
const gop = try self.captured_responses.getOrPut(arena, id); const gop = try self.captured_responses.getOrPut(arena, id);
@@ -700,7 +719,7 @@ pub fn BrowserContext(comptime CDP_T: type) type {
// + 10 for the max websocket header // + 10 for the max websocket header
const message_len = msg.len + session_id.len + 1 + field.len + 10; const message_len = msg.len + session_id.len + 1 + field.len + 10;
var buf: std.ArrayListUnmanaged(u8) = .{}; var buf: std.ArrayList(u8) = .{};
buf.ensureTotalCapacity(allocator, message_len) catch |err| { buf.ensureTotalCapacity(allocator, message_len) catch |err| {
log.err(.cdp, "inspector buffer", .{ .err = err }); log.err(.cdp, "inspector buffer", .{ .err = err });
return; return;
@@ -734,20 +753,20 @@ pub fn BrowserContext(comptime CDP_T: type) type {
/// Generally the client needs to resolve a node into the isolated world to be able to work with it. /// Generally the client needs to resolve a node into the isolated world to be able to work with it.
/// 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,
browser: *Browser,
name: []const u8, name: []const u8,
env: *js.Env,
context: ?*js.Context = null, context: ?*js.Context = null,
grant_universal_access: bool, grant_universal_access: bool,
pub fn deinit(self: *IsolatedWorld) void { pub fn deinit(self: *IsolatedWorld) void {
if (self.context) |ctx| { self.removeContext() catch {};
self.env.destroyContext(ctx); self.browser.arena_pool.release(self.arena);
self.context = null;
}
} }
pub fn removeContext(self: *IsolatedWorld) !void { pub fn removeContext(self: *IsolatedWorld) !void {
const ctx = self.context orelse return error.NoIsolatedContextToRemove; const ctx = self.context orelse return error.NoIsolatedContextToRemove;
self.env.destroyContext(ctx); self.browser.env.destroyContext(ctx);
self.context = null; self.context = null;
} }
@@ -758,7 +777,7 @@ 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.env.createContext(page, false); self.context = try self.browser.env.createContext(page, false);
} 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",

View File

@@ -284,7 +284,7 @@ fn resolveNode(cmd: anytype) !void {
break :blk; break :blk;
} }
// not the default scope, check the other ones // not the default scope, check the other ones
for (bc.isolated_worlds.items) |*isolated_world| { for (bc.isolated_worlds.items) |isolated_world| {
ls.?.deinit(); ls.?.deinit();
ls = null; ls = null;

View File

@@ -167,7 +167,7 @@ fn close(cmd: anytype) !void {
} }
bc.session.removePage(); bc.session.removePage();
for (bc.isolated_worlds.items) |*world| { for (bc.isolated_worlds.items) |world| {
world.deinit(); world.deinit();
} }
bc.isolated_worlds.clearRetainingCapacity(); bc.isolated_worlds.clearRetainingCapacity();
@@ -275,15 +275,21 @@ pub fn pageNavigate(arena: Allocator, bc: anytype, event: *const Notification.Pa
pub fn pageRemove(bc: anytype) !void { pub fn pageRemove(bc: anytype) !void {
// The main page is going to be removed, we need to remove contexts from other worlds first. // The main page is going to be removed, we need to remove contexts from other worlds first.
for (bc.isolated_worlds.items) |*isolated_world| { for (bc.isolated_worlds.items) |isolated_world| {
try isolated_world.removeContext(); try isolated_world.removeContext();
} }
} }
pub fn pageCreated(bc: anytype, page: *Page) !void { pub fn pageCreated(bc: anytype, page: *Page) !void {
for (bc.isolated_worlds.items) |*isolated_world| { _ = bc.cdp.page_arena.reset(.{ .retain_with_limit = 1024 * 512 });
for (bc.isolated_worlds.items) |isolated_world| {
_ = try isolated_world.createContext(page); _ = try isolated_world.createContext(page);
} }
// Only retain captured responses until a navigation event. In CDP term,
// this is called a "renderer" and the cache-duration can be controlled via
// the Network.configureDurableMessages message (which we don't support)
bc.captured_responses = .empty;
} }
pub fn pageNavigated(arena: Allocator, bc: anytype, event: *const Notification.PageNavigated) !void { pub fn pageNavigated(arena: Allocator, bc: anytype, event: *const Notification.PageNavigated) !void {
@@ -359,7 +365,7 @@ pub fn pageNavigated(arena: Allocator, bc: anytype, event: *const Notification.P
true, true,
); );
} }
for (bc.isolated_worlds.items) |*isolated_world| { for (bc.isolated_worlds.items) |isolated_world| {
const aux_json = try std.fmt.allocPrint(arena, "{{\"isDefault\":false,\"type\":\"isolated\",\"frameId\":\"{s}\"}}", .{target_id}); const aux_json = try std.fmt.allocPrint(arena, "{{\"isDefault\":false,\"type\":\"isolated\",\"frameId\":\"{s}\"}}", .{target_id});
// Calling contextCreated will assign a new Id to the context and send the contextCreated event // Calling contextCreated will assign a new Id to the context and send the contextCreated event

View File

@@ -283,7 +283,7 @@ fn closeTarget(cmd: anytype) !void {
} }
bc.session.removePage(); bc.session.removePage();
for (bc.isolated_worlds.items) |*world| { for (bc.isolated_worlds.items) |world| {
world.deinit(); world.deinit();
} }
bc.isolated_worlds.clearRetainingCapacity(); bc.isolated_worlds.clearRetainingCapacity();