Optimize memory usage

The two bigger changes here are:

1- The http_client has been moved from the Session to the Browser, allowing
   its connection pool to be re-used across multiple sessions

2- The browser now has a page_arena which is used for all page-level allocation
   and which can be re-used between pages (currently retains 1MB of memory).
   Previously, pages uses an arena that was tied to the lifetime of the page,
   thus it could not be re-used.

Using the Bench allocator for zig-js-runtime, allocated bytes went from
1347037879 to 834932438 (in a RUNS=1000 of puppeteer demo).

Various other changes to try to simplify the API and remove the possibility
of invalid states. For example, session.newPage() now includes the logic for
page.start() so that there should now never be a page that wasn't started.
This commit is contained in:
Karl Seguin
2025-03-12 12:05:42 +08:00
parent 43f42f9ca0
commit 3fe28d5441
6 changed files with 184 additions and 204 deletions

View File

@@ -62,31 +62,35 @@ pub const Browser = struct {
loop: *Loop, loop: *Loop,
session: ?*Session, session: ?*Session,
allocator: Allocator, allocator: Allocator,
http_client: HttpClient,
session_pool: SessionPool, session_pool: SessionPool,
page_arena: std.heap.ArenaAllocator,
const SessionPool = std.heap.MemoryPool(Session); const SessionPool = std.heap.MemoryPool(Session);
const uri = "about:blank";
pub fn init(allocator: Allocator, loop: *Loop) Browser { pub fn init(allocator: Allocator, loop: *Loop) Browser {
return .{ return .{
.loop = loop, .loop = loop,
.session = null, .session = null,
.allocator = allocator, .allocator = allocator,
.http_client = .{ .allocator = allocator },
.session_pool = SessionPool.init(allocator), .session_pool = SessionPool.init(allocator),
.page_arena = std.heap.ArenaAllocator.init(allocator),
}; };
} }
pub fn deinit(self: *Browser) void { pub fn deinit(self: *Browser) void {
self.closeSession(); self.closeSession();
self.http_client.deinit();
self.session_pool.deinit(); self.session_pool.deinit();
self.page_arena.deinit();
} }
pub fn newSession(self: *Browser, ctx: anytype) !*Session { pub fn newSession(self: *Browser, ctx: anytype) !*Session {
self.closeSession(); self.closeSession();
const session = try self.session_pool.create(); const session = try self.session_pool.create();
try Session.init(session, self.allocator, ctx, self.loop, uri); try Session.init(session, self, ctx);
self.session = session; self.session = session;
return session; return session;
} }
@@ -105,8 +109,7 @@ pub const Browser = struct {
// You can create successively multiple pages for a session, but you must // You can create successively multiple pages for a session, but you must
// deinit a page before running another one. // deinit a page before running another one.
pub const Session = struct { pub const Session = struct {
// allocator used to init the arena. browser: *Browser,
allocator: Allocator,
// The arena is used only to bound the js env init b/c it leaks memory. // The arena is used only to bound the js env init b/c it leaks memory.
// see https://github.com/lightpanda-io/jsruntime-lib/issues/181 // see https://github.com/lightpanda-io/jsruntime-lib/issues/181
@@ -115,41 +118,34 @@ pub const Session = struct {
// all others Session deps use directly self.alloc and not the arena. // all others Session deps use directly self.alloc and not the arena.
arena: std.heap.ArenaAllocator, arena: std.heap.ArenaAllocator,
uri: []const u8,
// TODO handle proxy // TODO handle proxy
loader: Loader, loader: Loader,
env: Env, env: Env,
loop: *Loop,
inspector: jsruntime.Inspector, inspector: jsruntime.Inspector,
window: Window, window: Window,
// TODO move the shed to the browser? // TODO move the shed to the browser?
storageShed: storage.Shed, storage_shed: storage.Shed,
page: ?Page = null, page: ?Page = null,
httpClient: HttpClient,
jstypes: [Types.len]usize = undefined, jstypes: [Types.len]usize = undefined,
fn init(self: *Session, allocator: Allocator, ctx: anytype, loop: *Loop, uri: []const u8) !void { fn init(self: *Session, browser: *Browser, ctx: anytype) !void {
const allocator = browser.allocator;
self.* = .{ self.* = .{
.uri = uri,
.env = undefined, .env = undefined,
.browser = browser,
.inspector = undefined, .inspector = undefined,
.allocator = allocator,
.loader = Loader.init(allocator), .loader = Loader.init(allocator),
.httpClient = .{ .allocator = allocator }, .storage_shed = storage.Shed.init(allocator),
.storageShed = storage.Shed.init(allocator),
.arena = std.heap.ArenaAllocator.init(allocator), .arena = std.heap.ArenaAllocator.init(allocator),
.window = Window.create(null, .{ .agent = user_agent }), .window = Window.create(null, .{ .agent = user_agent }),
.loop = loop,
}; };
const arena = self.arena.allocator(); const arena = self.arena.allocator();
Env.init(&self.env, arena, browser.loop, null);
Env.init(&self.env, arena, loop, null);
errdefer self.env.deinit(); errdefer self.env.deinit();
try self.env.load(&self.jstypes); try self.env.load(&self.jstypes);
@@ -164,24 +160,24 @@ pub const Session = struct {
// const ctx_opaque = @as(*anyopaque, @ptrCast(ctx)); // const ctx_opaque = @as(*anyopaque, @ptrCast(ctx));
self.inspector = try jsruntime.Inspector.init( self.inspector = try jsruntime.Inspector.init(
arena, arena,
self.env, self.env, // TODO: change to 'env' when https://github.com/lightpanda-io/zig-js-runtime/pull/285 lands
if (@TypeOf(ctx) == void) @constCast(@ptrCast(&{})) else ctx, if (@TypeOf(ctx) == void) @constCast(@ptrCast(&{})) else ctx,
InspectorContainer.onInspectorResponse, InspectorContainer.onInspectorResponse,
InspectorContainer.onInspectorEvent, InspectorContainer.onInspectorEvent,
); );
self.env.setInspector(self.inspector); self.env.setInspector(self.inspector);
try self.env.setModuleLoadFn(self, Session.fetchModule);
} }
fn deinit(self: *Session) void { fn deinit(self: *Session) void {
if (self.page) |*p| { if (self.page != null) {
p.deinit(); self.removePage();
} }
self.env.deinit(); self.env.deinit();
self.arena.deinit(); self.arena.deinit();
self.httpClient.deinit();
self.loader.deinit(); self.loader.deinit();
self.storageShed.deinit(); self.storage_shed.deinit();
} }
fn fetchModule(ctx: *anyopaque, referrer: ?jsruntime.Module, specifier: []const u8) !jsruntime.Module { fn fetchModule(ctx: *anyopaque, referrer: ?jsruntime.Module, specifier: []const u8) !jsruntime.Module {
@@ -189,13 +185,16 @@ pub const Session = struct {
const self: *Session = @ptrCast(@alignCast(ctx)); const self: *Session = @ptrCast(@alignCast(ctx));
if (self.page == null) return error.NoPage; if (self.page == null) {
return error.NoPage;
}
log.debug("fetch module: specifier: {s}", .{specifier}); log.debug("fetch module: specifier: {s}", .{specifier});
const alloc = self.arena.allocator(); // fetchModule is called within the context of processing a page.
const body = try self.page.?.fetchData(alloc, specifier); // Use the page_arena for this, which has a more appropriate lifetime
defer alloc.free(body); // and which has more retained memory between sessions and pages.
const arena = self.browser.page_arena.allocator();
const body = try self.page.?.fetchData(arena, specifier);
return self.env.compileModule(body, specifier); return self.env.compileModule(body, specifier);
} }
@@ -205,15 +204,62 @@ pub const Session = struct {
// NOTE: the caller is not the owner of the returned value, // NOTE: the caller is not the owner of the returned value,
// the pointer on Page is just returned as a convenience // the pointer on Page is just returned as a convenience
pub fn createPage(self: *Session) !*Page { pub fn createPage(self: *Session, aux_data: ?[]const u8) !*Page {
if (self.page != null) return error.SessionPageExists; std.debug.assert(self.page == null);
self.page = Page.init(self.allocator, self);
return &self.page.?; _ = self.browser.page_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 });
self.page = Page.init(self);
const page = &self.page.?;
// start JS env
log.debug("start js env", .{});
try self.env.start();
if (comptime builtin.is_test == false) {
// By not loading this during tests, we aren't required to load
// all of the interfaces into zig-js-runtime.
log.debug("setup global env", .{});
try self.env.bindGlobal(&self.window);
}
// load polyfills
// TODO: change to 'env' when https://github.com/lightpanda-io/zig-js-runtime/pull/285 lands
try polyfill.load(self.arena.allocator(), self.env);
// inspector
self.contextCreated(page, aux_data);
return page;
}
pub fn removePage(self: *Session) void {
std.debug.assert(self.page != null);
// Reset all existing callbacks.
self.browser.loop.reset();
self.env.stop();
// TODO unload document: https://html.spec.whatwg.org/#unloading-documents
self.window.replaceLocation(null) catch |e| {
log.err("reset window location: {any}", .{e});
};
// clear netsurf memory arena.
parser.deinit();
self.page = null;
} }
pub fn currentPage(self: *Session) ?*Page { pub fn currentPage(self: *Session) ?*Page {
return &(self.page orelse return null); return &(self.page orelse return null);
} }
fn contextCreated(self: *Session, page: *Page, aux_data: ?[]const u8) void {
log.debug("inspector context created", .{});
self.inspector.contextCreated(self.env, "", page.origin orelse "://", aux_data);
}
}; };
// Page navigates to an url. // Page navigates to an url.
@@ -222,8 +268,8 @@ pub const Session = struct {
// The page handle all its memory in an arena allocator. The arena is reseted // The page handle all its memory in an arena allocator. The arena is reseted
// when end() is called. // when end() is called.
pub const Page = struct { pub const Page = struct {
arena: Allocator,
session: *Session, session: *Session,
arena: std.heap.ArenaAllocator,
doc: ?*parser.Document = null, doc: ?*parser.Document = null,
// handle url // handle url
@@ -237,71 +283,15 @@ pub const Page = struct {
raw_data: ?[]const u8 = null, raw_data: ?[]const u8 = null,
fn init(allocator: Allocator, session: *Session) Page { fn init(session: *Session) Page {
return .{ return .{
.session = session, .session = session,
.arena = std.heap.ArenaAllocator.init(allocator), .arena = session.browser.page_arena.allocator(),
}; };
} }
pub fn deinit(self: *Page) void {
self.end();
self.arena.deinit();
self.session.page = null;
}
// start js env.
// - auxData: extra data forwarded to the Inspector
// see Inspector.contextCreated
pub fn start(self: *Page, auxData: ?[]const u8) !void {
// start JS env
log.debug("start js env", .{});
try self.session.env.start();
// register the module loader
try self.session.env.setModuleLoadFn(self.session, Session.fetchModule);
// add global objects
log.debug("setup global env", .{});
if (comptime builtin.is_test == false) {
// By not loading this during tests, we aren't required to load
// all of the interfaces into zig-js-runtime.
try self.session.env.bindGlobal(&self.session.window);
}
// load polyfills
try polyfill.load(self.arena.allocator(), self.session.env);
// inspector
log.debug("inspector context created", .{});
self.session.inspector.contextCreated(self.session.env, "", self.origin orelse "://", auxData);
}
// reset js env and mem arena.
pub fn end(self: *Page) void {
// Reset all existing callbacks.
self.session.loop.reset();
self.session.env.stop();
// TODO unload document: https://html.spec.whatwg.org/#unloading-documents
self.url = null;
self.location.url = null;
self.session.window.replaceLocation(&self.location) catch |e| {
log.err("reset window location: {any}", .{e});
};
self.doc = null;
// clear netsurf memory arena.
parser.deinit();
_ = self.arena.reset(.free_all);
}
// dump writes the page content into the given file. // dump writes the page content into the given file.
pub fn dump(self: *const Page, out: std.fs.File) !void { pub fn dump(self: *const Page, out: std.fs.File) !void {
// if no HTML document pointer available, dump the data content only. // if no HTML document pointer available, dump the data content only.
if (self.doc == null) { if (self.doc == null) {
// no data loaded, nothing to do. // no data loaded, nothing to do.
@@ -314,7 +304,6 @@ pub const Page = struct {
} }
pub fn wait(self: *Page) !void { pub fn wait(self: *Page) !void {
// try catch // try catch
var try_catch: jsruntime.TryCatch = undefined; var try_catch: jsruntime.TryCatch = undefined;
try_catch.init(self.session.env); try_catch.init(self.session.env);
@@ -324,9 +313,9 @@ pub const Page = struct {
// the js env could not be started if the document wasn't an HTML. // the js env could not be started if the document wasn't an HTML.
if (err == error.EnvNotStarted) return; if (err == error.EnvNotStarted) return;
const alloc = self.arena.allocator(); const arena = self.arena;
if (try try_catch.err(alloc, self.session.env)) |msg| { if (try try_catch.err(arena, self.session.env)) |msg| {
defer alloc.free(msg); defer arena.free(msg);
log.info("wait error: {s}", .{msg}); log.info("wait error: {s}", .{msg});
return; return;
} }
@@ -335,10 +324,10 @@ pub const Page = struct {
} }
// spec reference: https://html.spec.whatwg.org/#document-lifecycle // spec reference: https://html.spec.whatwg.org/#document-lifecycle
// - auxData: extra data forwarded to the Inspector // - aux_data: extra data forwarded to the Inspector
// see Inspector.contextCreated // see Inspector.contextCreated
pub fn navigate(self: *Page, uri: []const u8, auxData: ?[]const u8) !void { pub fn navigate(self: *Page, uri: []const u8, aux_data: ?[]const u8) !void {
const alloc = self.arena.allocator(); const arena = self.arena;
log.debug("starting GET {s}", .{uri}); log.debug("starting GET {s}", .{uri});
@@ -348,26 +337,25 @@ pub const Page = struct {
} }
// own the url // own the url
self.rawuri = try alloc.dupe(u8, uri); self.rawuri = try arena.dupe(u8, uri);
self.uri = std.Uri.parse(self.rawuri.?) catch try std.Uri.parseAfterScheme("", self.rawuri.?); self.uri = std.Uri.parse(self.rawuri.?) catch try std.Uri.parseAfterScheme("", self.rawuri.?);
self.url = try URL.constructor(alloc, self.rawuri.?, null); self.url = try URL.constructor(arena, self.rawuri.?, null);
self.location.url = &self.url.?; self.location.url = &self.url.?;
try self.session.window.replaceLocation(&self.location); try self.session.window.replaceLocation(&self.location);
// prepare origin value. // prepare origin value.
var buf = std.ArrayList(u8).init(alloc); var buf: std.ArrayListUnmanaged(u8) = .{};
defer buf.deinit();
try self.uri.writeToStream(.{ try self.uri.writeToStream(.{
.scheme = true, .scheme = true,
.authority = true, .authority = true,
}, buf.writer()); }, buf.writer(arena));
self.origin = try buf.toOwnedSlice(); self.origin = buf.items;
// TODO handle fragment in url. // TODO handle fragment in url.
// load the data // load the data
var resp = try self.session.loader.get(alloc, self.uri); var resp = try self.session.loader.get(arena, self.uri);
defer resp.deinit(); defer resp.deinit();
const req = resp.req; const req = resp.req;
@@ -385,46 +373,43 @@ pub const Page = struct {
// TODO handle charset // TODO handle charset
// https://html.spec.whatwg.org/#content-type // https://html.spec.whatwg.org/#content-type
var it = req.response.iterateHeaders(); var it = req.response.iterateHeaders();
var ct: ?[]const u8 = null; var ct_: ?[]const u8 = null;
while (true) { while (true) {
const h = it.next() orelse break; const h = it.next() orelse break;
if (std.ascii.eqlIgnoreCase(h.name, "Content-Type")) { if (std.ascii.eqlIgnoreCase(h.name, "Content-Type")) {
ct = try alloc.dupe(u8, h.value); ct_ = try arena.dupe(u8, h.value);
} }
} }
if (ct == null) { const ct = ct_ orelse {
// no content type in HTTP headers. // no content type in HTTP headers.
// TODO try to sniff mime type from the body. // TODO try to sniff mime type from the body.
log.info("no content-type HTTP header", .{}); log.info("no content-type HTTP header", .{});
return; return;
} };
defer alloc.free(ct.?);
log.debug("header content-type: {s}", .{ct.?}); log.debug("header content-type: {s}", .{ct});
var mime = try Mime.parse(alloc, ct.?); var mime = try Mime.parse(arena, ct);
defer mime.deinit();
if (mime.isHTML()) { if (mime.isHTML()) {
try self.loadHTMLDoc(req.reader(), mime.charset orelse "utf-8", auxData); try self.loadHTMLDoc(req.reader(), mime.charset orelse "utf-8", aux_data);
} else { } else {
log.info("non-HTML document: {s}", .{ct.?}); log.info("non-HTML document: {s}", .{ct});
// save the body into the page. // save the body into the page.
self.raw_data = try req.reader().readAllAlloc(alloc, 16 * 1024 * 1024); self.raw_data = try req.reader().readAllAlloc(arena, 16 * 1024 * 1024);
} }
} }
// https://html.spec.whatwg.org/#read-html // https://html.spec.whatwg.org/#read-html
fn loadHTMLDoc(self: *Page, reader: anytype, charset: []const u8, auxData: ?[]const u8) !void { fn loadHTMLDoc(self: *Page, reader: anytype, charset: []const u8, aux_data: ?[]const u8) !void {
const alloc = self.arena.allocator(); const arena = self.arena;
// start netsurf memory arena. // start netsurf memory arena.
try parser.init(); try parser.init();
log.debug("parse html with charset {s}", .{charset}); log.debug("parse html with charset {s}", .{charset});
const ccharset = try alloc.dupeZ(u8, charset); const ccharset = try arena.dupeZ(u8, charset);
defer alloc.free(ccharset);
const html_doc = try parser.documentHTMLParse(reader, ccharset); const html_doc = try parser.documentHTMLParse(reader, ccharset);
const doc = parser.documentHTMLToDocument(html_doc); const doc = parser.documentHTMLToDocument(html_doc);
@@ -438,22 +423,22 @@ pub const Page = struct {
// inject the URL to the document including the fragment. // inject the URL to the document including the fragment.
try parser.documentSetDocumentURI(doc, self.rawuri orelse "about:blank"); try parser.documentSetDocumentURI(doc, self.rawuri orelse "about:blank");
const session = self.session;
// TODO set the referrer to the document. // TODO set the referrer to the document.
try session.window.replaceDocument(html_doc);
try self.session.window.replaceDocument(html_doc); session.window.setStorageShelf(
self.session.window.setStorageShelf( try session.storage_shed.getOrPut(self.origin orelse "null"),
try self.session.storageShed.getOrPut(self.origin orelse "null"),
); );
// https://html.spec.whatwg.org/#read-html // https://html.spec.whatwg.org/#read-html
// inspector // inspector
self.session.inspector.contextCreated(self.session.env, "", self.origin.?, auxData); session.contextCreated(self, aux_data);
// replace the user context document with the new one. // replace the user context document with the new one.
try self.session.env.setUserContext(.{ try session.env.setUserContext(.{
.document = html_doc, .document = html_doc,
.httpClient = &self.session.httpClient, .httpClient = &self.session.browser.http_client,
}); });
// browse the DOM tree to retrieve scripts // browse the DOM tree to retrieve scripts
@@ -464,8 +449,7 @@ pub const Page = struct {
// sasync stores scripts which can be run asynchronously. // sasync stores scripts which can be run asynchronously.
// for now they are just run after the non-async one in order to // for now they are just run after the non-async one in order to
// dispatch DOMContentLoaded the sooner as possible. // dispatch DOMContentLoaded the sooner as possible.
var sasync = std.ArrayList(Script).init(alloc); var sasync: std.ArrayListUnmanaged(Script) = .{};
defer sasync.deinit();
const root = parser.documentToNode(doc); const root = parser.documentToNode(doc);
const walker = Walker{}; const walker = Walker{};
@@ -496,8 +480,8 @@ pub const Page = struct {
// > then the classic script will be fetched in parallel to // > then the classic script will be fetched in parallel to
// > parsing and evaluated as soon as it is available. // > parsing and evaluated as soon as it is available.
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#async // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#async
if (script.isasync) { if (script.is_async) {
try sasync.append(script); try sasync.append(arena, script);
continue; continue;
} }
@@ -562,8 +546,6 @@ pub const Page = struct {
// if no src is present, we evaluate the text source. // if no src is present, we evaluate the text source.
// https://html.spec.whatwg.org/multipage/scripting.html#script-processing-model // https://html.spec.whatwg.org/multipage/scripting.html#script-processing-model
fn evalScript(self: *Page, s: Script) !void { fn evalScript(self: *Page, s: Script) !void {
const alloc = self.arena.allocator();
// https://html.spec.whatwg.org/multipage/webappapis.html#fetch-a-classic-script // https://html.spec.whatwg.org/multipage/webappapis.html#fetch-a-classic-script
const opt_src = try parser.elementGetAttribute(s.element, "src"); const opt_src = try parser.elementGetAttribute(s.element, "src");
if (opt_src) |src| { if (opt_src) |src| {
@@ -591,7 +573,9 @@ pub const Page = struct {
// TODO handle charset attribute // TODO handle charset attribute
const opt_text = try parser.nodeTextContent(parser.elementToNode(s.element)); const opt_text = try parser.nodeTextContent(parser.elementToNode(s.element));
if (opt_text) |text| { if (opt_text) |text| {
try s.eval(alloc, self.session.env, text); // TODO: change to &self.session.env when
// https://github.com/lightpanda-io/zig-js-runtime/pull/285 lands
try s.eval(self.arena, self.session.env, text);
return; return;
} }
@@ -607,27 +591,31 @@ pub const Page = struct {
}; };
// the caller owns the returned string // the caller owns the returned string
fn fetchData(self: *Page, alloc: Allocator, src: []const u8) ![]const u8 { fn fetchData(self: *Page, arena: Allocator, src: []const u8) ![]const u8 {
log.debug("starting fetch {s}", .{src}); log.debug("starting fetch {s}", .{src});
var buffer: [1024]u8 = undefined; var buffer: [1024]u8 = undefined;
var b: []u8 = buffer[0..]; var b: []u8 = buffer[0..];
const u = try std.Uri.resolve_inplace(self.uri, src, &b); const u = try std.Uri.resolve_inplace(self.uri, src, &b);
var fetchres = try self.session.loader.get(alloc, u); var fetchres = try self.session.loader.get(arena, u);
defer fetchres.deinit(); defer fetchres.deinit();
const resp = fetchres.req.response; const resp = fetchres.req.response;
log.info("fetch {any}: {d}", .{ u, resp.status }); log.info("fetch {any}: {d}", .{ u, resp.status });
if (resp.status != .ok) return FetchError.BadStatusCode; if (resp.status != .ok) {
return FetchError.BadStatusCode;
}
// TODO check content-type // TODO check content-type
const body = try fetchres.req.reader().readAllAlloc(alloc, 16 * 1024 * 1024); const body = try fetchres.req.reader().readAllAlloc(arena, 16 * 1024 * 1024);
// check no body // check no body
if (body.len == 0) return FetchError.NoBody; if (body.len == 0) {
return FetchError.NoBody;
}
return body; return body;
} }
@@ -635,17 +623,17 @@ pub const Page = struct {
// fetchScript senf a GET request to the src and execute the script // fetchScript senf a GET request to the src and execute the script
// received. // received.
fn fetchScript(self: *Page, s: Script) !void { fn fetchScript(self: *Page, s: Script) !void {
const alloc = self.arena.allocator(); const arena = self.arena;
const body = try self.fetchData(alloc, s.src); const body = try self.fetchData(arena, s.src);
defer alloc.free(body); // TODO: change to &self.session.env when
// https://github.com/lightpanda-io/zig-js-runtime/pull/285 lands
try s.eval(alloc, self.session.env, body); try s.eval(arena, self.session.env, body);
} }
const Script = struct { const Script = struct {
element: *parser.Element, element: *parser.Element,
kind: Kind, kind: Kind,
isasync: bool, is_async: bool,
src: []const u8, src: []const u8,
@@ -663,7 +651,7 @@ pub const Page = struct {
return .{ return .{
.element = e, .element = e,
.kind = kind(try parser.elementGetAttribute(e, "type")), .kind = kind(try parser.elementGetAttribute(e, "type")),
.isasync = try parser.elementGetAttribute(e, "async") != null, .is_async = try parser.elementGetAttribute(e, "async") != null,
.src = try parser.elementGetAttribute(e, "src") orelse "inline", .src = try parser.elementGetAttribute(e, "src") orelse "inline",
}; };
@@ -682,7 +670,7 @@ pub const Page = struct {
return .unknown; return .unknown;
} }
fn eval(self: Script, alloc: Allocator, env: Env, body: []const u8) !void { fn eval(self: Script, arena: Allocator, env: Env, body: []const u8) !void {
var try_catch: jsruntime.TryCatch = undefined; var try_catch: jsruntime.TryCatch = undefined;
try_catch.init(env); try_catch.init(env);
defer try_catch.deinit(); defer try_catch.deinit();
@@ -692,16 +680,14 @@ pub const Page = struct {
.javascript => env.exec(body, self.src), .javascript => env.exec(body, self.src),
.module => env.module(body, self.src), .module => env.module(body, self.src),
} catch { } catch {
if (try try_catch.err(alloc, env)) |msg| { if (try try_catch.err(arena, env)) |msg| {
defer alloc.free(msg);
log.info("eval script {s}: {s}", .{ self.src, msg }); log.info("eval script {s}: {s}", .{ self.src, msg });
} }
return FetchError.JsErr; return FetchError.JsErr;
}; };
if (builtin.mode == .Debug) { if (builtin.mode == .Debug) {
const msg = try res.toString(alloc, env); const msg = try res.toString(arena, env);
defer alloc.free(msg);
log.debug("eval script {s}: {s}", .{ self.src, msg }); log.debug("eval script {s}: {s}", .{ self.src, msg });
} }
} }

View File

@@ -55,13 +55,15 @@ pub fn CDPT(comptime TypeProvider: type) type {
allocator: Allocator, allocator: Allocator,
// The active browser // The active browser
browser: ?Browser = null, browser: Browser,
target_id_gen: TargetIdGen = .{}, target_id_gen: TargetIdGen = .{},
session_id_gen: SessionIdGen = .{}, session_id_gen: SessionIdGen = .{},
browser_context_id_gen: BrowserContextIdGen = .{}, browser_context_id_gen: BrowserContextIdGen = .{},
browser_context: ?BrowserContext(Self), browser_context: ?*BrowserContext(Self),
browser_context_pool: std.heap.MemoryPool(BrowserContext(Self)),
// Re-used arena for processing a message. We're assuming that we're getting // Re-used arena for processing a message. We're assuming that we're getting
// 1 message at a time. // 1 message at a time.
@@ -77,15 +79,19 @@ pub fn CDPT(comptime TypeProvider: type) type {
.client = client, .client = client,
.allocator = allocator, .allocator = allocator,
.browser_context = null, .browser_context = null,
.browser = Browser.init(allocator, loop),
.message_arena = std.heap.ArenaAllocator.init(allocator), .message_arena = std.heap.ArenaAllocator.init(allocator),
.browser_context_pool = std.heap.MemoryPool(BrowserContext(Self)).init(allocator),
}; };
} }
pub fn deinit(self: *Self) void { pub fn deinit(self: *Self) void {
if (self.browser_context) |*bc| { if (self.browser_context) |bc| {
bc.deinit(); bc.deinit();
} }
self.browser.deinit();
self.message_arena.deinit(); self.message_arena.deinit();
self.browser_context_pool.deinit();
} }
pub fn handleMessage(self: *Self, msg: []const u8) bool { pub fn handleMessage(self: *Self, msg: []const u8) bool {
@@ -119,7 +125,7 @@ pub fn CDPT(comptime TypeProvider: type) type {
.cdp = self, .cdp = self,
.arena = arena, .arena = arena,
.sender = sender, .sender = sender,
.browser_context = if (self.browser_context) |*bc| bc else null, .browser_context = if (self.browser_context) |bc| bc else null,
}; };
// See dispatchStartupCommand for more info on this. // See dispatchStartupCommand for more info on this.
@@ -213,7 +219,7 @@ pub fn CDPT(comptime TypeProvider: type) type {
} }
fn isValidSessionId(self: *const Self, input_session_id: []const u8) bool { fn isValidSessionId(self: *const Self, input_session_id: []const u8) bool {
const browser_context = &(self.browser_context orelse return false); const browser_context = self.browser_context orelse return false;
const session_id = browser_context.session_id orelse return false; const session_id = browser_context.session_id orelse return false;
return std.mem.eql(u8, session_id, input_session_id); return std.mem.eql(u8, session_id, input_session_id);
} }
@@ -224,20 +230,21 @@ pub fn CDPT(comptime TypeProvider: type) type {
} }
const browser_context_id = self.browser_context_id_gen.next(); const browser_context_id = self.browser_context_id_gen.next();
// is this safe? const browser_context = try self.browser_context_pool.create();
self.browser_context = undefined; errdefer self.browser_context_pool.destroy(browser_context);
errdefer self.browser_context = null;
try BrowserContext(Self).init(&self.browser_context.?, browser_context_id, self);
try BrowserContext(Self).init(browser_context, browser_context_id, self);
self.browser_context = browser_context;
return browser_context_id; return browser_context_id;
} }
pub fn disposeBrowserContext(self: *Self, browser_context_id: []const u8) bool { pub fn disposeBrowserContext(self: *Self, browser_context_id: []const u8) bool {
const bc = &(self.browser_context orelse return false); const bc = self.browser_context orelse return false;
if (std.mem.eql(u8, bc.id, browser_context_id) == false) { if (std.mem.eql(u8, bc.id, browser_context_id) == false) {
return false; return false;
} }
bc.deinit(); bc.deinit();
self.browser_context_pool.destroy(bc);
self.browser_context = null; self.browser_context = null;
return true; return true;
} }
@@ -257,7 +264,6 @@ pub fn BrowserContext(comptime CDP_T: type) type {
id: []const u8, id: []const u8,
cdp: *CDP_T, cdp: *CDP_T,
browser: CDP_T.Browser,
// Represents the browser session. There is no equivalent in CDP. For // Represents the browser session. There is no equivalent in CDP. For
// all intents and purpose, from CDP's point of view our Browser and // all intents and purpose, from CDP's point of view our Browser and
// our Session more or less maps to a BrowserContext. THIS HAS ZERO // our Session more or less maps to a BrowserContext. THIS HAS ZERO
@@ -294,22 +300,17 @@ pub fn BrowserContext(comptime CDP_T: type) type {
self.* = .{ self.* = .{
.id = id, .id = id,
.cdp = cdp, .cdp = cdp,
.browser = undefined,
.session = undefined,
.target_id = null, .target_id = null,
.session_id = null, .session_id = null,
.url = URL_BASE, .url = URL_BASE,
.security_origin = URL_BASE, .security_origin = URL_BASE,
.secure_context_type = "Secure", // TODO = enum .secure_context_type = "Secure", // TODO = enum
.loader_id = LOADER_ID, .loader_id = LOADER_ID,
.session = try cdp.browser.newSession(self),
.page_life_cycle_events = false, // TODO; Target based value .page_life_cycle_events = false, // TODO; Target based value
.node_list = dom.NodeList.init(cdp.allocator), .node_list = dom.NodeList.init(cdp.allocator),
.node_search_list = dom.NodeSearchList.init(cdp.allocator), .node_search_list = dom.NodeSearchList.init(cdp.allocator),
}; };
self.browser = CDP_T.Browser.init(cdp.allocator, cdp.loop);
errdefer self.browser.deinit();
self.session = try self.browser.newSession(self);
} }
pub fn deinit(self: *Self) void { pub fn deinit(self: *Self) void {
@@ -318,7 +319,6 @@ pub fn BrowserContext(comptime CDP_T: type) type {
s.deinit(); s.deinit();
} }
self.node_search_list.deinit(); self.node_search_list.deinit();
self.browser.deinit();
} }
pub fn reset(self: *Self) void { pub fn reset(self: *Self) void {
@@ -447,7 +447,7 @@ pub fn Command(comptime CDP_T: type, comptime Sender: type) type {
pub fn createBrowserContext(self: *Self) !*BrowserContext(CDP_T) { pub fn createBrowserContext(self: *Self) !*BrowserContext(CDP_T) {
_ = try self.cdp.createBrowserContext(); _ = try self.cdp.createBrowserContext();
self.browser_context = &self.cdp.browser_context.?; self.browser_context = self.cdp.browser_context.?;
return self.browser_context.?; return self.browser_context.?;
} }

View File

@@ -116,15 +116,8 @@ fn createTarget(cmd: anytype) !void {
// if target_id is null, we should never have a session_id // if target_id is null, we should never have a session_id
std.debug.assert(bc.session_id == null); std.debug.assert(bc.session_id == null);
const page = try bc.session.createPage();
const target_id = cmd.cdp.target_id_gen.next(); const target_id = cmd.cdp.target_id_gen.next();
// change CDP state
bc.url = "about:blank";
bc.security_origin = "://";
bc.secure_context_type = "InsecureScheme";
bc.loader_id = LOADER_ID;
// start the js env // start the js env
const aux_data = try std.fmt.allocPrint( const aux_data = try std.fmt.allocPrint(
cmd.arena, cmd.arena,
@@ -132,7 +125,13 @@ fn createTarget(cmd: anytype) !void {
"{{\"isDefault\":true,\"type\":\"default\",\"frameId\":\"{s}\"}}", "{{\"isDefault\":true,\"type\":\"default\",\"frameId\":\"{s}\"}}",
.{target_id}, .{target_id},
); );
try page.start(aux_data); _ = try bc.session.createPage(aux_data);
// change CDP state
bc.url = "about:blank";
bc.security_origin = "://";
bc.secure_context_type = "InsecureScheme";
bc.loader_id = LOADER_ID;
// send targetCreated event // send targetCreated event
// TODO: should this only be sent when Target.setDiscoverTargets // TODO: should this only be sent when Target.setDiscoverTargets
@@ -213,7 +212,7 @@ fn closeTarget(cmd: anytype) !void {
bc.session_id = null; bc.session_id = null;
} }
bc.session.currentPage().?.end(); bc.session.removePage();
bc.target_id = null; bc.target_id = null;
} }
@@ -508,7 +507,7 @@ test "cdp.target: closeTarget" {
} }
// pretend we createdTarget first // pretend we createdTarget first
_ = try bc.session.createPage(); _ = try bc.session.createPage(null);
bc.target_id = "TID-A"; bc.target_id = "TID-A";
{ {
try testing.expectError(error.UnknownTargetId, ctx.processMessage(.{ .id = 10, .method = "Target.closeTarget", .params = .{ .targetId = "TID-8" } })); try testing.expectError(error.UnknownTargetId, ctx.processMessage(.{ .id = 10, .method = "Target.closeTarget", .params = .{ .targetId = "TID-8" } }));
@@ -539,7 +538,7 @@ test "cdp.target: attachToTarget" {
} }
// pretend we createdTarget first // pretend we createdTarget first
_ = try bc.session.createPage(); _ = try bc.session.createPage(null);
bc.target_id = "TID-B"; bc.target_id = "TID-B";
{ {
try testing.expectError(error.UnknownTargetId, ctx.processMessage(.{ .id = 10, .method = "Target.attachToTarget", .params = .{ .targetId = "TID-8" } })); try testing.expectError(error.UnknownTargetId, ctx.processMessage(.{ .id = 10, .method = "Target.attachToTarget", .params = .{ .targetId = "TID-8" } }));
@@ -583,7 +582,7 @@ test "cdp.target: getTargetInfo" {
} }
// pretend we createdTarget first // pretend we createdTarget first
_ = try bc.session.createPage(); _ = try bc.session.createPage(null);
bc.target_id = "TID-A"; bc.target_id = "TID-A";
{ {
try testing.expectError(error.UnknownTargetId, ctx.processMessage(.{ .id = 10, .method = "Target.getTargetInfo", .params = .{ .targetId = "TID-8" } })); try testing.expectError(error.UnknownTargetId, ctx.processMessage(.{ .id = 10, .method = "Target.getTargetInfo", .params = .{ .targetId = "TID-8" } }));

View File

@@ -56,17 +56,21 @@ const Session = struct {
return &(self.page orelse return null); return &(self.page orelse return null);
} }
pub fn createPage(self: *Session) !*Page { pub fn createPage(self: *Session, aux_data: ?[]const u8) !*Page {
if (self.page != null) { if (self.page != null) {
return error.MockBrowserPageAlreadyExists; return error.MockBrowserPageAlreadyExists;
} }
self.page = .{ self.page = .{
.session = self, .session = self,
.allocator = self.allocator, .aux_data = try self.allocator.dupe(u8, aux_data orelse ""),
}; };
return &self.page.?; return &self.page.?;
} }
pub fn removePage(self: *Session) void {
self.page = null;
}
pub fn callInspector(self: *Session, msg: []const u8) void { pub fn callInspector(self: *Session, msg: []const u8) void {
_ = self; _ = self;
_ = msg; _ = msg;
@@ -75,7 +79,6 @@ const Session = struct {
const Page = struct { const Page = struct {
session: *Session, session: *Session,
allocator: Allocator,
aux_data: []const u8 = "", aux_data: []const u8 = "",
doc: ?*parser.Document = null, doc: ?*parser.Document = null,
@@ -84,14 +87,6 @@ const Page = struct {
_ = url; _ = url;
_ = aux_data; _ = aux_data;
} }
pub fn start(self: *Page, aux_data: []const u8) !void {
self.aux_data = try self.allocator.dupe(u8, aux_data);
}
pub fn end(self: *Page) void {
self.session.page = null;
}
}; };
const Client = struct { const Client = struct {
@@ -152,13 +147,15 @@ const TestContext = struct {
}; };
pub fn loadBrowserContext(self: *TestContext, opts: BrowserContextOpts) !*main.BrowserContext(TestCDP) { pub fn loadBrowserContext(self: *TestContext, opts: BrowserContextOpts) !*main.BrowserContext(TestCDP) {
var c = self.cdp(); var c = self.cdp();
if (c.browser_context) |*bc| { c.browser.session = null;
if (c.browser_context) |bc| {
bc.deinit(); bc.deinit();
c.browser_context = null; c.browser_context = null;
} }
_ = try c.createBrowserContext(); _ = try c.createBrowserContext();
var bc = &c.browser_context.?; var bc = c.browser_context.?;
if (opts.id) |id| { if (opts.id) |id| {
bc.id = id; bc.id = id;

View File

@@ -58,17 +58,17 @@ pub const Window = struct {
navigator: Navigator, navigator: Navigator,
pub fn create(target: ?[]const u8, navigator: ?Navigator) Window { pub fn create(target: ?[]const u8, navigator: ?Navigator) Window {
return Window{ return .{
.target = target orelse "", .target = target orelse "",
.navigator = navigator orelse .{}, .navigator = navigator orelse .{},
}; };
} }
pub fn replaceLocation(self: *Window, loc: *Location) !void { pub fn replaceLocation(self: *Window, loc: ?*Location) !void {
self.location = loc; self.location = loc orelse &emptyLocation;
if (self.document != null) { if (self.document) |doc| {
try parser.documentHTMLSetLocation(Location, self.document.?, self.location); try parser.documentHTMLSetLocation(Location, doc, self.location);
} }
} }

View File

@@ -96,9 +96,7 @@ pub fn main() !void {
var session = try browser.newSession({}); var session = try browser.newSession({});
// page // page
const page = try session.createPage(); const page = try session.createPage(null);
try page.start(null);
defer page.end();
_ = page.navigate(opts.url, null) catch |err| switch (err) { _ = page.navigate(opts.url, null) catch |err| switch (err) {
error.UnsupportedUriScheme, error.UriMissingHost => { error.UnsupportedUriScheme, error.UriMissingHost => {