mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-10-29 23:23:28 +00:00
Merge pull request #463 from karlseguin/page_arena
Optimize memory usage
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