mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-10-29 07:03:29 +00:00
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:
@@ -62,31 +62,35 @@ pub const Browser = struct {
|
||||
loop: *Loop,
|
||||
session: ?*Session,
|
||||
allocator: Allocator,
|
||||
http_client: HttpClient,
|
||||
session_pool: SessionPool,
|
||||
page_arena: std.heap.ArenaAllocator,
|
||||
|
||||
const SessionPool = std.heap.MemoryPool(Session);
|
||||
|
||||
const uri = "about:blank";
|
||||
|
||||
pub fn init(allocator: Allocator, loop: *Loop) Browser {
|
||||
return .{
|
||||
.loop = loop,
|
||||
.session = null,
|
||||
.allocator = allocator,
|
||||
.http_client = .{ .allocator = allocator },
|
||||
.session_pool = SessionPool.init(allocator),
|
||||
.page_arena = std.heap.ArenaAllocator.init(allocator),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Browser) void {
|
||||
self.closeSession();
|
||||
self.http_client.deinit();
|
||||
self.session_pool.deinit();
|
||||
self.page_arena.deinit();
|
||||
}
|
||||
|
||||
pub fn newSession(self: *Browser, ctx: anytype) !*Session {
|
||||
self.closeSession();
|
||||
|
||||
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;
|
||||
return session;
|
||||
}
|
||||
@@ -105,8 +109,7 @@ pub const Browser = struct {
|
||||
// You can create successively multiple pages for a session, but you must
|
||||
// deinit a page before running another one.
|
||||
pub const Session = struct {
|
||||
// allocator used to init the arena.
|
||||
allocator: Allocator,
|
||||
browser: *Browser,
|
||||
|
||||
// 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
|
||||
@@ -115,41 +118,34 @@ pub const Session = struct {
|
||||
// all others Session deps use directly self.alloc and not the arena.
|
||||
arena: std.heap.ArenaAllocator,
|
||||
|
||||
uri: []const u8,
|
||||
|
||||
// TODO handle proxy
|
||||
loader: Loader,
|
||||
|
||||
env: Env,
|
||||
loop: *Loop,
|
||||
inspector: jsruntime.Inspector,
|
||||
|
||||
window: Window,
|
||||
|
||||
// TODO move the shed to the browser?
|
||||
storageShed: storage.Shed,
|
||||
storage_shed: storage.Shed,
|
||||
page: ?Page = null,
|
||||
httpClient: HttpClient,
|
||||
|
||||
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.* = .{
|
||||
.uri = uri,
|
||||
.env = undefined,
|
||||
.browser = browser,
|
||||
.inspector = undefined,
|
||||
.allocator = allocator,
|
||||
.loader = Loader.init(allocator),
|
||||
.httpClient = .{ .allocator = allocator },
|
||||
.storageShed = storage.Shed.init(allocator),
|
||||
.storage_shed = storage.Shed.init(allocator),
|
||||
.arena = std.heap.ArenaAllocator.init(allocator),
|
||||
.window = Window.create(null, .{ .agent = user_agent }),
|
||||
.loop = loop,
|
||||
};
|
||||
|
||||
const arena = self.arena.allocator();
|
||||
|
||||
Env.init(&self.env, arena, loop, null);
|
||||
Env.init(&self.env, arena, browser.loop, null);
|
||||
errdefer self.env.deinit();
|
||||
try self.env.load(&self.jstypes);
|
||||
|
||||
@@ -164,24 +160,24 @@ pub const Session = struct {
|
||||
// const ctx_opaque = @as(*anyopaque, @ptrCast(ctx));
|
||||
self.inspector = try jsruntime.Inspector.init(
|
||||
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,
|
||||
InspectorContainer.onInspectorResponse,
|
||||
InspectorContainer.onInspectorEvent,
|
||||
);
|
||||
self.env.setInspector(self.inspector);
|
||||
|
||||
try self.env.setModuleLoadFn(self, Session.fetchModule);
|
||||
}
|
||||
|
||||
fn deinit(self: *Session) void {
|
||||
if (self.page) |*p| {
|
||||
p.deinit();
|
||||
if (self.page != null) {
|
||||
self.removePage();
|
||||
}
|
||||
|
||||
self.env.deinit();
|
||||
self.arena.deinit();
|
||||
self.httpClient.deinit();
|
||||
self.loader.deinit();
|
||||
self.storageShed.deinit();
|
||||
self.storage_shed.deinit();
|
||||
}
|
||||
|
||||
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));
|
||||
|
||||
if (self.page == null) return error.NoPage;
|
||||
if (self.page == null) {
|
||||
return error.NoPage;
|
||||
}
|
||||
|
||||
log.debug("fetch module: specifier: {s}", .{specifier});
|
||||
const alloc = self.arena.allocator();
|
||||
const body = try self.page.?.fetchData(alloc, specifier);
|
||||
defer alloc.free(body);
|
||||
|
||||
// fetchModule is called within the context of processing a page.
|
||||
// Use the page_arena for this, which has a more appropriate lifetime
|
||||
// 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);
|
||||
}
|
||||
|
||||
@@ -205,15 +204,62 @@ pub const Session = struct {
|
||||
|
||||
// NOTE: the caller is not the owner of the returned value,
|
||||
// the pointer on Page is just returned as a convenience
|
||||
pub fn createPage(self: *Session) !*Page {
|
||||
if (self.page != null) return error.SessionPageExists;
|
||||
self.page = Page.init(self.allocator, self);
|
||||
return &self.page.?;
|
||||
pub fn createPage(self: *Session, aux_data: ?[]const u8) !*Page {
|
||||
std.debug.assert(self.page == null);
|
||||
|
||||
_ = 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 {
|
||||
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.
|
||||
@@ -222,8 +268,8 @@ pub const Session = struct {
|
||||
// The page handle all its memory in an arena allocator. The arena is reseted
|
||||
// when end() is called.
|
||||
pub const Page = struct {
|
||||
arena: Allocator,
|
||||
session: *Session,
|
||||
arena: std.heap.ArenaAllocator,
|
||||
doc: ?*parser.Document = null,
|
||||
|
||||
// handle url
|
||||
@@ -237,71 +283,15 @@ pub const Page = struct {
|
||||
|
||||
raw_data: ?[]const u8 = null,
|
||||
|
||||
fn init(allocator: Allocator, session: *Session) Page {
|
||||
fn init(session: *Session) Page {
|
||||
return .{
|
||||
.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.
|
||||
pub fn dump(self: *const Page, out: std.fs.File) !void {
|
||||
|
||||
// if no HTML document pointer available, dump the data content only.
|
||||
if (self.doc == null) {
|
||||
// no data loaded, nothing to do.
|
||||
@@ -314,7 +304,6 @@ pub const Page = struct {
|
||||
}
|
||||
|
||||
pub fn wait(self: *Page) !void {
|
||||
|
||||
// try catch
|
||||
var try_catch: jsruntime.TryCatch = undefined;
|
||||
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.
|
||||
if (err == error.EnvNotStarted) return;
|
||||
|
||||
const alloc = self.arena.allocator();
|
||||
if (try try_catch.err(alloc, self.session.env)) |msg| {
|
||||
defer alloc.free(msg);
|
||||
const arena = self.arena;
|
||||
if (try try_catch.err(arena, self.session.env)) |msg| {
|
||||
defer arena.free(msg);
|
||||
log.info("wait error: {s}", .{msg});
|
||||
return;
|
||||
}
|
||||
@@ -335,10 +324,10 @@ pub const Page = struct {
|
||||
}
|
||||
|
||||
// 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
|
||||
pub fn navigate(self: *Page, uri: []const u8, auxData: ?[]const u8) !void {
|
||||
const alloc = self.arena.allocator();
|
||||
pub fn navigate(self: *Page, uri: []const u8, aux_data: ?[]const u8) !void {
|
||||
const arena = self.arena;
|
||||
|
||||
log.debug("starting GET {s}", .{uri});
|
||||
|
||||
@@ -348,26 +337,25 @@ pub const Page = struct {
|
||||
}
|
||||
|
||||
// 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.url = try URL.constructor(alloc, self.rawuri.?, null);
|
||||
self.url = try URL.constructor(arena, self.rawuri.?, null);
|
||||
self.location.url = &self.url.?;
|
||||
try self.session.window.replaceLocation(&self.location);
|
||||
|
||||
// prepare origin value.
|
||||
var buf = std.ArrayList(u8).init(alloc);
|
||||
defer buf.deinit();
|
||||
var buf: std.ArrayListUnmanaged(u8) = .{};
|
||||
try self.uri.writeToStream(.{
|
||||
.scheme = true,
|
||||
.authority = true,
|
||||
}, buf.writer());
|
||||
self.origin = try buf.toOwnedSlice();
|
||||
}, buf.writer(arena));
|
||||
self.origin = buf.items;
|
||||
|
||||
// TODO handle fragment in url.
|
||||
|
||||
// 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();
|
||||
|
||||
const req = resp.req;
|
||||
@@ -385,46 +373,43 @@ pub const Page = struct {
|
||||
// TODO handle charset
|
||||
// https://html.spec.whatwg.org/#content-type
|
||||
var it = req.response.iterateHeaders();
|
||||
var ct: ?[]const u8 = null;
|
||||
var ct_: ?[]const u8 = null;
|
||||
while (true) {
|
||||
const h = it.next() orelse break;
|
||||
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.
|
||||
// TODO try to sniff mime type from the body.
|
||||
log.info("no content-type HTTP header", .{});
|
||||
return;
|
||||
}
|
||||
defer alloc.free(ct.?);
|
||||
};
|
||||
|
||||
log.debug("header content-type: {s}", .{ct.?});
|
||||
var mime = try Mime.parse(alloc, ct.?);
|
||||
defer mime.deinit();
|
||||
log.debug("header content-type: {s}", .{ct});
|
||||
var mime = try Mime.parse(arena, ct);
|
||||
|
||||
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 {
|
||||
log.info("non-HTML document: {s}", .{ct.?});
|
||||
log.info("non-HTML document: {s}", .{ct});
|
||||
|
||||
// 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
|
||||
fn loadHTMLDoc(self: *Page, reader: anytype, charset: []const u8, auxData: ?[]const u8) !void {
|
||||
const alloc = self.arena.allocator();
|
||||
fn loadHTMLDoc(self: *Page, reader: anytype, charset: []const u8, aux_data: ?[]const u8) !void {
|
||||
const arena = self.arena;
|
||||
|
||||
// start netsurf memory arena.
|
||||
try parser.init();
|
||||
|
||||
log.debug("parse html with charset {s}", .{charset});
|
||||
|
||||
const ccharset = try alloc.dupeZ(u8, charset);
|
||||
defer alloc.free(ccharset);
|
||||
const ccharset = try arena.dupeZ(u8, charset);
|
||||
|
||||
const html_doc = try parser.documentHTMLParse(reader, ccharset);
|
||||
const doc = parser.documentHTMLToDocument(html_doc);
|
||||
@@ -438,22 +423,22 @@ pub const Page = struct {
|
||||
// inject the URL to the document including the fragment.
|
||||
try parser.documentSetDocumentURI(doc, self.rawuri orelse "about:blank");
|
||||
|
||||
const session = self.session;
|
||||
// TODO set the referrer to the document.
|
||||
|
||||
try self.session.window.replaceDocument(html_doc);
|
||||
self.session.window.setStorageShelf(
|
||||
try self.session.storageShed.getOrPut(self.origin orelse "null"),
|
||||
try session.window.replaceDocument(html_doc);
|
||||
session.window.setStorageShelf(
|
||||
try session.storage_shed.getOrPut(self.origin orelse "null"),
|
||||
);
|
||||
|
||||
// https://html.spec.whatwg.org/#read-html
|
||||
|
||||
// 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.
|
||||
try self.session.env.setUserContext(.{
|
||||
try session.env.setUserContext(.{
|
||||
.document = html_doc,
|
||||
.httpClient = &self.session.httpClient,
|
||||
.httpClient = &self.session.browser.http_client,
|
||||
});
|
||||
|
||||
// browse the DOM tree to retrieve scripts
|
||||
@@ -464,8 +449,7 @@ pub const Page = struct {
|
||||
// sasync stores scripts which can be run asynchronously.
|
||||
// for now they are just run after the non-async one in order to
|
||||
// dispatch DOMContentLoaded the sooner as possible.
|
||||
var sasync = std.ArrayList(Script).init(alloc);
|
||||
defer sasync.deinit();
|
||||
var sasync: std.ArrayListUnmanaged(Script) = .{};
|
||||
|
||||
const root = parser.documentToNode(doc);
|
||||
const walker = Walker{};
|
||||
@@ -496,8 +480,8 @@ pub const Page = struct {
|
||||
// > then the classic script will be fetched in parallel to
|
||||
// > parsing and evaluated as soon as it is available.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#async
|
||||
if (script.isasync) {
|
||||
try sasync.append(script);
|
||||
if (script.is_async) {
|
||||
try sasync.append(arena, script);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -562,8 +546,6 @@ pub const Page = struct {
|
||||
// if no src is present, we evaluate the text source.
|
||||
// https://html.spec.whatwg.org/multipage/scripting.html#script-processing-model
|
||||
fn evalScript(self: *Page, s: Script) !void {
|
||||
const alloc = self.arena.allocator();
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/webappapis.html#fetch-a-classic-script
|
||||
const opt_src = try parser.elementGetAttribute(s.element, "src");
|
||||
if (opt_src) |src| {
|
||||
@@ -591,7 +573,9 @@ pub const Page = struct {
|
||||
// TODO handle charset attribute
|
||||
const opt_text = try parser.nodeTextContent(parser.elementToNode(s.element));
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -607,27 +591,31 @@ pub const Page = struct {
|
||||
};
|
||||
|
||||
// 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});
|
||||
|
||||
var buffer: [1024]u8 = undefined;
|
||||
var b: []u8 = buffer[0..];
|
||||
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();
|
||||
|
||||
const resp = fetchres.req.response;
|
||||
|
||||
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
|
||||
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
|
||||
if (body.len == 0) return FetchError.NoBody;
|
||||
if (body.len == 0) {
|
||||
return FetchError.NoBody;
|
||||
}
|
||||
|
||||
return body;
|
||||
}
|
||||
@@ -635,17 +623,17 @@ pub const Page = struct {
|
||||
// fetchScript senf a GET request to the src and execute the script
|
||||
// received.
|
||||
fn fetchScript(self: *Page, s: Script) !void {
|
||||
const alloc = self.arena.allocator();
|
||||
const body = try self.fetchData(alloc, s.src);
|
||||
defer alloc.free(body);
|
||||
|
||||
try s.eval(alloc, self.session.env, body);
|
||||
const arena = self.arena;
|
||||
const body = try self.fetchData(arena, s.src);
|
||||
// TODO: change to &self.session.env when
|
||||
// https://github.com/lightpanda-io/zig-js-runtime/pull/285 lands
|
||||
try s.eval(arena, self.session.env, body);
|
||||
}
|
||||
|
||||
const Script = struct {
|
||||
element: *parser.Element,
|
||||
kind: Kind,
|
||||
isasync: bool,
|
||||
is_async: bool,
|
||||
|
||||
src: []const u8,
|
||||
|
||||
@@ -663,7 +651,7 @@ pub const Page = struct {
|
||||
return .{
|
||||
.element = e,
|
||||
.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",
|
||||
};
|
||||
@@ -682,7 +670,7 @@ pub const Page = struct {
|
||||
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;
|
||||
try_catch.init(env);
|
||||
defer try_catch.deinit();
|
||||
@@ -692,16 +680,14 @@ pub const Page = struct {
|
||||
.javascript => env.exec(body, self.src),
|
||||
.module => env.module(body, self.src),
|
||||
} catch {
|
||||
if (try try_catch.err(alloc, env)) |msg| {
|
||||
defer alloc.free(msg);
|
||||
if (try try_catch.err(arena, env)) |msg| {
|
||||
log.info("eval script {s}: {s}", .{ self.src, msg });
|
||||
}
|
||||
return FetchError.JsErr;
|
||||
};
|
||||
|
||||
if (builtin.mode == .Debug) {
|
||||
const msg = try res.toString(alloc, env);
|
||||
defer alloc.free(msg);
|
||||
const msg = try res.toString(arena, env);
|
||||
log.debug("eval script {s}: {s}", .{ self.src, msg });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,13 +55,15 @@ pub fn CDPT(comptime TypeProvider: type) type {
|
||||
allocator: Allocator,
|
||||
|
||||
// The active browser
|
||||
browser: ?Browser = null,
|
||||
browser: Browser,
|
||||
|
||||
target_id_gen: TargetIdGen = .{},
|
||||
session_id_gen: SessionIdGen = .{},
|
||||
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
|
||||
// 1 message at a time.
|
||||
@@ -77,15 +79,19 @@ pub fn CDPT(comptime TypeProvider: type) type {
|
||||
.client = client,
|
||||
.allocator = allocator,
|
||||
.browser_context = null,
|
||||
.browser = Browser.init(allocator, loop),
|
||||
.message_arena = std.heap.ArenaAllocator.init(allocator),
|
||||
.browser_context_pool = std.heap.MemoryPool(BrowserContext(Self)).init(allocator),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Self) void {
|
||||
if (self.browser_context) |*bc| {
|
||||
if (self.browser_context) |bc| {
|
||||
bc.deinit();
|
||||
}
|
||||
self.browser.deinit();
|
||||
self.message_arena.deinit();
|
||||
self.browser_context_pool.deinit();
|
||||
}
|
||||
|
||||
pub fn handleMessage(self: *Self, msg: []const u8) bool {
|
||||
@@ -119,7 +125,7 @@ pub fn CDPT(comptime TypeProvider: type) type {
|
||||
.cdp = self,
|
||||
.arena = arena,
|
||||
.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.
|
||||
@@ -213,7 +219,7 @@ pub fn CDPT(comptime TypeProvider: type) type {
|
||||
}
|
||||
|
||||
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;
|
||||
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();
|
||||
|
||||
// is this safe?
|
||||
self.browser_context = undefined;
|
||||
errdefer self.browser_context = null;
|
||||
try BrowserContext(Self).init(&self.browser_context.?, browser_context_id, self);
|
||||
const browser_context = try self.browser_context_pool.create();
|
||||
errdefer self.browser_context_pool.destroy(browser_context);
|
||||
|
||||
try BrowserContext(Self).init(browser_context, browser_context_id, self);
|
||||
self.browser_context = browser_context;
|
||||
return browser_context_id;
|
||||
}
|
||||
|
||||
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) {
|
||||
return false;
|
||||
}
|
||||
bc.deinit();
|
||||
self.browser_context_pool.destroy(bc);
|
||||
self.browser_context = null;
|
||||
return true;
|
||||
}
|
||||
@@ -257,7 +264,6 @@ pub fn BrowserContext(comptime CDP_T: type) type {
|
||||
id: []const u8,
|
||||
cdp: *CDP_T,
|
||||
|
||||
browser: CDP_T.Browser,
|
||||
// Represents the browser session. There is no equivalent in CDP. For
|
||||
// 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
|
||||
@@ -294,22 +300,17 @@ pub fn BrowserContext(comptime CDP_T: type) type {
|
||||
self.* = .{
|
||||
.id = id,
|
||||
.cdp = cdp,
|
||||
.browser = undefined,
|
||||
.session = undefined,
|
||||
.target_id = null,
|
||||
.session_id = null,
|
||||
.url = URL_BASE,
|
||||
.security_origin = URL_BASE,
|
||||
.secure_context_type = "Secure", // TODO = enum
|
||||
.loader_id = LOADER_ID,
|
||||
.session = try cdp.browser.newSession(self),
|
||||
.page_life_cycle_events = false, // TODO; Target based value
|
||||
.node_list = dom.NodeList.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 {
|
||||
@@ -318,7 +319,6 @@ pub fn BrowserContext(comptime CDP_T: type) type {
|
||||
s.deinit();
|
||||
}
|
||||
self.node_search_list.deinit();
|
||||
self.browser.deinit();
|
||||
}
|
||||
|
||||
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) {
|
||||
_ = try self.cdp.createBrowserContext();
|
||||
self.browser_context = &self.cdp.browser_context.?;
|
||||
self.browser_context = self.cdp.browser_context.?;
|
||||
return self.browser_context.?;
|
||||
}
|
||||
|
||||
|
||||
@@ -116,15 +116,8 @@ fn createTarget(cmd: anytype) !void {
|
||||
// if target_id is null, we should never have a session_id
|
||||
std.debug.assert(bc.session_id == null);
|
||||
|
||||
const page = try bc.session.createPage();
|
||||
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
|
||||
const aux_data = try std.fmt.allocPrint(
|
||||
cmd.arena,
|
||||
@@ -132,7 +125,13 @@ fn createTarget(cmd: anytype) !void {
|
||||
"{{\"isDefault\":true,\"type\":\"default\",\"frameId\":\"{s}\"}}",
|
||||
.{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
|
||||
// TODO: should this only be sent when Target.setDiscoverTargets
|
||||
@@ -213,7 +212,7 @@ fn closeTarget(cmd: anytype) !void {
|
||||
bc.session_id = null;
|
||||
}
|
||||
|
||||
bc.session.currentPage().?.end();
|
||||
bc.session.removePage();
|
||||
bc.target_id = null;
|
||||
}
|
||||
|
||||
@@ -508,7 +507,7 @@ test "cdp.target: closeTarget" {
|
||||
}
|
||||
|
||||
// pretend we createdTarget first
|
||||
_ = try bc.session.createPage();
|
||||
_ = try bc.session.createPage(null);
|
||||
bc.target_id = "TID-A";
|
||||
{
|
||||
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
|
||||
_ = try bc.session.createPage();
|
||||
_ = try bc.session.createPage(null);
|
||||
bc.target_id = "TID-B";
|
||||
{
|
||||
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
|
||||
_ = try bc.session.createPage();
|
||||
_ = try bc.session.createPage(null);
|
||||
bc.target_id = "TID-A";
|
||||
{
|
||||
try testing.expectError(error.UnknownTargetId, ctx.processMessage(.{ .id = 10, .method = "Target.getTargetInfo", .params = .{ .targetId = "TID-8" } }));
|
||||
|
||||
@@ -56,17 +56,21 @@ const Session = struct {
|
||||
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) {
|
||||
return error.MockBrowserPageAlreadyExists;
|
||||
}
|
||||
self.page = .{
|
||||
.session = self,
|
||||
.allocator = self.allocator,
|
||||
.aux_data = try self.allocator.dupe(u8, aux_data orelse ""),
|
||||
};
|
||||
return &self.page.?;
|
||||
}
|
||||
|
||||
pub fn removePage(self: *Session) void {
|
||||
self.page = null;
|
||||
}
|
||||
|
||||
pub fn callInspector(self: *Session, msg: []const u8) void {
|
||||
_ = self;
|
||||
_ = msg;
|
||||
@@ -75,7 +79,6 @@ const Session = struct {
|
||||
|
||||
const Page = struct {
|
||||
session: *Session,
|
||||
allocator: Allocator,
|
||||
aux_data: []const u8 = "",
|
||||
doc: ?*parser.Document = null,
|
||||
|
||||
@@ -84,14 +87,6 @@ const Page = struct {
|
||||
_ = url;
|
||||
_ = 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 {
|
||||
@@ -152,13 +147,15 @@ const TestContext = struct {
|
||||
};
|
||||
pub fn loadBrowserContext(self: *TestContext, opts: BrowserContextOpts) !*main.BrowserContext(TestCDP) {
|
||||
var c = self.cdp();
|
||||
if (c.browser_context) |*bc| {
|
||||
c.browser.session = null;
|
||||
|
||||
if (c.browser_context) |bc| {
|
||||
bc.deinit();
|
||||
c.browser_context = null;
|
||||
}
|
||||
|
||||
_ = try c.createBrowserContext();
|
||||
var bc = &c.browser_context.?;
|
||||
var bc = c.browser_context.?;
|
||||
|
||||
if (opts.id) |id| {
|
||||
bc.id = id;
|
||||
|
||||
@@ -58,17 +58,17 @@ pub const Window = struct {
|
||||
navigator: Navigator,
|
||||
|
||||
pub fn create(target: ?[]const u8, navigator: ?Navigator) Window {
|
||||
return Window{
|
||||
return .{
|
||||
.target = target orelse "",
|
||||
.navigator = navigator orelse .{},
|
||||
};
|
||||
}
|
||||
|
||||
pub fn replaceLocation(self: *Window, loc: *Location) !void {
|
||||
self.location = loc;
|
||||
pub fn replaceLocation(self: *Window, loc: ?*Location) !void {
|
||||
self.location = loc orelse &emptyLocation;
|
||||
|
||||
if (self.document != null) {
|
||||
try parser.documentHTMLSetLocation(Location, self.document.?, self.location);
|
||||
if (self.document) |doc| {
|
||||
try parser.documentHTMLSetLocation(Location, doc, self.location);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -96,9 +96,7 @@ pub fn main() !void {
|
||||
var session = try browser.newSession({});
|
||||
|
||||
// page
|
||||
const page = try session.createPage();
|
||||
try page.start(null);
|
||||
defer page.end();
|
||||
const page = try session.createPage(null);
|
||||
|
||||
_ = page.navigate(opts.url, null) catch |err| switch (err) {
|
||||
error.UnsupportedUriScheme, error.UriMissingHost => {
|
||||
|
||||
Reference in New Issue
Block a user