Move page.wait to session.wait

page.wait is the only significant difference between the "root" page and a page
for an iframe. I think it's more explicit to move this out of the page and
into the session, which was already the sole entry-point for page.wait.
This commit is contained in:
Karl Seguin
2026-02-17 17:54:29 +08:00
parent 081979be3b
commit da48ffe05c
3 changed files with 165 additions and 18 deletions

View File

@@ -942,6 +942,7 @@ fn clearTransferArena(self: *Page) void {
self.arena_pool.reset(self._session.transfer_arena, 4 * 1024); self.arena_pool.reset(self._session.transfer_arena, 4 * 1024);
} }
<<<<<<< HEAD
pub fn wait(self: *Page, wait_ms: u32) Session.WaitResult { pub fn wait(self: *Page, wait_ms: u32) Session.WaitResult {
return self._wait(wait_ms) catch |err| { return self._wait(wait_ms) catch |err| {
switch (err) { switch (err) {
@@ -1173,6 +1174,8 @@ fn printWaitAnalysis(self: *Page) void {
} }
} }
=======
>>>>>>> 3bd80eb3 (Move page.wait to session.wait)
pub fn isGoingAway(self: *const Page) bool { pub fn isGoingAway(self: *const Page) bool {
return self._queued_navigation != null; return self._queued_navigation != null;
} }
@@ -1565,7 +1568,7 @@ pub fn deliverSlotchangeEvents(self: *Page) void {
} }
} }
fn notifyNetworkIdle(self: *Page) void { pub fn notifyNetworkIdle(self: *Page) void {
lp.assert(self._notified_network_idle == .done, "Page.notifyNetworkIdle", .{}); lp.assert(self._notified_network_idle == .done, "Page.notifyNetworkIdle", .{});
self._session.notification.dispatch(.page_network_idle, &.{ self._session.notification.dispatch(.page_network_idle, &.{
.page_id = self.id, .page_id = self.id,
@@ -1574,7 +1577,7 @@ fn notifyNetworkIdle(self: *Page) void {
}); });
} }
fn notifyNetworkAlmostIdle(self: *Page) void { pub fn notifyNetworkAlmostIdle(self: *Page) void {
lp.assert(self._notified_network_almost_idle == .done, "Page.notifyNetworkAlmostIdle", .{}); lp.assert(self._notified_network_almost_idle == .done, "Page.notifyNetworkAlmostIdle", .{});
self._session.notification.dispatch(.page_network_almost_idle, &.{ self._session.notification.dispatch(.page_network_almost_idle, &.{
.page_id = self.id, .page_id = self.id,

View File

@@ -18,6 +18,7 @@
const std = @import("std"); const std = @import("std");
const lp = @import("lightpanda"); const lp = @import("lightpanda");
const builtin = @import("builtin");
const log = @import("../log.zig"); const log = @import("../log.zig");
@@ -31,7 +32,7 @@ const Browser = @import("Browser.zig");
const Notification = @import("../Notification.zig"); const Notification = @import("../Notification.zig");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const IS_DEBUG = @import("builtin").mode == .Debug; const IS_DEBUG = builtin.mode == .Debug;
// Session is like a browser's tab. // Session is like a browser's tab.
// It owns the js env and the loader for all the pages of the session. // It owns the js env and the loader for all the pages of the session.
@@ -176,26 +177,167 @@ pub fn findPage(self: *Session, id: u32) ?*Page {
} }
pub fn wait(self: *Session, wait_ms: u32) WaitResult { pub fn wait(self: *Session, wait_ms: u32) WaitResult {
var page = &(self.page orelse return .no_page);
while (true) { while (true) {
if (self.page) |*page| { const wait_result = self._wait(page, wait_ms) catch |err| {
switch (page.wait(wait_ms)) { switch (err) {
.done => { error.JsError => {}, // already logged (with hopefully more context)
if (page._queued_navigation == null) { else => log.err(.browser, "session wait", .{ .err = err, }),
return .done;
}
self.processScheduledNavigation() catch return .done;
},
else => |result| return result,
} }
} else { return .done;
return .no_page; };
switch (wait_result) {
.done => {
if (page._queued_navigation == null) {
return .done;
}
page = self.processScheduledNavigation() catch return .done;
},
else => |result| return result,
} }
// if we've successfull navigated, we'll give the new page another
// page.wait(wait_ms)
} }
} }
fn processScheduledNavigation(self: *Session) !void { fn _wait(self: *Session, page: *Page, wait_ms: u32) !WaitResult {
var timer = try std.time.Timer.start();
var ms_remaining = wait_ms;
const browser = self.browser;
var http_client = browser.http_client;
// I'd like the page to know NOTHING about cdp_socket / CDP, but the
// fact is that the behavior of wait changes depending on whether or
// not we're using CDP.
// If we aren't using CDP, as soon as we think there's nothing left
// to do, we can exit - we'de done.
// But if we are using CDP, we should wait for the whole `wait_ms`
// because the http_click.tick() also monitors the CDP socket. And while
// we could let CDP poll http (like it does for HTTP requests), the fact
// is that we know more about the timing of stuff (e.g. how long to
// poll/sleep) in the page.
const exit_when_done = http_client.cdp_client == null;
while (true) {
switch (page._parse_state) {
.pre, .raw, .text, .image => {
// The main page hasn't started/finished navigating.
// There's no JS to run, and no reason to run the scheduler.
if (http_client.active == 0 and exit_when_done) {
// haven't started navigating, I guess.
return .done;
}
// Either we have active http connections, or we're in CDP
// mode with an extra socket. Either way, we're waiting
// for http traffic
if (try http_client.tick(@intCast(ms_remaining)) == .cdp_socket) {
// exit_when_done is explicitly set when there isn't
// an extra socket, so it should not be possibl to
// get an cdp_socket message when exit_when_done
// is true.
if (IS_DEBUG) {
std.debug.assert(exit_when_done == false);
}
// data on a socket we aren't handling, return to caller
return .cdp_socket;
}
},
.html, .complete => {
if (page._queued_navigation != null) {
return .done;
}
// The HTML page was parsed. We now either have JS scripts to
// download, or scheduled tasks to execute, or both.
// scheduler.run could trigger new http transfers, so do not
// store http_client.active BEFORE this call and then use
// it AFTER.
const ms_to_next_task = try browser.runMacrotasks();
const http_active = http_client.active;
const total_network_activity = http_active + http_client.intercepted;
if (page._notified_network_almost_idle.check(total_network_activity <= 2)) {
page.notifyNetworkAlmostIdle();
}
if (page._notified_network_idle.check(total_network_activity == 0)) {
page.notifyNetworkIdle();
}
if (http_active == 0 and exit_when_done) {
// we don't need to consider http_client.intercepted here
// because exit_when_done is true, and that can only be
// the case when interception isn't possible.
if (comptime IS_DEBUG) {
std.debug.assert(http_client.intercepted == 0);
}
const ms = ms_to_next_task orelse blk: {
if (wait_ms - ms_remaining < 100) {
if (comptime builtin.is_test) {
return .done;
}
// Look, we want to exit ASAP, but we don't want
// to exit so fast that we've run none of the
// background jobs.
break :blk 50;
}
// No http transfers, no cdp extra socket, no
// scheduled tasks, we're done.
return .done;
};
if (ms > ms_remaining) {
// Same as above, except we have a scheduled task,
// it just happens to be too far into the future
// compared to how long we were told to wait.
return .done;
}
// We have a task to run in the not-so-distant future.
// You might think we can just sleep until that task is
// ready, but we should continue to run lowPriority tasks
// in the meantime, and that could unblock things. So
// we'll just sleep for a bit, and then restart our wait
// loop to see if anything new can be processed.
std.Thread.sleep(std.time.ns_per_ms * @as(u64, @intCast(@min(ms, 20))));
} else {
// We're here because we either have active HTTP
// connections, or exit_when_done == false (aka, there's
// an cdp_socket registered with the http client).
// We should continue to run lowPriority tasks, so we
// minimize how long we'll poll for network I/O.
const ms_to_wait = @min(200, @min(ms_remaining, ms_to_next_task orelse 200));
if (try http_client.tick(ms_to_wait) == .cdp_socket) {
// data on a socket we aren't handling, return to caller
return .cdp_socket;
}
}
},
.err => |err| {
page._parse_state = .{ .raw_done = @errorName(err) };
return err;
},
.raw_done => {
if (exit_when_done) {
return .done;
}
// we _could_ http_client.tick(ms_to_wait), but this has
// the same result, and I feel is more correct.
return .no_page;
},
}
const ms_elapsed = timer.lap() / 1_000_000;
if (ms_elapsed >= ms_remaining) {
return .done;
}
ms_remaining -= @intCast(ms_elapsed);
}
}
fn processScheduledNavigation(self: *Session) !*Page {
defer self.browser.arena_pool.reset(self.transfer_arena, 4 * 1024); defer self.browser.arena_pool.reset(self.transfer_arena, 4 * 1024);
const url, const opts, const page_id = blk: { const url, const opts, const page_id = blk: {
const page = self.page.?; const page = self.page.?;
@@ -226,4 +368,6 @@ fn processScheduledNavigation(self: *Session) !void {
log.err(.browser, "queued navigation error", .{ .err = err, .url = url }); log.err(.browser, "queued navigation error", .{ .err = err, .url = url });
return err; return err;
}; };
return page;
} }

View File

@@ -126,7 +126,7 @@ fn run(
const url = try std.fmt.allocPrintSentinel(arena, "http://localhost:9582/{s}", .{test_file}, 0); const url = try std.fmt.allocPrintSentinel(arena, "http://localhost:9582/{s}", .{test_file}, 0);
try page.navigate(url, .{}); try page.navigate(url, .{});
_ = page.wait(2000); _ = session.wait(2000);
var ls: lp.js.Local.Scope = undefined; var ls: lp.js.Local.Scope = undefined;
page.js.localScope(&ls); page.js.localScope(&ls);