mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-10-29 23:23:28 +00:00
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).
This commit is contained in:
@@ -65,14 +65,11 @@ pub fn add(self: *Scheduler, ctx: *anyopaque, func: Task.Func, ms: u32, opts: Ad
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn runHighPriority(self: *Scheduler) !?i32 {
|
pub fn run(self: *Scheduler) !?i32 {
|
||||||
|
_ = try self.runQueue(&self.low_priority);
|
||||||
return self.runQueue(&self.high_priority);
|
return self.runQueue(&self.high_priority);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn runLowPriority(self: *Scheduler) !?i32 {
|
|
||||||
return self.runQueue(&self.low_priority);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn runQueue(self: *Scheduler, queue: *Queue) !?i32 {
|
fn runQueue(self: *Scheduler, queue: *Queue) !?i32 {
|
||||||
// this is O(1)
|
// this is O(1)
|
||||||
if (queue.count() == 0) {
|
if (queue.count() == 0) {
|
||||||
@@ -127,33 +124,24 @@ test "Scheduler" {
|
|||||||
var task = TestTask{ .allocator = testing.arena_allocator };
|
var task = TestTask{ .allocator = testing.arena_allocator };
|
||||||
|
|
||||||
var s = Scheduler.init(testing.arena_allocator);
|
var s = Scheduler.init(testing.arena_allocator);
|
||||||
try testing.expectEqual(null, s.runHighPriority());
|
try testing.expectEqual(null, s.run());
|
||||||
try testing.expectEqual(0, task.calls.items.len);
|
try testing.expectEqual(0, task.calls.items.len);
|
||||||
|
|
||||||
try s.add(&task, TestTask.run1, 3, .{});
|
try s.add(&task, TestTask.run1, 3, .{});
|
||||||
|
|
||||||
try testing.expectDelta(3, try s.runHighPriority(), 1);
|
try testing.expectDelta(3, try s.run(), 1);
|
||||||
try testing.expectEqual(0, task.calls.items.len);
|
try testing.expectEqual(0, task.calls.items.len);
|
||||||
|
|
||||||
std.Thread.sleep(std.time.ns_per_ms * 5);
|
std.Thread.sleep(std.time.ns_per_ms * 5);
|
||||||
try testing.expectEqual(null, s.runHighPriority());
|
try testing.expectEqual(null, s.run());
|
||||||
try testing.expectEqualSlices(u32, &.{1}, task.calls.items);
|
try testing.expectEqualSlices(u32, &.{1}, task.calls.items);
|
||||||
|
|
||||||
try s.add(&task, TestTask.run2, 3, .{});
|
try s.add(&task, TestTask.run2, 3, .{});
|
||||||
try s.add(&task, TestTask.run1, 2, .{});
|
try s.add(&task, TestTask.run1, 2, .{});
|
||||||
|
|
||||||
std.Thread.sleep(std.time.ns_per_ms * 5);
|
std.Thread.sleep(std.time.ns_per_ms * 5);
|
||||||
try testing.expectDelta(null, try s.runHighPriority(), 1);
|
try testing.expectDelta(null, try s.run(), 1);
|
||||||
try testing.expectEqualSlices(u32, &.{ 1, 1, 2 }, task.calls.items);
|
try testing.expectEqualSlices(u32, &.{ 1, 1, 2 }, task.calls.items);
|
||||||
|
|
||||||
std.Thread.sleep(std.time.ns_per_ms * 5);
|
|
||||||
// won't run low_priority
|
|
||||||
try testing.expectEqual(null, try s.runHighPriority());
|
|
||||||
try testing.expectEqualSlices(u32, &.{ 1, 1, 2 }, task.calls.items);
|
|
||||||
|
|
||||||
//runs low_priority
|
|
||||||
try testing.expectDelta(2, try s.runLowPriority(), 1);
|
|
||||||
try testing.expectEqualSlices(u32, &.{ 1, 1, 2, 2 }, task.calls.items);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const TestTask = struct {
|
const TestTask = struct {
|
||||||
|
|||||||
@@ -90,10 +90,8 @@ pub const Page = struct {
|
|||||||
|
|
||||||
load_state: LoadState = .parsing,
|
load_state: LoadState = .parsing,
|
||||||
|
|
||||||
// We should only emit these events once per page. To make sure of that, we
|
notified_network_idle: IdleNotification = .init,
|
||||||
// track whether or not we've already emitted the notifications.
|
notified_network_almost_idle: IdleNotification = .init,
|
||||||
notified_network_idle: bool = false,
|
|
||||||
notified_network_almost_idle: bool = false,
|
|
||||||
|
|
||||||
const Mode = union(enum) {
|
const Mode = union(enum) {
|
||||||
pre: void,
|
pre: void,
|
||||||
@@ -325,8 +323,7 @@ pub const Page = struct {
|
|||||||
// scheduler.run could trigger new http transfers, so do not
|
// scheduler.run could trigger new http transfers, so do not
|
||||||
// store http_client.active BEFORE this call and then use
|
// store http_client.active BEFORE this call and then use
|
||||||
// it AFTER.
|
// it AFTER.
|
||||||
const ms_to_next_task = try scheduler.runHighPriority();
|
const ms_to_next_task = try scheduler.run();
|
||||||
_ = try scheduler.runLowPriority();
|
|
||||||
|
|
||||||
if (try_catch.hasCaught()) {
|
if (try_catch.hasCaught()) {
|
||||||
const msg = (try try_catch.err(self.arena)) orelse "unknown";
|
const msg = (try try_catch.err(self.arena)) orelse "unknown";
|
||||||
@@ -335,6 +332,14 @@ pub const Page = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const http_active = http_client.active;
|
const http_active = http_client.active;
|
||||||
|
const total_network_activity = http_active + http_client.intercepted;
|
||||||
|
if (self.notified_network_almost_idle.check(total_network_activity <= 2)) {
|
||||||
|
self.notifyNetworkAlmostIdle();
|
||||||
|
}
|
||||||
|
if (self.notified_network_idle.check(total_network_activity == 0)) {
|
||||||
|
self.notifyNetworkIdle();
|
||||||
|
}
|
||||||
|
|
||||||
if (http_active == 0 and exit_when_done) {
|
if (http_active == 0 and exit_when_done) {
|
||||||
// we don't need to consider http_client.intercepted here
|
// we don't need to consider http_client.intercepted here
|
||||||
// because exit_when_done is true, and that can only be
|
// because exit_when_done is true, and that can only be
|
||||||
@@ -359,7 +364,8 @@ pub const Page = struct {
|
|||||||
|
|
||||||
if (ms > ms_remaining) {
|
if (ms > ms_remaining) {
|
||||||
// Same as above, except we have a scheduled task,
|
// Same as above, except we have a scheduled task,
|
||||||
// it just happens to be too far into the future.
|
// it just happens to be too far into the future
|
||||||
|
// compared to how long we were told to wait.
|
||||||
return .done;
|
return .done;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -371,9 +377,6 @@ pub const Page = struct {
|
|||||||
// loop to see if anything new can be processed.
|
// loop to see if anything new can be processed.
|
||||||
std.Thread.sleep(std.time.ns_per_ms * @as(u64, @intCast(@min(ms, 20))));
|
std.Thread.sleep(std.time.ns_per_ms * @as(u64, @intCast(@min(ms, 20))));
|
||||||
} else {
|
} else {
|
||||||
if (self.notified_network_idle == false and http_active == 0 and http_client.intercepted == 0) {
|
|
||||||
self.notifyNetworkIdle();
|
|
||||||
}
|
|
||||||
// We're here because we either have active HTTP
|
// We're here because we either have active HTTP
|
||||||
// connections, or exit_when_done == false (aka, there's
|
// connections, or exit_when_done == false (aka, there's
|
||||||
// an extra_socket registered with the http client).
|
// an extra_socket registered with the http client).
|
||||||
@@ -484,25 +487,14 @@ pub const Page = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn notifyNetworkIdle(self: *Page) void {
|
fn notifyNetworkIdle(self: *Page) void {
|
||||||
// caller should always check that we haven't sent this already;
|
std.debug.assert(self.notified_network_idle == .done);
|
||||||
std.debug.assert(self.notified_network_idle == false);
|
|
||||||
|
|
||||||
// if we're going to send networkIdle, we should first send networkAlmostIdle
|
|
||||||
// if it hasn't already been sent.
|
|
||||||
if (self.notified_network_almost_idle == false) {
|
|
||||||
self.notifyNetworkAlmostIdle();
|
|
||||||
}
|
|
||||||
|
|
||||||
self.notified_network_idle = true;
|
|
||||||
self.session.browser.notification.dispatch(.page_network_idle, &.{
|
self.session.browser.notification.dispatch(.page_network_idle, &.{
|
||||||
.timestamp = timestamp(),
|
.timestamp = timestamp(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn notifyNetworkAlmostIdle(self: *Page) void {
|
fn notifyNetworkAlmostIdle(self: *Page) void {
|
||||||
// caller should always check that we haven't sent this already;
|
std.debug.assert(self.notified_network_almost_idle == .done);
|
||||||
std.debug.assert(self.notified_network_almost_idle == false);
|
|
||||||
self.notified_network_almost_idle = true;
|
|
||||||
self.session.browser.notification.dispatch(.page_network_almost_idle, &.{
|
self.session.browser.notification.dispatch(.page_network_almost_idle, &.{
|
||||||
.timestamp = timestamp(),
|
.timestamp = timestamp(),
|
||||||
});
|
});
|
||||||
@@ -1140,9 +1132,82 @@ pub const NavigateOpts = struct {
|
|||||||
header: ?[:0]const u8 = null,
|
header: ?[:0]const u8 = null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const IdleNotification = union(enum) {
|
||||||
|
// hasn't started yet.
|
||||||
|
init,
|
||||||
|
|
||||||
|
// timestamp where the state was first triggered. If the state stays
|
||||||
|
// true (e.g. 0 nework activity for NetworkIdle, or <= 2 for NetworkAlmostIdle)
|
||||||
|
// for 500ms, it'll send the notification and transition to .done. If
|
||||||
|
// the state doesn't stay true, it'll revert to .init.
|
||||||
|
triggered: u64,
|
||||||
|
|
||||||
|
// notification sent - should never be reset
|
||||||
|
done,
|
||||||
|
|
||||||
|
// Returns `true` if we should send a notification. Only returns true if it
|
||||||
|
// was previously triggered 500+ milliseconds ago.
|
||||||
|
// active == true when the condition for the notification is true
|
||||||
|
// active == false when the condition for the notification is false
|
||||||
|
pub fn check(self: *IdleNotification, active: bool) bool {
|
||||||
|
if (active) {
|
||||||
|
switch (self.*) {
|
||||||
|
.done => {
|
||||||
|
// Notification was already sent.
|
||||||
|
},
|
||||||
|
.init => {
|
||||||
|
// This is the first time the condition was triggered (or
|
||||||
|
// the first time after being un-triggered). Record the time
|
||||||
|
// so that if the condition holds for long enough, we can
|
||||||
|
// send a notification.
|
||||||
|
self.* = .{ .triggered = milliTimestamp() };
|
||||||
|
},
|
||||||
|
.triggered => |ms| {
|
||||||
|
// The condition was already triggered and was triggered
|
||||||
|
// again. When this condition holds for 500+ms, we'll send
|
||||||
|
// a notification.
|
||||||
|
if (milliTimestamp() - ms >= 500) {
|
||||||
|
// This is the only place in this function where we can
|
||||||
|
// return true. The only place where we can tell our caller
|
||||||
|
// "send the notification!".
|
||||||
|
self.* = .done;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// the state hasn't held for 500ms.
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch (self.*) {
|
||||||
|
.done => {
|
||||||
|
// The condition became false, but we already sent the notification
|
||||||
|
// There's nothing we can do, it stays .done. We never re-send
|
||||||
|
// a notification or "undo" a sent notification (not that we can).
|
||||||
|
},
|
||||||
|
.init => {
|
||||||
|
// The condition remains false
|
||||||
|
},
|
||||||
|
.triggered => {
|
||||||
|
// The condition _had_ been true, and we were waiting (500ms)
|
||||||
|
// for it to hold, but it hasn't. So we go back to waiting.
|
||||||
|
self.* = .init;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// See above for the only case where we ever return true. All other
|
||||||
|
// paths go here. This means "don't send the notification". Maybe
|
||||||
|
// because it's already been sent, maybe because active is false, or
|
||||||
|
// maybe because the condition hasn't held long enough.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
fn timestamp() u32 {
|
fn timestamp() u32 {
|
||||||
return @import("../datetime.zig").timestamp();
|
return @import("../datetime.zig").timestamp();
|
||||||
}
|
}
|
||||||
|
fn milliTimestamp() u64 {
|
||||||
|
return @import("../datetime.zig").milliTimestamp();
|
||||||
|
}
|
||||||
|
|
||||||
// A callback from libdom whenever a script tag is added to the DOM.
|
// A callback from libdom whenever a script tag is added to the DOM.
|
||||||
// element is guaranteed to be a script element.
|
// element is guaranteed to be a script element.
|
||||||
|
|||||||
Reference in New Issue
Block a user