Merge pull request #1432 from lightpanda-io/remove_execution_world

Remove js.ExecutionWorld
This commit is contained in:
Karl Seguin
2026-01-30 06:54:55 +08:00
committed by GitHub
11 changed files with 132 additions and 192 deletions

View File

@@ -53,7 +53,7 @@ notification: *Notification,
pub fn init(app: *App) !Browser { pub fn init(app: *App) !Browser {
const allocator = app.allocator; const allocator = app.allocator;
var env = try js.Env.init(allocator, &app.platform, &app.snapshot); var env = try js.Env.init(app);
errdefer env.deinit(); errdefer env.deinit();
const notification = try Notification.init(allocator, app.notification); const notification = try Notification.init(allocator, app.notification);

View File

@@ -244,7 +244,7 @@ pub fn deinit(self: *Page) void {
} }
const session = self._session; const session = self._session;
session.executor.removeContext(); session.browser.env.destroyContext(self.js);
self._script_manager.shutdown = true; self._script_manager.shutdown = true;
session.browser.http_client.abort(); session.browser.http_client.abort();
@@ -263,8 +263,10 @@ pub fn deinit(self: *Page) void {
} }
fn reset(self: *Page, comptime initializing: bool) !void { fn reset(self: *Page, comptime initializing: bool) !void {
const browser = self._session.browser;
if (comptime initializing == false) { if (comptime initializing == false) {
self._session.executor.removeContext(); browser.env.destroyContext(self.js);
// removing a context can trigger finalizers, so we can only check for // removing a context can trigger finalizers, so we can only check for
// a leak after the above. // a leak after the above.
@@ -276,15 +278,14 @@ fn reset(self: *Page, comptime initializing: bool) !void {
self._arena_pool_leak_track.clearRetainingCapacity(); self._arena_pool_leak_track.clearRetainingCapacity();
} }
// We force a garbage collection between page navigations to keep v8 // We force a garbage collection between page navigations to keep v8
// memory usage as low as possible. // memory usage as low as possible.
self._session.browser.env.memoryPressureNotification(.moderate); browser.env.memoryPressureNotification(.moderate);
self._script_manager.shutdown = true; self._script_manager.shutdown = true;
self._session.browser.http_client.abort(); browser.http_client.abort();
self._script_manager.deinit(); self._script_manager.deinit();
_ = self._session.browser.page_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 }); _ = browser.page_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 });
} }
self._factory = Factory.init(self); self._factory = Factory.init(self);
@@ -320,7 +321,7 @@ fn reset(self: *Page, comptime initializing: bool) !void {
self._script_manager = ScriptManager.init(self); self._script_manager = ScriptManager.init(self);
errdefer self._script_manager.deinit(); errdefer self._script_manager.deinit();
self.js = try self._session.executor.createContext(self, true); self.js = try browser.env.createContext(self, true);
errdefer self.js.deinit(); errdefer self.js.deinit();
self._element_styles = .{}; self._element_styles = .{};

View File

@@ -19,7 +19,6 @@
const std = @import("std"); const std = @import("std");
const builtin = @import("builtin"); const builtin = @import("builtin");
const js = @import("js/js.zig");
const log = @import("../log.zig"); const log = @import("../log.zig");
const milliTimestamp = @import("../datetime.zig").milliTimestamp; const milliTimestamp = @import("../datetime.zig").milliTimestamp;

View File

@@ -53,7 +53,6 @@ arena: Allocator,
// page and start another. // page and start another.
transfer_arena: Allocator, transfer_arena: Allocator,
executor: js.ExecutionWorld,
cookie_jar: storage.Cookie.Jar, cookie_jar: storage.Cookie.Jar,
storage_shed: storage.Shed, storage_shed: storage.Shed,
@@ -63,20 +62,16 @@ navigation: Navigation,
page: ?*Page = null, page: ?*Page = null,
pub fn init(self: *Session, browser: *Browser) !void { pub fn init(self: *Session, browser: *Browser) !void {
var executor = try browser.env.newExecutionWorld();
errdefer executor.deinit();
const allocator = browser.app.allocator; const allocator = browser.app.allocator;
const session_allocator = browser.session_arena.allocator(); const session_allocator = browser.session_arena.allocator();
self.* = .{ self.* = .{
.browser = browser, .history = .{},
.executor = executor, .navigation = .{},
.storage_shed = .{}, .storage_shed = .{},
.browser = browser,
.arena = session_allocator, .arena = session_allocator,
.cookie_jar = storage.Cookie.Jar.init(allocator), .cookie_jar = storage.Cookie.Jar.init(allocator),
.navigation = .{},
.history = .{},
.transfer_arena = browser.transfer_arena.allocator(), .transfer_arena = browser.transfer_arena.allocator(),
}; };
} }
@@ -87,7 +82,6 @@ pub fn deinit(self: *Session) void {
} }
self.cookie_jar.deinit(); self.cookie_jar.deinit();
self.storage_shed.deinit(self.browser.app.allocator); self.storage_shed.deinit(self.browser.app.allocator);
self.executor.deinit();
} }
// NOTE: the caller is not the owner of the returned value, // NOTE: the caller is not the owner of the returned value,

