Skip to content

Commit 5dda86b

Browse files
committed
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.
1 parent 7fdc857 commit 5dda86b

File tree

6 files changed

+163
-21
lines changed

6 files changed

+163
-21
lines changed

src/browser/page.zig

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,11 @@ pub const Page = struct {
9090

9191
load_state: LoadState = .parsing,
9292

93+
// We should only emit these events once per page. To make sure of that, we
94+
// track whether or not we've already emitted the notifications.
95+
notified_network_idle: bool = false,
96+
notified_network_almost_idle: bool = false,
97+
9398
const Mode = union(enum) {
9499
pre: void,
95100
err: anyerror,
@@ -329,7 +334,13 @@ pub const Page = struct {
329334
return error.JsError;
330335
}
331336

332-
if (http_client.active == 0 and exit_when_done) {
337+
const http_active = http_client.active;
338+
if (http_active == 0 and exit_when_done) {
339+
// we don't need to concider http_client.intercepted here
340+
// because exit_when_done is true, and that can only be
341+
// the case when interception isn't possible.
342+
std.debug.assert(http_client.intercepted == 0);
343+
333344
const ms = ms_to_next_task orelse blk: {
334345
// TODO: when jsRunner is fully replaced with the
335346
// htmlRunner, we can remove the first part of this
@@ -341,29 +352,34 @@ pub const Page = struct {
341352
// background jobs.
342353
break :blk 50;
343354
}
344-
// no http transfers, no cdp extra socket, no
355+
// No http transfers, no cdp extra socket, no
345356
// scheduled tasks, we're done.
346357
return .done;
347358
};
348359

349360
if (ms > ms_remaining) {
350-
// same as above, except we have a scheduled task,
351-
// it just happens to be too far into the future.s
361+
// Same as above, except we have a scheduled task,
362+
// it just happens to be too far into the future.
352363
return .done;
353364
}
354365

355-
// we have a task to run in the not-so-distant future.
366+
// We have a task to run in the not-so-distant future.
356367
// You might think we can just sleep until that task is
357368
// ready, but we should continue to run lowPriority tasks
358369
// in the meantime, and that could unblock things. So
359370
// we'll just sleep for a bit, and then restart our wait
360-
// loop to see what's changed
371+
// loop to see if anything new can be processed.
361372
std.Thread.sleep(std.time.ns_per_ms * @as(u64, @intCast(@min(ms, 20))));
362373
} else {
374+
if (self.notified_network_idle == false and http_active == 0 and http_client.intercepted == 0) {
375+
self.notifyNetworkIdle();
376+
}
363377
// We're here because we either have active HTTP
364-
// connections, of exit_when_done == false (aka, there's
378+
// connections, or exit_when_done == false (aka, there's
365379
// an extra_socket registered with the http client).
366-
const ms_to_wait = @min(ms_remaining, ms_to_next_task orelse 100);
380+
// We should continue to run lowPriority tasks, so we
381+
// minimize how long we'll poll for network I/O.
382+
const ms_to_wait = @min(200, @min(ms_remaining, ms_to_next_task orelse 200));
367383
if (try http_client.tick(ms_to_wait) == .extra_socket) {
368384
// data on a socket we aren't handling, return to caller
369385
return .extra_socket;
@@ -467,6 +483,31 @@ pub const Page = struct {
467483
}
468484
}
469485

486+
fn notifyNetworkIdle(self: *Page) void {
487+
// caller should always check that we haven't sent this already;
488+
std.debug.assert(self.notified_network_idle == false);
489+
490+
// if we're going to send networkIdle, we should first send networkAlmostIdle
491+
// if it hasn't already been sent.
492+
if (self.notified_network_almost_idle == false) {
493+
self.notifyNetworkAlmostIdle();
494+
}
495+
496+
self.notified_network_idle = true;
497+
self.session.browser.notification.dispatch(.page_network_idle, &.{
498+
.timestamp = timestamp(),
499+
});
500+
}
501+
502+
fn notifyNetworkAlmostIdle(self: *Page) void {
503+
// caller should always check that we haven't sent this already;
504+
std.debug.assert(self.notified_network_almost_idle == false);
505+
self.notified_network_almost_idle = true;
506+
self.session.browser.notification.dispatch(.page_network_almost_idle, &.{
507+
.timestamp = timestamp(),
508+
});
509+
}
510+
470511
pub fn origin(self: *const Page, arena: Allocator) ![]const u8 {
471512
var aw = std.Io.Writer.Allocating.init(arena);
472513
try self.url.origin(&aw.writer);

src/cdp/cdp.zig

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,18 @@ pub fn BrowserContext(comptime CDP_T: type) type {
487487
self.cdp.browser.notification.unregister(.http_request_auth_required, self);
488488
}
489489

490+
pub fn lifecycleEventsEnable(self: *Self) !void {
491+
self.page_life_cycle_events = true;
492+
try self.cdp.browser.notification.register(.page_network_idle, self, onPageNetworkIdle);
493+
try self.cdp.browser.notification.register(.page_network_almost_idle, self, onPageNetworkAlmostIdle);
494+
}
495+
496+
pub fn lifecycleEventsDisable(self: *Self) void {
497+
self.page_life_cycle_events = false;
498+
self.cdp.browser.notification.unregister(.page_network_idle, self);
499+
self.cdp.browser.notification.unregister(.page_network_almost_idle, self);
500+
}
501+
490502
pub fn onPageRemove(ctx: *anyopaque, _: Notification.PageRemove) !void {
491503
const self: *Self = @ptrCast(@alignCast(ctx));
492504
try @import("domains/page.zig").pageRemove(self);
@@ -508,6 +520,16 @@ pub fn BrowserContext(comptime CDP_T: type) type {
508520
return @import("domains/page.zig").pageNavigated(self, msg);
509521
}
510522

523+
pub fn onPageNetworkIdle(ctx: *anyopaque, msg: *const Notification.PageNetworkIdle) !void {
524+
const self: *Self = @ptrCast(@alignCast(ctx));
525+
return @import("domains/page.zig").pageNetworkIdle(self, msg);
526+
}
527+
528+
pub fn onPageNetworkAlmostIdle(ctx: *anyopaque, msg: *const Notification.PageNetworkAlmostIdle) !void {
529+
const self: *Self = @ptrCast(@alignCast(ctx));
530+
return @import("domains/page.zig").pageNetworkAlmostIdle(self, msg);
531+
}
532+
511533
pub fn onHttpRequestStart(ctx: *anyopaque, msg: *const Notification.RequestStart) !void {
512534
const self: *Self = @ptrCast(@alignCast(ctx));
513535
defer self.resetNotificationArena();

src/cdp/domains/fetch.zig

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ fn continueRequest(cmd: anytype) !void {
261261
transfer.req.body = body;
262262
}
263263

264-
try bc.cdp.browser.http_client.process(transfer);
264+
try bc.cdp.browser.http_client.continueTransfer(transfer);
265265
return cmd.sendResult(null, .{});
266266
}
267267

@@ -311,7 +311,7 @@ fn continueWithAuth(cmd: anytype) !void {
311311
);
312312

313313
transfer.reset();
314-
try bc.cdp.browser.http_client.process(transfer);
314+
try bc.cdp.browser.http_client.continueTransfer(transfer);
315315
return cmd.sendResult(null, .{});
316316
}
317317

@@ -352,7 +352,7 @@ fn fulfillRequest(cmd: anytype) !void {
352352
body = buf;
353353
}
354354

355-
try transfer.fulfill(params.responseCode, params.responseHeaders orelse &.{}, body);
355+
try bc.cdp.browser.http_client.fulfillTransfer(transfer, params.responseCode, params.responseHeaders orelse &.{}, body);
356356

357357
return cmd.sendResult(null, .{});
358358
}
@@ -368,7 +368,7 @@ fn failRequest(cmd: anytype) !void {
368368
const request_id = try idFromRequestId(params.requestId);
369369

370370
const transfer = intercept_state.remove(request_id) orelse return error.RequestNotFound;
371-
defer transfer.abort();
371+
defer bc.cdp.browser.http_client.abortTransfer(transfer);
372372

373373
log.info(.cdp, "request intercept", .{
374374
.state = "fail",

src/cdp/domains/page.zig

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,12 +78,16 @@ fn getFrameTree(cmd: anytype) !void {
7878
}
7979

8080
fn setLifecycleEventsEnabled(cmd: anytype) !void {
81-
// const params = (try cmd.params(struct {
82-
// enabled: bool,
83-
// })) orelse return error.InvalidParams;
81+
const params = (try cmd.params(struct {
82+
enabled: bool,
83+
})) orelse return error.InvalidParams;
8484

8585
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
86-
bc.page_life_cycle_events = true;
86+
if (params.enabled) {
87+
try bc.lifecycleEventsEnable();
88+
} else {
89+
bc.lifecycleEventsDisable();
90+
}
8791
return cmd.sendResult(null, .{});
8892
}
8993

@@ -357,6 +361,27 @@ pub fn pageNavigated(bc: anytype, event: *const Notification.PageNavigated) !voi
357361
}, .{ .session_id = session_id });
358362
}
359363

364+
pub fn pageNetworkIdle(bc: anytype, event: *const Notification.PageNetworkIdle) !void {
365+
return sendPageLifecycle(bc, "networkIdle", event.timestamp);
366+
}
367+
368+
pub fn pageNetworkAlmostIdle(bc: anytype, event: *const Notification.PageNetworkAlmostIdle) !void {
369+
return sendPageLifecycle(bc, "networkAlmostIdle", event.timestamp);
370+
}
371+
372+
fn sendPageLifecycle(bc: anytype, name: []const u8, timestamp: u32) !void {
373+
const loader_id = bc.loader_id;
374+
const target_id = bc.target_id orelse unreachable;
375+
const session_id = bc.session_id orelse unreachable;
376+
377+
return bc.cdp.sendEvent("Page.lifecycleEvent", LifecycleEvent{
378+
.name = name,
379+
.frameId = target_id,
380+
.loaderId = loader_id,
381+
.timestamp = timestamp,
382+
}, .{ .session_id = session_id });
383+
}
384+
360385
const LifecycleEvent = struct {
361386
frameId: []const u8,
362387
loaderId: ?[]const u8,

src/http/Client.zig

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,18 @@ const Method = Http.Method;
5050
// those other http requests.
5151
pub const Client = @This();
5252

53-
// count of active requests
53+
// Count of active requests
5454
active: usize,
5555

56+
// Count of intercepted requests. This is to help deal with intercepted requests.
57+
// The client doesn't track intercepted transfers. If a request is intercepted,
58+
// the client forgets about it and requires the interceptor to continue or abort
59+
// it. That works well, except if we only rely on active, we might think there's
60+
// no more network activity when, with interecepted requests, there might be more
61+
// in the future. (We really only need this to properly emit a 'networkIdle' and
62+
// 'networkAlmostIdle' Page.lifecycleEvent in CDP).
63+
intercepted: usize,
64+
5665
// curl has 2 APIs: easy and multi. Multi is like a combination of some I/O block
5766
// (e.g. epoll) and a bunch of pools. You add/remove easys to the multiple and
5867
// then poll the multi.
@@ -115,6 +124,7 @@ pub fn init(allocator: Allocator, ca_blob: ?c.curl_blob, opts: Http.Opts) !*Clie
115124
client.* = .{
116125
.queue = .{},
117126
.active = 0,
127+
.intercepted = 0,
118128
.multi = multi,
119129
.handles = handles,
120130
.blocking = blocking,
@@ -191,6 +201,10 @@ pub fn request(self: *Client, req: Request) !void {
191201
var wait_for_interception = false;
192202
notification.dispatch(.http_request_intercept, &.{ .transfer = transfer, .wait_for_interception = &wait_for_interception });
193203
if (wait_for_interception) {
204+
self.intercepted += 1;
205+
if (builtin.mode == .Debug) {
206+
transfer._intercepted = true;
207+
}
194208
// The user is send an invitation to intercept this request.
195209
return;
196210
}
@@ -200,16 +214,43 @@ pub fn request(self: *Client, req: Request) !void {
200214
}
201215

202216
// Above, request will not process if there's an interception request. In such
203-
// cases, the interecptor is expected to call process to continue the transfer
217+
// cases, the interecptor is expected to call resume to continue the transfer
204218
// or transfer.abort() to abort it.
205-
pub fn process(self: *Client, transfer: *Transfer) !void {
219+
fn process(self: *Client, transfer: *Transfer) !void {
206220
if (self.handles.getFreeHandle()) |handle| {
207221
return self.makeRequest(handle, transfer);
208222
}
209223

210224
self.queue.append(&transfer._node);
211225
}
212226

227+
// For an intercepted request
228+
pub fn continueTransfer(self: *Client, transfer: *Transfer) !void {
229+
if (builtin.mode == .Debug) {
230+
std.debug.assert(transfer._intercepted);
231+
}
232+
self.intercepted -= 1;
233+
return self.process(transfer);
234+
}
235+
236+
// For an intercepted request
237+
pub fn abortTransfer(self: *Client, transfer: *Transfer) void {
238+
if (builtin.mode == .Debug) {
239+
std.debug.assert(transfer._intercepted);
240+
}
241+
self.intercepted -= 1;
242+
transfer.abort();
243+
}
244+
245+
// For an intercepted request
246+
pub fn fulfillTransfer(self: *Client, transfer: *Transfer, status: u16, headers: []const Http.Header, body: ?[]const u8) !void {
247+
if (builtin.mode == .Debug) {
248+
std.debug.assert(transfer._intercepted);
249+
}
250+
self.intercepted -= 1;
251+
return transfer.fulfill(status, headers, body);
252+
}
253+
213254
// See ScriptManager.blockingGet
214255
pub fn blockingRequest(self: *Client, req: Request) !void {
215256
const transfer = try self.makeTransfer(req);
@@ -674,6 +715,7 @@ pub const Transfer = struct {
674715

675716
// for when a Transfer is queued in the client.queue
676717
_node: std.DoublyLinkedList.Node = .{},
718+
_intercepted: if (builtin.mode == .Debug) bool else void = if (builtin.mode == .Debug) false else {},
677719

678720
pub fn reset(self: *Transfer) void {
679721
self._redirecting = false;

src/notification.zig

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,9 @@ const List = std.DoublyLinkedList;
3232
// "scope". This would have a run-time cost and still require some coordination
3333
// between components to share a common scope.
3434
//
35-
// Instead, the approach that we take is to have a notification per
35+
// Instead, the approach that we take is to have a notification instance per
3636
// scope. This makes some things harder, but we only plan on having 2
37-
// notifications at a given time: one in a Browser and one in the App.
37+
// notification instances at a given time: one in a Browser and one in the App.
3838
// What about something like Telemetry, which lives outside of a Browser but
3939
// still cares about Browser-events (like .page_navigate)? When the Browser
4040
// notification is created, a `notification_created` event is raised in the
@@ -59,6 +59,8 @@ pub const Notification = struct {
5959
page_created: List = .{},
6060
page_navigate: List = .{},
6161
page_navigated: List = .{},
62+
page_network_idle: List = .{},
63+
page_network_almost_idle: List = .{},
6264
http_request_fail: List = .{},
6365
http_request_start: List = .{},
6466
http_request_intercept: List = .{},
@@ -74,6 +76,8 @@ pub const Notification = struct {
7476
page_created: *page.Page,
7577
page_navigate: *const PageNavigate,
7678
page_navigated: *const PageNavigated,
79+
page_network_idle: *const PageNetworkIdle,
80+
page_network_almost_idle: *const PageNetworkAlmostIdle,
7781
http_request_fail: *const RequestFail,
7882
http_request_start: *const RequestStart,
7983
http_request_intercept: *const RequestIntercept,
@@ -98,6 +102,14 @@ pub const Notification = struct {
98102
url: []const u8,
99103
};
100104

105+
pub const PageNetworkIdle = struct {
106+
timestamp: u32,
107+
};
108+
109+
pub const PageNetworkAlmostIdle = struct {
110+
timestamp: u32,
111+
};
112+
101113
pub const RequestStart = struct {
102114
transfer: *Transfer,
103115
};

0 commit comments

Comments
 (0)