Emit networkIdle and networkAlmostIdle Page.lifecycleEvent

Most CDP drivers have a mechanism to wait for idle network, or an almost idle
network (sometimes called networkIdle2). These are events the browser must emit.

The page will now emit `networkIdle` when we are reasonably sure there's no more
network activity (this requires some slight changes to request interception,
since, I believe, intercepted requests should be considered).

`networkAlmostIdle` is currently _always_ emitted prior to emitting
`networkIdle`. We should tweak this but I can't, at a glance, think of a great
heuristic for when this should be emitted.
This commit is contained in:
Karl Seguin
2025-09-04 16:28:12 +08:00
parent 7fdc857326
commit 5dda86bf4a
6 changed files with 163 additions and 21 deletions

View File

@@ -487,6 +487,18 @@ pub fn BrowserContext(comptime CDP_T: type) type {
self.cdp.browser.notification.unregister(.http_request_auth_required, self);
}
pub fn lifecycleEventsEnable(self: *Self) !void {
self.page_life_cycle_events = true;
try self.cdp.browser.notification.register(.page_network_idle, self, onPageNetworkIdle);
try self.cdp.browser.notification.register(.page_network_almost_idle, self, onPageNetworkAlmostIdle);
}
pub fn lifecycleEventsDisable(self: *Self) void {
self.page_life_cycle_events = false;
self.cdp.browser.notification.unregister(.page_network_idle, self);
self.cdp.browser.notification.unregister(.page_network_almost_idle, self);
}
pub fn onPageRemove(ctx: *anyopaque, _: Notification.PageRemove) !void {
const self: *Self = @ptrCast(@alignCast(ctx));
try @import("domains/page.zig").pageRemove(self);
@@ -508,6 +520,16 @@ pub fn BrowserContext(comptime CDP_T: type) type {
return @import("domains/page.zig").pageNavigated(self, msg);
}
pub fn onPageNetworkIdle(ctx: *anyopaque, msg: *const Notification.PageNetworkIdle) !void {
const self: *Self = @ptrCast(@alignCast(ctx));
return @import("domains/page.zig").pageNetworkIdle(self, msg);
}
pub fn onPageNetworkAlmostIdle(ctx: *anyopaque, msg: *const Notification.PageNetworkAlmostIdle) !void {
const self: *Self = @ptrCast(@alignCast(ctx));
return @import("domains/page.zig").pageNetworkAlmostIdle(self, msg);
}
pub fn onHttpRequestStart(ctx: *anyopaque, msg: *const Notification.RequestStart) !void {
const self: *Self = @ptrCast(@alignCast(ctx));
defer self.resetNotificationArena();

View File

@@ -261,7 +261,7 @@ fn continueRequest(cmd: anytype) !void {
transfer.req.body = body;
}
try bc.cdp.browser.http_client.process(transfer);
try bc.cdp.browser.http_client.continueTransfer(transfer);
return cmd.sendResult(null, .{});
}
@@ -311,7 +311,7 @@ fn continueWithAuth(cmd: anytype) !void {
);
transfer.reset();
try bc.cdp.browser.http_client.process(transfer);
try bc.cdp.browser.http_client.continueTransfer(transfer);
return cmd.sendResult(null, .{});
}
@@ -352,7 +352,7 @@ fn fulfillRequest(cmd: anytype) !void {
body = buf;
}
try transfer.fulfill(params.responseCode, params.responseHeaders orelse &.{}, body);
try bc.cdp.browser.http_client.fulfillTransfer(transfer, params.responseCode, params.responseHeaders orelse &.{}, body);
return cmd.sendResult(null, .{});
}
@@ -368,7 +368,7 @@ fn failRequest(cmd: anytype) !void {
const request_id = try idFromRequestId(params.requestId);
const transfer = intercept_state.remove(request_id) orelse return error.RequestNotFound;
defer transfer.abort();
defer bc.cdp.browser.http_client.abortTransfer(transfer);
log.info(.cdp, "request intercept", .{
.state = "fail",

View File

@@ -78,12 +78,16 @@ fn getFrameTree(cmd: anytype) !void {
}
fn setLifecycleEventsEnabled(cmd: anytype) !void {
// const params = (try cmd.params(struct {
// enabled: bool,
// })) orelse return error.InvalidParams;
const params = (try cmd.params(struct {
enabled: bool,
})) orelse return error.InvalidParams;
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
bc.page_life_cycle_events = true;
if (params.enabled) {
try bc.lifecycleEventsEnable();
} else {
bc.lifecycleEventsDisable();
}
return cmd.sendResult(null, .{});
}
@@ -357,6 +361,27 @@ pub fn pageNavigated(bc: anytype, event: *const Notification.PageNavigated) !voi
}, .{ .session_id = session_id });
}
pub fn pageNetworkIdle(bc: anytype, event: *const Notification.PageNetworkIdle) !void {
return sendPageLifecycle(bc, "networkIdle", event.timestamp);
}
pub fn pageNetworkAlmostIdle(bc: anytype, event: *const Notification.PageNetworkAlmostIdle) !void {
return sendPageLifecycle(bc, "networkAlmostIdle", event.timestamp);
}
fn sendPageLifecycle(bc: anytype, name: []const u8, timestamp: u32) !void {
const loader_id = bc.loader_id;
const target_id = bc.target_id orelse unreachable;
const session_id = bc.session_id orelse unreachable;
return bc.cdp.sendEvent("Page.lifecycleEvent", LifecycleEvent{
.name = name,
.frameId = target_id,
.loaderId = loader_id,
.timestamp = timestamp,
}, .{ .session_id = session_id });
}
const LifecycleEvent = struct {
frameId: []const u8,
loaderId: ?[]const u8,