mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-10-29 23:23:28 +00:00
add custom scheduler
This commit is contained in:
173
src/browser/Scheduler.zig
Normal file
173
src/browser/Scheduler.zig
Normal file
@@ -0,0 +1,173 @@
|
||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const log = @import("../log.zig");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const Scheduler = @This();
|
||||
|
||||
primary: Queue,
|
||||
|
||||
// For repeating tasks. We only want to run these if there are other things to
|
||||
// do. We don't, for example, want a window.setInterval or the page.runMicrotasks
|
||||
// to block the page.wait.
|
||||
secondary: Queue,
|
||||
|
||||
// we expect allocator to be the page arena, hence we never call primary.deinit
|
||||
pub fn init(allocator: Allocator) Scheduler {
|
||||
return .{
|
||||
.primary = Queue.init(allocator, {}),
|
||||
.secondary = Queue.init(allocator, {}),
|
||||
};
|
||||
}
|
||||
|
||||
const AddOpts = struct {
|
||||
name: []const u8 = "",
|
||||
};
|
||||
pub fn add(self: *Scheduler, ctx: *anyopaque, func: Task.Func, ms: u32, opts: AddOpts) !void {
|
||||
if (ms > 5_000) {
|
||||
log.warn(.user_script, "long timeout ignored", .{ .delay = ms });
|
||||
// ignore any task that we're almost certainly never going to run
|
||||
return;
|
||||
}
|
||||
return self.primary.add(.{
|
||||
.ms = std.time.milliTimestamp() + ms,
|
||||
.ctx = ctx,
|
||||
.func = func,
|
||||
.name = opts.name,
|
||||
});
|
||||
}
|
||||
|
||||
// fn debug(self: *Scheduler) void {
|
||||
// var it = self.primary.iterator();
|
||||
// while (it.next()) |task| {
|
||||
// std.debug.print("- {s}\n", .{task.name});
|
||||
// }
|
||||
// }
|
||||
|
||||
pub fn run(self: *Scheduler, force_secondary: bool) !?u32 {
|
||||
if (self.primary.count() == 0 and force_secondary == false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const now = std.time.milliTimestamp();
|
||||
const time_to_next_primary = try self.runQueue(&self.primary, now);
|
||||
const time_to_next_secondary = try self.runQueue(&self.secondary, now);
|
||||
|
||||
if (time_to_next_primary == null) {
|
||||
return time_to_next_secondary;
|
||||
}
|
||||
if (time_to_next_secondary == null) {
|
||||
return time_to_next_primary;
|
||||
}
|
||||
return @min(time_to_next_primary.?, time_to_next_secondary.?);
|
||||
}
|
||||
|
||||
fn runQueue(self: *Scheduler, queue: *Queue, now: i64) !?u32 {
|
||||
var next = queue.peek();
|
||||
while (next) |task| {
|
||||
const time_to_next = task.ms - now;
|
||||
if (time_to_next > 0) {
|
||||
// @intCast is petty safe since we limit tasks to just 5 seconds
|
||||
// in the future
|
||||
return @intCast(time_to_next);
|
||||
}
|
||||
|
||||
if (task.func(task.ctx)) |repeat_delay| {
|
||||
// if we do (now + 0) then our WHILE loop will run endlessly.
|
||||
// no task should ever return 0
|
||||
std.debug.assert(repeat_delay != 0);
|
||||
|
||||
var copy = task;
|
||||
copy.ms = now + repeat_delay;
|
||||
try self.secondary.add(copy);
|
||||
}
|
||||
_ = queue.remove();
|
||||
next = queue.peek();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const Task = struct {
|
||||
ms: i64,
|
||||
func: Func,
|
||||
ctx: *anyopaque,
|
||||
name: []const u8,
|
||||
|
||||
const Func = *const fn (ctx: *anyopaque) ?u32;
|
||||
};
|
||||
|
||||
const Queue = std.PriorityQueue(Task, void, struct {
|
||||
fn compare(_: void, a: Task, b: Task) std.math.Order {
|
||||
return std.math.order(a.ms, b.ms);
|
||||
}
|
||||
}.compare);
|
||||
|
||||
const testing = @import("../testing.zig");
|
||||
test "Scheduler" {
|
||||
defer testing.reset();
|
||||
|
||||
var task = TestTask{ .allocator = testing.arena_allocator };
|
||||
|
||||
var s = Scheduler.init(testing.arena_allocator);
|
||||
try testing.expectEqual(null, s.run(false));
|
||||
try testing.expectEqual(0, task.calls.items.len);
|
||||
|
||||
try s.add(&task, TestTask.run1, 3, .{});
|
||||
|
||||
try testing.expectDelta(3, try s.run(false), 1);
|
||||
try testing.expectEqual(0, task.calls.items.len);
|
||||
|
||||
std.time.sleep(std.time.ns_per_ms * 5);
|
||||
try testing.expectEqual(null, s.run(false));
|
||||
try testing.expectEqualSlices(u32, &.{1}, task.calls.items);
|
||||
|
||||
try s.add(&task, TestTask.run2, 3, .{});
|
||||
try s.add(&task, TestTask.run1, 2, .{});
|
||||
|
||||
std.time.sleep(std.time.ns_per_ms * 5);
|
||||
try testing.expectDelta(2, try s.run(false), 1);
|
||||
try testing.expectEqualSlices(u32, &.{ 1, 1, 2 }, task.calls.items);
|
||||
|
||||
std.time.sleep(std.time.ns_per_ms * 5);
|
||||
// only secondary won't be run unless forced
|
||||
try testing.expectEqual(null, try s.run(false));
|
||||
try testing.expectEqualSlices(u32, &.{ 1, 1, 2 }, task.calls.items);
|
||||
|
||||
// only secondary will be run when forced
|
||||
try testing.expectDelta(2, try s.run(true), 1);
|
||||
try testing.expectEqualSlices(u32, &.{ 1, 1, 2, 2 }, task.calls.items);
|
||||
}
|
||||
|
||||
const TestTask = struct {
|
||||
allocator: Allocator,
|
||||
calls: std.ArrayListUnmanaged(u32) = .{},
|
||||
|
||||
fn run1(ctx: *anyopaque) ?u32 {
|
||||
var self: *TestTask = @alignCast(@ptrCast(ctx));
|
||||
self.calls.append(self.allocator, 1) catch unreachable;
|
||||
return null;
|
||||
}
|
||||
|
||||
fn run2(ctx: *anyopaque) ?u32 {
|
||||
var self: *TestTask = @alignCast(@ptrCast(ctx));
|
||||
self.calls.append(self.allocator, 2) catch unreachable;
|
||||
return 2;
|
||||
}
|
||||
};
|
||||
@@ -18,7 +18,6 @@
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
|
||||
const log = @import("../log.zig");
|
||||
const parser = @import("netsurf.zig");
|
||||
const http = @import("../http/client.zig");
|
||||
@@ -250,8 +249,7 @@ fn evaluate(self: *ScriptManager) void {
|
||||
|
||||
fn asyncDone(self: *ScriptManager) void {
|
||||
self.async_count -= 1;
|
||||
if (
|
||||
self.async_count == 0 and // there are no more async scripts
|
||||
if (self.async_count == 0 and // there are no more async scripts
|
||||
self.static_scripts_done and // and we've finished parsing the HTML to queue all <scripts>
|
||||
self.scripts.first == null and // and there are no more <script src=> to wait for
|
||||
self.deferred.first == null // and there are no more <script defer src=> to wait for
|
||||
@@ -380,7 +378,6 @@ const PendingScript = struct {
|
||||
log.warn(.http, "script fetch error", .{ .req = transfer, .err = err });
|
||||
self.deinit();
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
const Script = struct {
|
||||
|
||||
@@ -22,7 +22,6 @@ const Allocator = std.mem.Allocator;
|
||||
const log = @import("../../log.zig");
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Page = @import("../page.zig").Page;
|
||||
const Loop = @import("../../runtime/loop.zig").Loop;
|
||||
|
||||
const Env = @import("../env.zig").Env;
|
||||
const NodeList = @import("nodelist.zig").NodeList;
|
||||
@@ -36,12 +35,10 @@ const Walker = @import("../dom/walker.zig").WalkerChildren;
|
||||
|
||||
// WEB IDL https://dom.spec.whatwg.org/#interface-mutationobserver
|
||||
pub const MutationObserver = struct {
|
||||
loop: *Loop,
|
||||
page: *Page,
|
||||
cbk: Env.Function,
|
||||
arena: Allocator,
|
||||
connected: bool,
|
||||
scheduled: bool,
|
||||
loop_node: Loop.CallbackNode,
|
||||
|
||||
// List of records which were observed. When the call scope ends, we need to
|
||||
// execute our callback with it.
|
||||
@@ -50,17 +47,15 @@ pub const MutationObserver = struct {
|
||||
pub fn constructor(cbk: Env.Function, page: *Page) !MutationObserver {
|
||||
return .{
|
||||
.cbk = cbk,
|
||||
.loop = page.loop,
|
||||
.page = page,
|
||||
.observed = .{},
|
||||
.connected = true,
|
||||
.scheduled = false,
|
||||
.arena = page.arena,
|
||||
.loop_node = .{ .func = callback },
|
||||
};
|
||||
}
|
||||
|
||||
pub fn _observe(self: *MutationObserver, node: *parser.Node, options_: ?Options) !void {
|
||||
const arena = self.arena;
|
||||
const arena = self.page.arena;
|
||||
var options = options_ orelse Options{};
|
||||
if (options.attributeFilter.len > 0) {
|
||||
options.attributeFilter = try arena.dupe([]const u8, options.attributeFilter);
|
||||
@@ -115,17 +110,17 @@ pub const MutationObserver = struct {
|
||||
}
|
||||
}
|
||||
|
||||
fn callback(node: *Loop.CallbackNode, _: *?u63) void {
|
||||
const self: *MutationObserver = @fieldParentPtr("loop_node", node);
|
||||
fn callback(ctx: *anyopaque) ?u32 {
|
||||
const self: *MutationObserver = @alignCast(@ptrCast(ctx));
|
||||
if (self.connected == false) {
|
||||
self.scheduled = true;
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
self.scheduled = false;
|
||||
|
||||
const records = self.observed.items;
|
||||
if (records.len == 0) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
defer self.observed.clearRetainingCapacity();
|
||||
@@ -138,6 +133,7 @@ pub const MutationObserver = struct {
|
||||
.source = "mutation observer",
|
||||
});
|
||||
};
|
||||
return null;
|
||||
}
|
||||
|
||||
// TODO
|
||||
@@ -301,7 +297,7 @@ const Observer = struct {
|
||||
.type = event_type.recordType(),
|
||||
};
|
||||
|
||||
const arena = mutation_observer.arena;
|
||||
const arena = mutation_observer.page.arena;
|
||||
switch (event_type) {
|
||||
.DOMAttrModified => {
|
||||
record.attribute_name = parser.mutationEventAttributeName(mutation_event) catch null;
|
||||
@@ -330,7 +326,12 @@ const Observer = struct {
|
||||
|
||||
if (mutation_observer.scheduled == false) {
|
||||
mutation_observer.scheduled = true;
|
||||
_ = try mutation_observer.loop.timeout(0, &mutation_observer.loop_node);
|
||||
try mutation_observer.page.scheduler.add(
|
||||
mutation_observer,
|
||||
MutationObserver.callback,
|
||||
0,
|
||||
.{ .name = "mutation_observer" },
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -21,7 +21,6 @@ const log = @import("../../log.zig");
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Env = @import("../env.zig").Env;
|
||||
const Page = @import("../page.zig").Page;
|
||||
const Loop = @import("../../runtime/loop.zig").Loop;
|
||||
const EventTarget = @import("../dom/event_target.zig").EventTarget;
|
||||
|
||||
pub const Interfaces = .{
|
||||
@@ -77,11 +76,9 @@ pub const AbortSignal = struct {
|
||||
const callback = try page.arena.create(TimeoutCallback);
|
||||
callback.* = .{
|
||||
.signal = .init,
|
||||
.node = .{ .func = TimeoutCallback.run },
|
||||
};
|
||||
|
||||
const delay_ms: u63 = @as(u63, delay) * std.time.ns_per_ms;
|
||||
_ = try page.loop.timeout(delay_ms, &callback.node);
|
||||
try page.scheduler.add(callback, TimeoutCallback.run, delay, .{ .name = "abort_signal" });
|
||||
return &callback.signal;
|
||||
}
|
||||
|
||||
@@ -131,15 +128,12 @@ pub const AbortSignal = struct {
|
||||
const TimeoutCallback = struct {
|
||||
signal: AbortSignal,
|
||||
|
||||
// This is the internal data that the event loop tracks. We'll get this
|
||||
// back in run and, from it, can get our TimeoutCallback instance
|
||||
node: Loop.CallbackNode = undefined,
|
||||
|
||||
fn run(node: *Loop.CallbackNode, _: *?u63) void {
|
||||
const self: *TimeoutCallback = @fieldParentPtr("node", node);
|
||||
fn run(ctx: *anyopaque) ?u32 {
|
||||
const self: *TimeoutCallback = @alignCast(@ptrCast(ctx));
|
||||
self.signal.abort("TimeoutError") catch |err| {
|
||||
log.warn(.app, "abort signal timeout", .{ .err = err });
|
||||
};
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@ const log = @import("../../log.zig");
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Env = @import("../env.zig").Env;
|
||||
const Page = @import("../page.zig").Page;
|
||||
const Loop = @import("../../runtime/loop.zig").Loop;
|
||||
|
||||
const Navigator = @import("navigator.zig").Navigator;
|
||||
const History = @import("history.zig").History;
|
||||
@@ -57,7 +56,7 @@ pub const Window = struct {
|
||||
|
||||
// counter for having unique timer ids
|
||||
timer_id: u30 = 0,
|
||||
timers: std.AutoHashMapUnmanaged(u32, *TimerCallback) = .{},
|
||||
timers: std.AutoHashMapUnmanaged(u32, void) = .{},
|
||||
|
||||
crypto: Crypto = .{},
|
||||
console: Console = .{},
|
||||
@@ -179,34 +178,31 @@ pub const Window = struct {
|
||||
}
|
||||
|
||||
pub fn _requestAnimationFrame(self: *Window, cbk: Function, page: *Page) !u32 {
|
||||
return self.createTimeout(cbk, 5, page, .{ .animation_frame = true });
|
||||
return self.createTimeout(cbk, 5, page, .{ .animation_frame = true, .name = "animationFrame" });
|
||||
}
|
||||
|
||||
pub fn _cancelAnimationFrame(self: *Window, id: u32, page: *Page) !void {
|
||||
const kv = self.timers.fetchRemove(id) orelse return;
|
||||
return page.loop.cancel(kv.value.loop_id);
|
||||
pub fn _cancelAnimationFrame(self: *Window, id: u32) !void {
|
||||
_ = self.timers.remove(id);
|
||||
}
|
||||
|
||||
pub fn _setTimeout(self: *Window, cbk: Function, delay: ?u32, params: []Env.JsObject, page: *Page) !u32 {
|
||||
return self.createTimeout(cbk, delay, page, .{ .args = params });
|
||||
return self.createTimeout(cbk, delay, page, .{ .args = params, .name = "setTimeout" });
|
||||
}
|
||||
|
||||
pub fn _setInterval(self: *Window, cbk: Function, delay: ?u32, params: []Env.JsObject, page: *Page) !u32 {
|
||||
return self.createTimeout(cbk, delay, page, .{ .repeat = true, .args = params });
|
||||
return self.createTimeout(cbk, delay, page, .{ .repeat = true, .args = params, .name = "setInterval" });
|
||||
}
|
||||
|
||||
pub fn _clearTimeout(self: *Window, id: u32, page: *Page) !void {
|
||||
const kv = self.timers.fetchRemove(id) orelse return;
|
||||
return page.loop.cancel(kv.value.loop_id);
|
||||
pub fn _clearTimeout(self: *Window, id: u32) !void {
|
||||
_ = self.timers.remove(id);
|
||||
}
|
||||
|
||||
pub fn _clearInterval(self: *Window, id: u32, page: *Page) !void {
|
||||
const kv = self.timers.fetchRemove(id) orelse return;
|
||||
return page.loop.cancel(kv.value.loop_id);
|
||||
pub fn _clearInterval(self: *Window, id: u32) !void {
|
||||
_ = self.timers.remove(id);
|
||||
}
|
||||
|
||||
pub fn _queueMicrotask(self: *Window, cbk: Function, page: *Page) !u32 {
|
||||
return self.createTimeout(cbk, 0, page, .{});
|
||||
return self.createTimeout(cbk, 0, page, .{ .name = "queueMicrotask" });
|
||||
}
|
||||
|
||||
pub fn _matchMedia(_: *const Window, media: []const u8, page: *Page) !MediaQueryList {
|
||||
@@ -232,6 +228,7 @@ pub const Window = struct {
|
||||
}
|
||||
|
||||
const CreateTimeoutOpts = struct {
|
||||
name: []const u8,
|
||||
args: []Env.JsObject = &.{},
|
||||
repeat: bool = false,
|
||||
animation_frame: bool = false,
|
||||
@@ -258,6 +255,8 @@ pub const Window = struct {
|
||||
if (gop.found_existing) {
|
||||
// this can only happen if we've created 2^31 timeouts.
|
||||
return error.TooManyTimeout;
|
||||
} else {
|
||||
gop.value_ptr.* = {};
|
||||
}
|
||||
errdefer _ = self.timers.remove(timer_id);
|
||||
|
||||
@@ -270,22 +269,19 @@ pub const Window = struct {
|
||||
}
|
||||
}
|
||||
|
||||
const delay_ms: u63 = @as(u63, delay) * std.time.ns_per_ms;
|
||||
const callback = try arena.create(TimerCallback);
|
||||
|
||||
callback.* = .{
|
||||
.cbk = cbk,
|
||||
.loop_id = 0, // we're going to set this to a real value shortly
|
||||
.window = self,
|
||||
.timer_id = timer_id,
|
||||
.args = persisted_args,
|
||||
.node = .{ .func = TimerCallback.run },
|
||||
.repeat = if (opts.repeat) delay_ms else null,
|
||||
.animation_frame = opts.animation_frame,
|
||||
// setting a repeat time of 0 is illegal, doing + 1 is a simple way to avoid that
|
||||
.repeat = if (opts.repeat) delay + 1 else null,
|
||||
};
|
||||
callback.loop_id = try page.loop.timeout(delay_ms, &callback.node);
|
||||
|
||||
gop.value_ptr.* = callback;
|
||||
try page.scheduler.add(callback, TimerCallback.run, delay, .{ .name = opts.name });
|
||||
|
||||
return timer_id;
|
||||
}
|
||||
|
||||
@@ -354,30 +350,32 @@ pub const Window = struct {
|
||||
};
|
||||
|
||||
const TimerCallback = struct {
|
||||
// the internal loop id, need it when cancelling
|
||||
loop_id: usize,
|
||||
|
||||
// the id of our timer (windows.timers key)
|
||||
timer_id: u31,
|
||||
|
||||
// if false, we'll remove the timer_id from the window.timers lookup on run
|
||||
repeat: ?u32,
|
||||
|
||||
// The JavaScript callback to execute
|
||||
cbk: Function,
|
||||
|
||||
// This is the internal data that the event loop tracks. We'll get this
|
||||
// back in run and, from it, can get our TimerCallback instance
|
||||
node: Loop.CallbackNode = undefined,
|
||||
|
||||
// if the event should be repeated
|
||||
repeat: ?u63 = null,
|
||||
|
||||
animation_frame: bool = false,
|
||||
|
||||
window: *Window,
|
||||
|
||||
args: []Env.JsObject = &.{},
|
||||
|
||||
fn run(node: *Loop.CallbackNode, repeat_delay: *?u63) void {
|
||||
const self: *TimerCallback = @fieldParentPtr("node", node);
|
||||
fn run(ctx: *anyopaque) ?u32 {
|
||||
const self: *TimerCallback = @alignCast(@ptrCast(ctx));
|
||||
if (self.repeat != null) {
|
||||
if (self.window.timers.contains(self.timer_id) == false) {
|
||||
// it was called
|
||||
return null;
|
||||
}
|
||||
} else if (self.window.timers.remove(self.timer_id) == false) {
|
||||
// it was cancelled
|
||||
return null;
|
||||
}
|
||||
|
||||
var result: Function.Result = undefined;
|
||||
|
||||
@@ -396,14 +394,7 @@ const TimerCallback = struct {
|
||||
});
|
||||
};
|
||||
|
||||
if (self.repeat) |r| {
|
||||
// setInterval
|
||||
repeat_delay.* = r;
|
||||
return;
|
||||
}
|
||||
|
||||
// setTimeout
|
||||
_ = self.window.timers.remove(self.timer_id);
|
||||
return self.repeat;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -412,13 +403,11 @@ test "Browser.HTML.Window" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "window.parent === window", "true" },
|
||||
.{ "window.top === window", "true" },
|
||||
}, .{});
|
||||
// try runner.testCases(&.{
|
||||
// .{ "window.parent === window", "true" },
|
||||
// .{ "window.top === window", "true" },
|
||||
// }, .{});
|
||||
|
||||
// requestAnimationFrame should be able to wait by recursively calling itself
|
||||
// Note however that we in this test do not wait as the request is just send to the browser
|
||||
try runner.testCases(&.{
|
||||
.{
|
||||
\\ let start = 0;
|
||||
|
||||
@@ -31,6 +31,7 @@ const Renderer = @import("renderer.zig").Renderer;
|
||||
const Window = @import("html/window.zig").Window;
|
||||
const Walker = @import("dom/walker.zig").WalkerDepthFirst;
|
||||
const Loop = @import("../runtime/loop.zig").Loop;
|
||||
const Scheduler = @import("Scheduler.zig");
|
||||
const ScriptManager = @import("ScriptManager.zig");
|
||||
const HTMLDocument = @import("html/document.zig").HTMLDocument;
|
||||
|
||||
@@ -75,11 +76,6 @@ pub const Page = struct {
|
||||
|
||||
renderer: Renderer,
|
||||
|
||||
// run v8 micro tasks
|
||||
microtask_node: Loop.CallbackNode,
|
||||
// run v8 pump message loop and idle tasks
|
||||
messageloop_node: Loop.CallbackNode,
|
||||
|
||||
keydown_event_node: parser.EventNode,
|
||||
window_clicked_event_node: parser.EventNode,
|
||||
|
||||
@@ -94,9 +90,9 @@ pub const Page = struct {
|
||||
|
||||
polyfill_loader: polyfill.Loader = .{},
|
||||
|
||||
scheduler: Scheduler,
|
||||
http_client: *http.Client,
|
||||
script_manager: ScriptManager,
|
||||
|
||||
mode: Mode,
|
||||
|
||||
loaded: bool = false,
|
||||
@@ -127,8 +123,7 @@ pub const Page = struct {
|
||||
.cookie_jar = &session.cookie_jar,
|
||||
.script_manager = script_manager,
|
||||
.http_client = browser.http_client,
|
||||
.microtask_node = .{ .func = microtaskCallback },
|
||||
.messageloop_node = .{ .func = messageLoopCallback },
|
||||
.scheduler = Scheduler.init(arena),
|
||||
.keydown_event_node = .{ .func = keydownCallback },
|
||||
.window_clicked_event_node = .{ .func = windowClicked },
|
||||
// @newhttp
|
||||
@@ -140,10 +135,10 @@ pub const Page = struct {
|
||||
self.main_context = try session.executor.createJsContext(&self.window, self, self, true, Env.GlobalMissingCallback.init(&self.polyfill_loader));
|
||||
try polyfill.preload(self.arena, self.main_context);
|
||||
|
||||
try self.scheduler.add(self, runMicrotasks, 5, .{ .name = "page.microtasks" });
|
||||
// message loop must run only non-test env
|
||||
if (comptime !builtin.is_test) {
|
||||
_ = try session.browser.app.loop.timeout(1 * std.time.ns_per_ms, &self.microtask_node);
|
||||
_ = try session.browser.app.loop.timeout(100 * std.time.ns_per_ms, &self.messageloop_node);
|
||||
try self.scheduler.add(self, runMessageLoop, 5, .{ .name = "page.messageLoop" });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,16 +146,16 @@ pub const Page = struct {
|
||||
self.script_manager.deinit();
|
||||
}
|
||||
|
||||
fn microtaskCallback(node: *Loop.CallbackNode, repeat_delay: *?u63) void {
|
||||
const self: *Page = @fieldParentPtr("microtask_node", node);
|
||||
fn runMicrotasks(ctx: *anyopaque) ?u32 {
|
||||
const self: *Page = @alignCast(@ptrCast(ctx));
|
||||
self.session.browser.runMicrotasks();
|
||||
repeat_delay.* = 1 * std.time.ns_per_ms;
|
||||
return 5;
|
||||
}
|
||||
|
||||
fn messageLoopCallback(node: *Loop.CallbackNode, repeat_delay: *?u63) void {
|
||||
const self: *Page = @fieldParentPtr("messageloop_node", node);
|
||||
fn runMessageLoop(ctx: *anyopaque) ?u32 {
|
||||
const self: *Page = @alignCast(@ptrCast(ctx));
|
||||
self.session.browser.runMessageLoop();
|
||||
repeat_delay.* = 100 * std.time.ns_per_ms;
|
||||
return 100;
|
||||
}
|
||||
|
||||
pub const DumpOpts = struct {
|
||||
@@ -237,39 +232,66 @@ pub const Page = struct {
|
||||
// The HTML page was parsed. We now either have JS scripts to
|
||||
// download, or timeouts to execute, or both.
|
||||
|
||||
const cutoff = timestamp() + wait_sec;
|
||||
|
||||
var try_catch: Env.TryCatch = undefined;
|
||||
try_catch.init(self.main_context);
|
||||
defer try_catch.deinit();
|
||||
|
||||
var scheduler = &self.scheduler;
|
||||
var http_client = self.http_client;
|
||||
var loop = self.session.browser.app.loop;
|
||||
|
||||
// @newhttp Not sure about the timing / the order / any of this.
|
||||
// I think I want to remove the loop. Implement our own timeouts
|
||||
// and switch the CDP server to blocking. For now, just try this.`
|
||||
while (timestamp() < cutoff) {
|
||||
const has_pending_timeouts = loop.hasPendingTimeout();
|
||||
if (http_client.active > 0) {
|
||||
try http_client.tick(10); // 10ms
|
||||
} else if (self.loaded and self.loaded and !has_pending_timeouts) {
|
||||
// we have no active HTTP requests, and no timeouts pending
|
||||
return;
|
||||
}
|
||||
var ms_remaining = wait_sec * 1000;
|
||||
var timer = try std.time.Timer.start();
|
||||
|
||||
if (!has_pending_timeouts) {
|
||||
continue;
|
||||
}
|
||||
while (true) {
|
||||
const has_active_http = http_client.active > 0;
|
||||
|
||||
// 10ms
|
||||
try loop.run(std.time.ns_per_ms * 10);
|
||||
const ms_to_next_task = try scheduler.run(has_active_http);
|
||||
|
||||
if (try_catch.hasCaught()) {
|
||||
const msg = (try try_catch.err(self.arena)) orelse "unknown";
|
||||
log.err(.browser, "page wait error", .{ .err = msg });
|
||||
return error.JsError;
|
||||
}
|
||||
|
||||
if (has_active_http == false) {
|
||||
if (ms_to_next_task) |ms| {
|
||||
// There are no HTTP transfers, so there's no point calling
|
||||
// http_client.tick.
|
||||
// TODO: should we just force-run the scheduler??
|
||||
|
||||
if (ms > ms_remaining) {
|
||||
// we'd wait to long, might as well exit early.
|
||||
return;
|
||||
}
|
||||
|
||||
std.time.sleep(std.time.ns_per_ms * ms);
|
||||
ms_remaining -= ms;
|
||||
continue;
|
||||
}
|
||||
|
||||
// We have no active http transfer and no pending
|
||||
// schedule tasks. We're done
|
||||
return;
|
||||
}
|
||||
|
||||
// We'll block here, waiting for network IO. We know
|
||||
// when the next timeout is scheduled, and we know how long
|
||||
// the caller wants to wait for, so we can pick a good wait
|
||||
// duration
|
||||
const ms_to_wait = @min(ms_remaining, ms_to_next_task orelse 1000);
|
||||
try http_client.tick(ms_to_wait);
|
||||
|
||||
if (try_catch.hasCaught()) {
|
||||
const msg = (try try_catch.err(self.arena)) orelse "unknown";
|
||||
log.err(.browser, "page wait error", .{ .err = msg });
|
||||
return error.JsError;
|
||||
}
|
||||
|
||||
const ms_elapsed = timer.lap() / 100_000;
|
||||
if (ms_elapsed > ms_remaining) {
|
||||
return;
|
||||
}
|
||||
ms_remaining -= ms_elapsed;
|
||||
}
|
||||
},
|
||||
.err => |err| return err,
|
||||
|
||||
@@ -793,12 +793,9 @@ test "Browser.XHR.XMLHttpRequest" {
|
||||
.{ "req.status", "200" },
|
||||
.{ "req.statusText", "OK" },
|
||||
.{ "req.getResponseHeader('Content-Type')", "text/html; charset=utf-8" },
|
||||
.{
|
||||
"req.getAllResponseHeaders()",
|
||||
"content-length: 100\r\n" ++
|
||||
.{ "req.getAllResponseHeaders()", "content-length: 100\r\n" ++
|
||||
"Content-Type: text/html; charset=utf-8\r\n" ++
|
||||
"Connection: Close\r\n"
|
||||
},
|
||||
"Connection: Close\r\n" },
|
||||
.{ "req.responseText.length", "100" },
|
||||
.{ "req.response.length == req.responseText.length", "true" },
|
||||
.{ "req.responseXML instanceof Document", "true" },
|
||||
|
||||
@@ -549,4 +549,3 @@ pub const ProxyAuth = union(enum) {
|
||||
basic: struct { user_pass: []const u8 },
|
||||
bearer: struct { token: []const u8 },
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user