diff --git a/src/browser/Page.zig b/src/browser/Page.zig index 964233ad..52a1b07f 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -942,6 +942,7 @@ fn clearTransferArena(self: *Page) void { self.arena_pool.reset(self._session.transfer_arena, 4 * 1024); } +<<<<<<< HEAD pub fn wait(self: *Page, wait_ms: u32) Session.WaitResult { return self._wait(wait_ms) catch |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 { 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", .{}); self._session.notification.dispatch(.page_network_idle, &.{ .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", .{}); self._session.notification.dispatch(.page_network_almost_idle, &.{ .page_id = self.id, diff --git a/src/browser/Session.zig b/src/browser/Session.zig index 0b9228bd..1fef35c9 100644 --- a/src/browser/Session.zig +++ b/src/browser/Session.zig @@ -18,6 +18,7 @@ const std = @import("std"); const lp = @import("lightpanda"); +const builtin = @import("builtin"); const log = @import("../log.zig"); @@ -31,7 +32,7 @@ const Browser = @import("Browser.zig"); const Notification = @import("../Notification.zig"); 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. // 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 { + var page = &(self.page orelse return .no_page); while (true) { - if (self.page) |*page| { - switch (page.wait(wait_ms)) { - .done => { - if (page._queued_navigation == null) { - return .done; - } - self.processScheduledNavigation() catch return .done; - }, - else => |result| return result, + const wait_result = self._wait(page, wait_ms) catch |err| { + switch (err) { + error.JsError => {}, // already logged (with hopefully more context) + else => log.err(.browser, "session wait", .{ .err = err, }), } - } else { - return .no_page; + return .done; + }; + + 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); const url, const opts, const page_id = blk: { const page = self.page.?; @@ -226,4 +368,6 @@ fn processScheduledNavigation(self: *Session) !void { log.err(.browser, "queued navigation error", .{ .err = err, .url = url }); return err; }; + + return page; } diff --git a/src/main_wpt.zig b/src/main_wpt.zig index bf63c6c2..3db9d92b 100644 --- a/src/main_wpt.zig +++ b/src/main_wpt.zig @@ -126,7 +126,7 @@ fn run( const url = try std.fmt.allocPrintSentinel(arena, "http://localhost:9582/{s}", .{test_file}, 0); try page.navigate(url, .{}); - _ = page.wait(2000); + _ = session.wait(2000); var ls: lp.js.Local.Scope = undefined; page.js.localScope(&ls);