View File

@@ -21,6 +21,7 @@ const lp = @import("lightpanda");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const js = @import("js.zig"); const js = @import("js.zig");
const Env = @import("Env.zig");
const bridge = @import("bridge.zig"); const bridge = @import("bridge.zig");
const TaggedOpaque = @import("TaggedOpaque.zig"); const TaggedOpaque = @import("TaggedOpaque.zig");
@@ -38,6 +39,7 @@ const IS_DEBUG = @import("builtin").mode == .Debug;
const Context = @This(); const Context = @This();
id: usize, id: usize,
env: *Env,
page: *Page, page: *Page,
isolate: js.Isolate, isolate: js.Isolate,
@@ -207,6 +209,7 @@ pub fn deinit(self: *Context) void {
v8.v8__Context__Exit(ls.local.handle); 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 {

View File

@@ -20,6 +20,7 @@ const std = @import("std");
const js = @import("js.zig"); const js = @import("js.zig");
const v8 = js.v8; const v8 = js.v8;
const App = @import("../../App.zig");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const bridge = @import("bridge.zig"); const bridge = @import("bridge.zig");
@@ -28,13 +29,13 @@ const Isolate = @import("Isolate.zig");
const Platform = @import("Platform.zig"); const Platform = @import("Platform.zig");
const Snapshot = @import("Snapshot.zig"); const Snapshot = @import("Snapshot.zig");
const Inspector = @import("Inspector.zig"); const Inspector = @import("Inspector.zig");
const ExecutionWorld = @import("ExecutionWorld.zig");
const Page = @import("../Page.zig");
const Window = @import("../webapi/Window.zig"); 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 ArenaAllocator = std.heap.ArenaAllocator; const IS_DEBUG = @import("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,
@@ -44,13 +45,15 @@ const ArenaAllocator = std.heap.ArenaAllocator;
// The `types` parameter is a tuple of Zig structures we want to bind to V8. // The `types` parameter is a tuple of Zig structures we want to bind to V8.
const Env = @This(); const Env = @This();
allocator: Allocator, app: *App,
platform: *const Platform, platform: *const Platform,
// the global isolate // the global isolate
isolate: js.Isolate, isolate: js.Isolate,
contexts: std.ArrayList(*js.Context),
// just kept around because we need to free it on deinit // just kept around because we need to free it on deinit
isolate_params: *v8.CreateParams, isolate_params: *v8.CreateParams,
@@ -65,7 +68,10 @@ templates: []*const v8.FunctionTemplate,
// Global template created once per isolate and reused across all contexts // Global template created once per isolate and reused across all contexts
global_template: v8.Eternal, global_template: v8.Eternal,
pub fn init(allocator: Allocator, platform: *const Platform, snapshot: *Snapshot) !Env { pub fn init(app: *App) !Env {
const allocator = app.allocator;
const snapshot = &app.snapshot;
var params = try allocator.create(v8.CreateParams); var params = try allocator.create(v8.CreateParams);
errdefer allocator.destroy(params); errdefer allocator.destroy(params);
v8.v8__Isolate__CreateParams__CONSTRUCT(params); v8.v8__Isolate__CreateParams__CONSTRUCT(params);
@@ -140,10 +146,11 @@ pub fn init(allocator: Allocator, platform: *const Platform, snapshot: *Snapshot
} }
return .{ return .{
.app = app,
.context_id = 0, .context_id = 0,
.contexts = .empty,
.isolate = isolate, .isolate = isolate,
.platform = platform, .platform = &app.platform,
.allocator = allocator,
.templates = templates, .templates = templates,
.isolate_params = params, .isolate_params = params,
.eternal_function_templates = eternal_function_templates, .eternal_function_templates = eternal_function_templates,
@@ -152,13 +159,94 @@ pub fn init(allocator: Allocator, platform: *const Platform, snapshot: *Snapshot
} }
pub fn deinit(self: *Env) void { pub fn deinit(self: *Env) void {
self.allocator.free(self.templates); if (comptime IS_DEBUG) {
self.allocator.free(self.eternal_function_templates); std.debug.assert(self.contexts.items.len == 0);
}
for (self.contexts.items) |ctx| {
ctx.deinit();
}
const allocator = self.app.allocator;
self.contexts.deinit(allocator);
allocator.free(self.templates);
allocator.free(self.eternal_function_templates);
self.isolate.exit(); self.isolate.exit();
self.isolate.deinit(); self.isolate.deinit();
v8.v8__ArrayBuffer__Allocator__DELETE(self.isolate_params.array_buffer_allocator.?); v8.v8__ArrayBuffer__Allocator__DELETE(self.isolate_params.array_buffer_allocator.?);
self.allocator.destroy(self.isolate_params); allocator.destroy(self.isolate_params);
}
pub fn createContext(self: *Env, page: *Page, enter: bool) !*Context {
const context_arena = try self.app.arena_pool.acquire();
errdefer self.app.arena_pool.release(context_arena);
const isolate = self.isolate;
var hs: js.HandleScope = undefined;
hs.init(isolate);
defer hs.deinit();
// Get the global template that was created once per isolate
const global_template: *const v8.ObjectTemplate = @ptrCast(@alignCast(v8.v8__Eternal__Get(&self.global_template, isolate.handle).?));
const v8_context = v8.v8__Context__New(isolate.handle, global_template, null).?;
// Create the v8::Context and wrap it in a v8::Global
var context_global: v8.Global = undefined;
v8.v8__Global__New(isolate.handle, v8_context, &context_global);
// our window wrapped in a v8::Global
const global_obj = v8.v8__Context__Global(v8_context).?;
var global_global: v8.Global = undefined;
v8.v8__Global__New(isolate.handle, global_obj, &global_global);
if (enter) {
v8.v8__Context__Enter(v8_context);
}
errdefer if (enter) {
v8.v8__Context__Exit(v8_context);
};
const context_id = self.context_id;
self.context_id = context_id + 1;
const context = try context_arena.create(Context);
context.* = .{
.env = self,
.page = page,
.id = context_id,
.entered = enter,
.isolate = isolate,
.arena = context_arena,
.handle = context_global,
.templates = self.templates,
.script_manager = &page._script_manager,
.call_arena = page.call_arena,
};
try context.identity_map.putNoClobber(context_arena, @intFromPtr(page.window), global_global);
// Store a pointer to our context inside the v8 context so that, given
// a v8 context, we can get our context out
const data = isolate.initBigInt(@intFromPtr(context));
v8.v8__Context__SetEmbedderData(v8_context, 1, @ptrCast(data.handle));
try self.contexts.append(self.app.allocator, context);
return context;
}
pub fn destroyContext(self: *Env, context: *Context) void {
for (self.contexts.items, 0..) |ctx, i| {
if (ctx == context) {
_ = self.contexts.swapRemove(i);
break;
}
} else {
if (comptime IS_DEBUG) {
@panic("Tried to remove unknown context");
}
}
context.deinit();
self.isolate.notifyContextDisposed();
} }
pub fn newInspector(self: *Env, arena: Allocator, ctx: anytype) !*Inspector { pub fn newInspector(self: *Env, arena: Allocator, ctx: anytype) !*Inspector {
@@ -182,13 +270,6 @@ pub fn pumpMessageLoop(self: *const Env) bool {
pub fn runIdleTasks(self: *const Env) void { pub fn runIdleTasks(self: *const Env) void {
v8.v8__Platform__RunIdleTasks(self.platform.handle, self.isolate.handle, 1); v8.v8__Platform__RunIdleTasks(self.platform.handle, self.isolate.handle, 1);
} }
pub fn newExecutionWorld(self: *Env) !ExecutionWorld {
return .{
.env = self,
.context = null,
.context_arena = ArenaAllocator.init(self.allocator),
};
}
// V8 doesn't immediately free memory associated with // V8 doesn't immediately free memory associated with
// a Context, it's managed by the garbage collector. We use the // a Context, it's managed by the garbage collector. We use the

View File

@@ -1,136 +0,0 @@
// Copyright (C) 2023-2025 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/>.
const std = @import("std");
const lp = @import("lightpanda");
const log = @import("../../log.zig");
const Page = @import("../Page.zig");
const js = @import("js.zig");
const v8 = js.v8;
const Env = @import("Env.zig");
const bridge = @import("bridge.zig");
const Context = @import("Context.zig");
const ArenaAllocator = std.heap.ArenaAllocator;
const IS_DEBUG = @import("builtin").mode == .Debug;
const CONTEXT_ARENA_RETAIN = 1024 * 64;
// ExecutionWorld closely models a JS World.
// https://chromium.googlesource.com/chromium/src/+/master/third_party/blink/renderer/bindings/core/v8/V8BindingDesign.md#World
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/scripting/ExecutionWorld
const ExecutionWorld = @This();
env: *Env,
// Arena whose lifetime is for a single page load. Where
// the call_arena lives for a single function call, the context_arena
// lives for the lifetime of the entire page. The allocator will be
// owned by the Context, but the arena itself is owned by the ExecutionWorld
// so that we can re-use it from context to context.
context_arena: ArenaAllocator,
// Currently a context maps to a Browser's Page. Here though, it's only a
// mechanism to organization page-specific memory. The ExecutionWorld
// does all the work, but having all page-specific data structures
// grouped together helps keep things clean.
context: ?Context = null,
// no init, must be initialized via env.newExecutionWorld()
pub fn deinit(self: *ExecutionWorld) void {
if (self.context != null) {
self.removeContext();
}
self.context_arena.deinit();
}
// Only the top Context in the Main ExecutionWorld should hold a handle_scope.
// A js.HandleScope is like an arena. Once created, any "Local" that
// v8 creates will be released (or at least, releasable by the v8 GC)
// when the handle_scope is freed.
// We also maintain our own "context_arena" which allows us to have
// all page related memory easily managed.
pub fn createContext(self: *ExecutionWorld, page: *Page, enter: bool) !*Context {
lp.assert(self.context == null, "ExecptionWorld.createContext has context", .{});
const env = self.env;
const isolate = env.isolate;
const arena = self.context_arena.allocator();
var hs: js.HandleScope = undefined;
hs.init(isolate);
defer hs.deinit();
// Get the global template that was created once per isolate
const global_template: *const v8.ObjectTemplate = @ptrCast(@alignCast(v8.v8__Eternal__Get(&env.global_template, isolate.handle).?));
const v8_context = v8.v8__Context__New(isolate.handle, global_template, null).?;
// Create the v8::Context and wrap it in a v8::Global
var context_global: v8.Global = undefined;
v8.v8__Global__New(isolate.handle, v8_context, &context_global);
// our window wrapped in a v8::Global
const global_obj = v8.v8__Context__Global(v8_context).?;
var global_global: v8.Global = undefined;
v8.v8__Global__New(isolate.handle, global_obj, &global_global);
if (enter) {
v8.v8__Context__Enter(v8_context);
}
errdefer if (enter) {
v8.v8__Context__Exit(v8_context);
};
const context_id = env.context_id;
env.context_id = context_id + 1;
self.context = Context{
.page = page,
.id = context_id,
.entered = enter,
.isolate = isolate,
.handle = context_global,
.templates = env.templates,
.script_manager = &page._script_manager,
.call_arena = page.call_arena,
.arena = arena,
};
var context = &self.context.?;
try context.identity_map.putNoClobber(arena, @intFromPtr(page.window), global_global);
// Store a pointer to our context inside the v8 context so that, given
// a v8 context, we can get our context out
const data = isolate.initBigInt(@intFromPtr(&self.context.?));
v8.v8__Context__SetEmbedderData(v8_context, 1, @ptrCast(data.handle));
return &self.context.?;
}
pub fn removeContext(self: *ExecutionWorld) void {
var context = &(self.context orelse return);
context.deinit();
self.context = null;
self.env.isolate.notifyContextDisposed();
_ = self.context_arena.reset(.{ .retain_with_limit = CONTEXT_ARENA_RETAIN });
}

View File

@@ -24,7 +24,6 @@ const string = @import("../../string.zig");
pub const Env = @import("Env.zig"); pub const Env = @import("Env.zig");
pub const bridge = @import("bridge.zig"); pub const bridge = @import("bridge.zig");
pub const ExecutionWorld = @import("ExecutionWorld.zig");
pub const Caller = @import("Caller.zig"); pub const Caller = @import("Caller.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");

View File

@@ -467,15 +467,13 @@ 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 {
var executor = try self.cdp.browser.env.newExecutionWorld();
errdefer executor.deinit();
const owned_name = try self.arena.dupe(u8, world_name); const owned_name = try self.arena.dupe(u8, world_name);
const world = try self.isolated_worlds.addOne(self.arena); const world = try self.isolated_worlds.addOne(self.arena);
world.* = .{ world.* = .{
.context = null,
.name = owned_name, .name = owned_name,
.executor = executor, .env = &self.cdp.browser.env,
.grant_universal_access = grant_universal_access, .grant_universal_access = grant_universal_access,
}; };
@@ -746,15 +744,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 {
name: []const u8, name: []const u8,
executor: js.ExecutionWorld, env: *js.Env,
context: ?*js.Context = null,
grant_universal_access: bool, grant_universal_access: bool,
pub fn deinit(self: *IsolatedWorld) void { pub fn deinit(self: *IsolatedWorld) void {
self.executor.deinit(); if (self.context) |ctx| {
self.env.destroyContext(ctx);
self.context = null;
}
} }
pub fn removeContext(self: *IsolatedWorld) !void { pub fn removeContext(self: *IsolatedWorld) !void {
if (self.executor.context == null) return error.NoIsolatedContextToRemove; const ctx = self.context orelse return error.NoIsolatedContextToRemove;
self.executor.removeContext(); self.env.destroyContext(ctx);
self.context = null;
} }
// 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
@@ -762,19 +765,16 @@ const IsolatedWorld = struct {
// We just created the world and the page. The page's state lives in the session, but is update on navigation. // We just created the world and the page. The page's state lives in the session, but is update on navigation.
// This also means this pointer becomes invalid after removePage until a new page is created. // This also means this pointer becomes invalid after removePage until a new page is created.
// 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) !void { pub fn createContext(self: *IsolatedWorld, page: *Page) !*js.Context {
// if (self.executor.context != null) return error.Only1IsolatedContextSupported; if (self.context == null) {
if (self.executor.context != null) { self.context = try self.env.createContext(page, false);
} 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",
.info = "reuse existing context", .info = "reuse existing context",
}); });
return;
} }
_ = try self.executor.createContext( return self.context.?;
page,
false,
);
} }
}; };

View File

@@ -288,7 +288,7 @@ fn resolveNode(cmd: anytype) !void {
ls.?.deinit(); ls.?.deinit();
ls = null; ls = null;
const ctx = &(isolated_world.executor.context orelse return error.ContextNotFound); const ctx = (isolated_world.context orelse return error.ContextNotFound);
ls = undefined; ls = undefined;
ctx.localScope(&ls.?); ctx.localScope(&ls.?);
if (ls.?.local.debugContextId() == context_id) { if (ls.?.local.debugContextId() == context_id) {

View File

@@ -190,8 +190,7 @@ fn createIsolatedWorld(cmd: anytype) !void {
const world = try bc.createIsolatedWorld(params.worldName, params.grantUniveralAccess); const world = try bc.createIsolatedWorld(params.worldName, params.grantUniveralAccess);
const page = bc.session.currentPage() orelse return error.PageNotLoaded; const page = bc.session.currentPage() orelse return error.PageNotLoaded;
try world.createContext(page); const js_context = try world.createContext(page);
const js_context = &world.executor.context.?;
// Create the auxdata json for the contextCreated event // Create the auxdata json for the contextCreated event
// Calling contextCreated will assign a Id to the context and send the contextCreated event // Calling contextCreated will assign a Id to the context and send the contextCreated event
@@ -292,7 +291,7 @@ pub fn pageRemove(bc: anytype) !void {
pub fn pageCreated(bc: anytype, page: *Page) !void { pub fn pageCreated(bc: anytype, page: *Page) !void {
for (bc.isolated_worlds.items) |*isolated_world| { for (bc.isolated_worlds.items) |*isolated_world| {
try isolated_world.createContext(page); _ = try isolated_world.createContext(page);
} }
} }
@@ -375,7 +374,7 @@ pub fn pageNavigated(arena: Allocator, bc: anytype, event: *const Notification.P
// 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
var ls: js.Local.Scope = undefined; var ls: js.Local.Scope = undefined;
(isolated_world.executor.context orelse continue).localScope(&ls); (isolated_world.context orelse continue).localScope(&ls);
defer ls.deinit(); defer ls.deinit();
bc.inspector.contextCreated( bc.inspector.contextCreated(