Skip to content

Commit 9971de2

Browse files
committed
Send NetworkIdle and NetworkAlmostIdle notifications after 500ms delay
Like Chrome, the NetworkIdle and NetworkAlmostIdle will only be sent if the condition (no network requests / <= 2 network requests) holds for at least 500ms Also merged runHighPriority and runLowPriority as they are now always run together (but we still only block/wait for high priority tasks).
1 parent 1e738dc commit 9971de2

File tree

2 files changed

+94
-41
lines changed

2 files changed

+94
-41
lines changed

src/browser/Scheduler.zig

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -65,14 +65,11 @@ pub fn add(self: *Scheduler, ctx: *anyopaque, func: Task.Func, ms: u32, opts: Ad
6565
});
6666
}
6767

68-
pub fn runHighPriority(self: *Scheduler) !?i32 {
68+
pub fn run(self: *Scheduler) !?i32 {
69+
_ = try self.runQueue(&self.low_priority);
6970
return self.runQueue(&self.high_priority);
7071
}
7172

72-
pub fn runLowPriority(self: *Scheduler) !?i32 {
73-
return self.runQueue(&self.low_priority);
74-
}
75-
7673
fn runQueue(self: *Scheduler, queue: *Queue) !?i32 {
7774
// this is O(1)
7875
if (queue.count() == 0) {
@@ -127,33 +124,24 @@ test "Scheduler" {
127124
var task = TestTask{ .allocator = testing.arena_allocator };
128125

129126
var s = Scheduler.init(testing.arena_allocator);
130-
try testing.expectEqual(null, s.runHighPriority());
127+
try testing.expectEqual(null, s.run());
131128
try testing.expectEqual(0, task.calls.items.len);
132129

133130
try s.add(&task, TestTask.run1, 3, .{});
134131

135-
try testing.expectDelta(3, try s.runHighPriority(), 1);
132+
try testing.expectDelta(3, try s.run(), 1);
136133
try testing.expectEqual(0, task.calls.items.len);
137134

138135
std.Thread.sleep(std.time.ns_per_ms * 5);
139-
try testing.expectEqual(null, s.runHighPriority());
136+
try testing.expectEqual(null, s.run());
140137
try testing.expectEqualSlices(u32, &.{1}, task.calls.items);
141138

142139
try s.add(&task, TestTask.run2, 3, .{});
143140
try s.add(&task, TestTask.run1, 2, .{});
144141

145142
std.Thread.sleep(std.time.ns_per_ms * 5);
146-
try testing.expectDelta(null, try s.runHighPriority(), 1);
147-
try testing.expectEqualSlices(u32, &.{ 1, 1, 2 }, task.calls.items);
148-
149-
std.Thread.sleep(std.time.ns_per_ms * 5);
150-
// won't run low_priority
151-
try testing.expectEqual(null, try s.runHighPriority());
143+
try testing.expectDelta(null, try s.run(), 1);
152144
try testing.expectEqualSlices(u32, &.{ 1, 1, 2 }, task.calls.items);
153-
154-
//runs low_priority
155-
try testing.expectDelta(2, try s.runLowPriority(), 1);
156-
try testing.expectEqualSlices(u32, &.{ 1, 1, 2, 2 }, task.calls.items);
157145
}
158146

159147
const TestTask = struct {

src/browser/page.zig

Lines changed: 88 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,8 @@ 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,
93+
notified_network_idle: IdleNotification = .init,
94+
notified_network_almost_idle: IdleNotification = .init,
9795

9896
const Mode = union(enum) {
9997
pre: void,
@@ -325,8 +323,7 @@ pub const Page = struct {
325323
// scheduler.run could trigger new http transfers, so do not
326324
// store http_client.active BEFORE this call and then use
327325
// it AFTER.
328-
const ms_to_next_task = try scheduler.runHighPriority();
329-
_ = try scheduler.runLowPriority();
326+
const ms_to_next_task = try scheduler.run();
330327

331328
if (try_catch.hasCaught()) {
332329
const msg = (try try_catch.err(self.arena)) orelse "unknown";
@@ -335,6 +332,14 @@ pub const Page = struct {
335332
}
336333

337334
const http_active = http_client.active;
335+
const total_network_activity = http_active + http_client.intercepted;
336+
if (self.notified_network_almost_idle.check(total_network_activity <= 2)) {
337+
self.notifyNetworkAlmostIdle();
338+
}
339+
if (self.notified_network_idle.check(total_network_activity == 0)) {
340+
self.notifyNetworkIdle();
341+
}
342+
338343
if (http_active == 0 and exit_when_done) {
339344
// we don't need to consider http_client.intercepted here
340345
// because exit_when_done is true, and that can only be
@@ -359,7 +364,8 @@ pub const Page = struct {
359364

360365
if (ms > ms_remaining) {
361366
// Same as above, except we have a scheduled task,
362-
// it just happens to be too far into the future.
367+
// it just happens to be too far into the future
368+
// compared to how long we were told to wait.
363369
return .done;
364370
}
365371

@@ -371,9 +377,6 @@ pub const Page = struct {
371377
// loop to see if anything new can be processed.
372378
std.Thread.sleep(std.time.ns_per_ms * @as(u64, @intCast(@min(ms, 20))));
373379
} else {
374-
if (self.notified_network_idle == false and http_active == 0 and http_client.intercepted == 0) {
375-
self.notifyNetworkIdle();
376-
}
377380
// We're here because we either have active HTTP
378381
// connections, or exit_when_done == false (aka, there's
379382
// an extra_socket registered with the http client).
@@ -484,25 +487,14 @@ pub const Page = struct {
484487
}
485488

486489
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;
490+
std.debug.assert(self.notified_network_idle == .done);
497491
self.session.browser.notification.dispatch(.page_network_idle, &.{
498492
.timestamp = timestamp(),
499493
});
500494
}
501495

502496
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;
497+
std.debug.assert(self.notified_network_almost_idle == .done);
506498
self.session.browser.notification.dispatch(.page_network_almost_idle, &.{
507499
.timestamp = timestamp(),
508500
});
@@ -1140,9 +1132,82 @@ pub const NavigateOpts = struct {
11401132
header: ?[:0]const u8 = null,
11411133
};
11421134

1135+
const IdleNotification = union(enum) {
1136+
// hasn't started yet.
1137+
init,
1138+
1139+
// timestamp where the state was first triggered. If the state stays
1140+
// true (e.g. 0 nework activity for NetworkIdle, or <= 2 for NetworkAlmostIdle)
1141+
// for 500ms, it'll send the notification and transition to .done. If
1142+
// the state doesn't stay true, it'll revert to .init.
1143+
triggered: u64,
1144+
1145+
// notification sent - should never be reset
1146+
done,
1147+
1148+
// Returns `true` if we should send a notification. Only returns true if it
1149+
// was previously triggered 500+ milliseconds ago.
1150+
// active == true when the condition for the notification is true
1151+
// active == false when the condition for the notification is false
1152+
pub fn check(self: *IdleNotification, active: bool) bool {
1153+
if (active) {
1154+
switch (self.*) {
1155+
.done => {
1156+
// Notification was already sent.
1157+
},
1158+
.init => {
1159+
// This is the first time the condition was triggered (or
1160+
// the first time after being un-triggered). Record the time
1161+
// so that if the condition holds for long enough, we can
1162+
// send a notification.
1163+
self.* = .{ .triggered = milliTimestamp() };
1164+
},
1165+
.triggered => |ms| {
1166+
// The condition was already triggered and was triggered
1167+
// again. When this condition holds for 500+ms, we'll send
1168+
// a notification.
1169+
if (milliTimestamp() - ms >= 500) {
1170+
// This is the only place in this function where we can
1171+
// return true. The only place where we can tell our caller
1172+
// "send the notification!".
1173+
self.* = .done;
1174+
return true;
1175+
}
1176+
// the state hasn't held for 500ms.
1177+
},
1178+
}
1179+
} else {
1180+
switch (self.*) {
1181+
.done => {
1182+
// The condition became false, but we already sent the notification
1183+
// There's nothing we can do, it stays .done. We never re-send
1184+
// a notification or "undo" a sent notification (not that we can).
1185+
},
1186+
.init => {
1187+
// The condition remains false
1188+
},
1189+
.triggered => {
1190+
// The condition _had_ been true, and we were waiting (500ms)
1191+
// for it to hold, but it hasn't. So we go back to waiting.
1192+
self.* = .init;
1193+
},
1194+
}
1195+
}
1196+
1197+
// See above for the only case where we ever return true. All other
1198+
// paths go here. This means "don't send the notification". Maybe
1199+
// because it's already been sent, maybe because active is false, or
1200+
// maybe because the condition hasn't held long enough.
1201+
return false;
1202+
}
1203+
};
1204+
11431205
fn timestamp() u32 {
11441206
return @import("../datetime.zig").timestamp();
11451207
}
1208+
fn milliTimestamp() u64 {
1209+
return @import("../datetime.zig").milliTimestamp();
1210+
}
11461211

11471212
// A callback from libdom whenever a script tag is added to the DOM.
11481213
// element is guaranteed to be a script element.

0 commit comments

Comments
 (0)