Merge pull request #463 from karlseguin/page_arena

Optimize memory usage
This commit is contained in:
Pierre Tachoire
2025-03-14 10:17:41 +01:00
committed by GitHub
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 => {