Merge pull request #1426 from lightpanda-io/update_page_js
Some checks are pending
e2e-test / zig build release (push) Waiting to run
e2e-test / demo-scripts (push) Blocked by required conditions
e2e-test / cdp-and-hyperfine-bench (push) Blocked by required conditions
e2e-test / perf-fmt (push) Blocked by required conditions
e2e-test / browser fetch (push) Blocked by required conditions
zig-test / zig test (push) Waiting to run
zig-test / perf-fmt (push) Blocked by required conditions

Update page.js based on the current context.
This commit is contained in:
Karl Seguin
2026-02-04 08:51:53 +08:00
committed by GitHub
10 changed files with 50 additions and 28 deletions

View File

@@ -112,6 +112,10 @@ pub fn runMicrotasks(self: *const Browser) void {
self.env.runMicrotasks(); self.env.runMicrotasks();
} }
pub fn runMacrotasks(self: *Browser) !?u64 {
return try self.env.runMacrotasks();
}
pub fn runMessageLoop(self: *const Browser) void { pub fn runMessageLoop(self: *const Browser) void {
while (self.env.pumpMessageLoop()) { while (self.env.pumpMessageLoop()) {
if (comptime IS_DEBUG) { if (comptime IS_DEBUG) {

View File

@@ -33,7 +33,6 @@ const String = @import("../string.zig").String;
const Mime = @import("Mime.zig"); const Mime = @import("Mime.zig");
const Factory = @import("Factory.zig"); const Factory = @import("Factory.zig");
const Session = @import("Session.zig"); const Session = @import("Session.zig");
const Scheduler = @import("Scheduler.zig");
const EventManager = @import("EventManager.zig"); const EventManager = @import("EventManager.zig");
const ScriptManager = @import("ScriptManager.zig"); const ScriptManager = @import("ScriptManager.zig");
@@ -202,8 +201,6 @@ document: *Document,
// DOM version used to invalidate cached state of "live" collections // DOM version used to invalidate cached state of "live" collections
version: usize, version: usize,
scheduler: Scheduler,
_req_id: ?usize = null, _req_id: ?usize = null,
_navigated_options: ?NavigatedOpts = null, _navigated_options: ?NavigatedOpts = null,
@@ -237,10 +234,6 @@ pub fn deinit(self: *Page) void {
// stats.print(&stream) catch unreachable; // stats.print(&stream) catch unreachable;
} }
// This can release JS objects, so we need to do this while the js.Context
// is still around.
self.scheduler.deinit();
{ {
// some MicroTasks might be referencing the page, we need to drain it while // some MicroTasks might be referencing the page, we need to drain it while
// the page still exists // the page still exists
@@ -271,8 +264,6 @@ fn reset(self: *Page, comptime initializing: bool) !void {
const browser = self._session.browser; const browser = self._session.browser;
if (comptime initializing == false) { if (comptime initializing == false) {
self.scheduler.deinit();
browser.env.destroyContext(self.js); browser.env.destroyContext(self.js);
// We force a garbage collection between page navigations to keep v8 // We force a garbage collection between page navigations to keep v8
@@ -299,7 +290,6 @@ fn reset(self: *Page, comptime initializing: bool) !void {
} }
self._factory = Factory.init(self); self._factory = Factory.init(self);
self.scheduler = Scheduler.init(self.arena);
self.version = 0; self.version = 0;
self.url = "about:blank"; self.url = "about:blank";
@@ -381,7 +371,7 @@ fn registerBackgroundTasks(self: *Page) !void {
const Browser = @import("Browser.zig"); const Browser = @import("Browser.zig");
try self.scheduler.add(self._session.browser, struct { try self.js.scheduler.add(self._session.browser, struct {
fn runMessageLoop(ctx: *anyopaque) !?u32 { fn runMessageLoop(ctx: *anyopaque) !?u32 {
const b: *Browser = @ptrCast(@alignCast(ctx)); const b: *Browser = @ptrCast(@alignCast(ctx));
b.runMessageLoop(); b.runMessageLoop();
@@ -887,8 +877,8 @@ fn _wait(self: *Page, wait_ms: u32) !Session.WaitResult {
var timer = try std.time.Timer.start(); var timer = try std.time.Timer.start();
var ms_remaining = wait_ms; var ms_remaining = wait_ms;
var scheduler = &self.scheduler; const browser = self._session.browser;
var http_client = self._session.browser.http_client; var http_client = browser.http_client;
// I'd like the page to know NOTHING about cdp_socket / CDP, but the // I'd like the page to know NOTHING about cdp_socket / CDP, but the
// fact is that the behavior of wait changes depending on whether or // fact is that the behavior of wait changes depending on whether or
@@ -941,7 +931,7 @@ fn _wait(self: *Page, wait_ms: u32) !Session.WaitResult {
// 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.run(); const ms_to_next_task = try browser.runMacrotasks();
const http_active = http_client.active; const http_active = http_client.active;
const total_network_activity = http_active + http_client.intercepted; const total_network_activity = http_active + http_client.intercepted;
@@ -1077,16 +1067,16 @@ fn printWaitAnalysis(self: *Page) void {
const now = milliTimestamp(.monotonic); const now = milliTimestamp(.monotonic);
{ {
std.debug.print("\nhigh_priority schedule: {d}\n", .{self.scheduler.high_priority.count()}); std.debug.print("\nhigh_priority schedule: {d}\n", .{self.js.scheduler.high_priority.count()});
var it = self.scheduler.high_priority.iterator(); var it = self.js.scheduler.high_priority.iterator();
while (it.next()) |task| { while (it.next()) |task| {
std.debug.print(" - {s} schedule: {d}ms\n", .{ task.name, task.run_at - now }); std.debug.print(" - {s} schedule: {d}ms\n", .{ task.name, task.run_at - now });
} }
} }
{ {
std.debug.print("\nlow_priority schedule: {d}\n", .{self.scheduler.low_priority.count()}); std.debug.print("\nlow_priority schedule: {d}\n", .{self.js.scheduler.low_priority.count()});
var it = self.scheduler.low_priority.iterator(); var it = self.js.scheduler.low_priority.iterator();
while (it.next()) |task| { while (it.next()) |task| {
std.debug.print(" - {s} schedule: {d}ms\n", .{ task.name, task.run_at - now }); std.debug.print(" - {s} schedule: {d}ms\n", .{ task.name, task.run_at - now });
} }
@@ -1258,7 +1248,7 @@ pub fn notifyPerformanceObservers(self: *Page, entry: *Performance.Entry) !void
} }
self._performance_delivery_scheduled = true; self._performance_delivery_scheduled = true;
return self.scheduler.add( return self.js.scheduler.add(
self, self,
struct { struct {
fn run(_page: *anyopaque) anyerror!?u32 { fn run(_page: *anyopaque) anyerror!?u32 {

View File

@@ -846,7 +846,7 @@ pub const Script = struct {
defer { defer {
// We should run microtasks even if script execution fails. // We should run microtasks even if script execution fails.
local.runMicrotasks(); local.runMicrotasks();
_ = page.scheduler.run() catch |err| { _ = page.js.scheduler.run() catch |err| {
log.err(.page, "scheduler", .{ .err = err }); log.err(.page, "scheduler", .{ .err = err });
}; };
} }

View File

@@ -35,6 +35,7 @@ const IS_DEBUG = @import("builtin").mode == .Debug;
const Caller = @This(); const Caller = @This();
local: js.Local, local: js.Local,
prev_local: ?*const js.Local, prev_local: ?*const js.Local,
prev_context: *Context,
// Takes the raw v8 isolate and extracts the context from it. // Takes the raw v8 isolate and extracts the context from it.
pub fn init(self: *Caller, v8_isolate: *v8.Isolate) void { pub fn init(self: *Caller, v8_isolate: *v8.Isolate) void {
@@ -53,7 +54,9 @@ pub fn init(self: *Caller, v8_isolate: *v8.Isolate) void {
.isolate = .{ .handle = v8_isolate }, .isolate = .{ .handle = v8_isolate },
}, },
.prev_local = ctx.local, .prev_local = ctx.local,
.prev_context = ctx.page.js,
}; };
ctx.page.js = ctx;
ctx.local = &self.local; ctx.local = &self.local;
} }
@@ -79,6 +82,7 @@ pub fn deinit(self: *Caller) void {
ctx.call_depth = call_depth; ctx.call_depth = call_depth;
ctx.local = self.prev_local; ctx.local = self.prev_local;
ctx.page.js = self.prev_context;
} }
pub const CallOpts = struct { pub const CallOpts = struct {

View File

@@ -23,6 +23,7 @@ const log = @import("../../log.zig");
const js = @import("js.zig"); const js = @import("js.zig");
const Env = @import("Env.zig"); const Env = @import("Env.zig");
const bridge = @import("bridge.zig"); const bridge = @import("bridge.zig");
const Scheduler = @import("Scheduler.zig");
const Page = @import("../Page.zig"); const Page = @import("../Page.zig");
const ScriptManager = @import("../ScriptManager.zig"); const ScriptManager = @import("../ScriptManager.zig");
@@ -118,6 +119,9 @@ module_identifier: std.AutoHashMapUnmanaged(u32, [:0]const u8) = .empty,
// the page's script manager // the page's script manager
script_manager: ?*ScriptManager, script_manager: ?*ScriptManager,
// Our macrotasks
scheduler: Scheduler,
const ModuleEntry = struct { const ModuleEntry = struct {
// Can be null if we're asynchrously loading the module, in // Can be null if we're asynchrously loading the module, in
// which case resolver_promise cannot be null. // which case resolver_promise cannot be null.
@@ -150,6 +154,14 @@ pub fn fromIsolate(isolate: js.Isolate) *Context {
} }
pub fn deinit(self: *Context) void { pub fn deinit(self: *Context) void {
var page = self.page;
const prev_context = page.js;
page.js = self;
defer page.js = prev_context;
// This can release JS objects
self.scheduler.deinit();
{ {
var it = self.identity_map.valueIterator(); var it = self.identity_map.valueIterator();
while (it.next()) |global| { while (it.next()) |global| {

View File

@@ -240,6 +240,7 @@ pub fn createContext(self: *Env, page: *Page, enter: bool) !*Context {
.templates = self.templates, .templates = self.templates,
.call_arena = page.call_arena, .call_arena = page.call_arena,
.script_manager = &page._script_manager, .script_manager = &page._script_manager,
.scheduler = .init(context_arena),
}; };
try context.identity_map.putNoClobber(context_arena, @intFromPtr(page.window), global_global); try context.identity_map.putNoClobber(context_arena, @intFromPtr(page.window), global_global);
@@ -271,6 +272,17 @@ pub fn runMicrotasks(self: *const Env) void {
self.isolate.performMicrotasksCheckpoint(); self.isolate.performMicrotasksCheckpoint();
} }
pub fn runMacrotasks(self: *Env) !?u64 {
var ms_to_next_task: ?u64 = null;
for (self.contexts.items) |ctx| {
const ms = (try ctx.scheduler.run()) orelse continue;
if (ms_to_next_task == null or ms < ms_to_next_task.?) {
ms_to_next_task = ms;
}
}
return ms_to_next_task;
}
pub fn pumpMessageLoop(self: *const Env) bool { pub fn pumpMessageLoop(self: *const Env) bool {
var hs: v8.HandleScope = undefined; var hs: v8.HandleScope = undefined;
v8.v8__HandleScope__CONSTRUCT(&hs, self.isolate.handle); v8.v8__HandleScope__CONSTRUCT(&hs, self.isolate.handle);

View File

@@ -19,8 +19,8 @@
const std = @import("std"); const std = @import("std");
const builtin = @import("builtin"); const builtin = @import("builtin");
const log = @import("../log.zig"); const log = @import("../../log.zig");
const milliTimestamp = @import("../datetime.zig").milliTimestamp; const milliTimestamp = @import("../../datetime.zig").milliTimestamp;
const IS_DEBUG = builtin.mode == .Debug; const IS_DEBUG = builtin.mode == .Debug;

View File

@@ -99,7 +99,7 @@ pub fn createTimeout(delay: u32, page: *Page) !*AbortSignal {
.signal = try init(page), .signal = try init(page),
}; };
try page.scheduler.add(callback, TimeoutCallback.run, delay, .{ try page.js.scheduler.add(callback, TimeoutCallback.run, delay, .{
.name = "AbortSignal.timeout", .name = "AbortSignal.timeout",
}); });

View File

@@ -65,7 +65,7 @@ pub fn postMessage(self: *MessagePort, message: js.Value.Global, page: *Page) !v
.message = message, .message = message,
}); });
try page.scheduler.add(callback, PostMessageCallback.run, 0, .{ try page.js.scheduler.add(callback, PostMessageCallback.run, 0, .{
.name = "MessagePort.postMessage", .name = "MessagePort.postMessage",
.low_priority = false, .low_priority = false,
}); });

View File

@@ -366,7 +366,7 @@ pub fn postMessage(self: *Window, message: js.Value.Global, target_origin: ?[]co
.message = message, .message = message,
.origin = try arena.dupe(u8, origin), .origin = try arena.dupe(u8, origin),
}; };
try page.scheduler.add(callback, PostMessageCallback.run, 0, .{ try page.js.scheduler.add(callback, PostMessageCallback.run, 0, .{
.name = "postMessage", .name = "postMessage",
.low_priority = false, .low_priority = false,
.finalizer = PostMessageCallback.cancelled, .finalizer = PostMessageCallback.cancelled,
@@ -447,7 +447,7 @@ pub fn scrollTo(self: *Window, opts: ScrollToOpts, y: ?i32, page: *Page) !void {
// We dispatch scroll event asynchronously after 10ms. So we can throttle // We dispatch scroll event asynchronously after 10ms. So we can throttle
// them. // them.
try page.scheduler.add( try page.js.scheduler.add(
page, page,
struct { struct {
fn dispatch(_page: *anyopaque) anyerror!?u32 { fn dispatch(_page: *anyopaque) anyerror!?u32 {
@@ -471,7 +471,7 @@ pub fn scrollTo(self: *Window, opts: ScrollToOpts, y: ?i32, page: *Page) !void {
.{ .low_priority = true }, .{ .low_priority = true },
); );
// We dispatch scrollend event asynchronously after 20ms. // We dispatch scrollend event asynchronously after 20ms.
try page.scheduler.add( try page.js.scheduler.add(
page, page,
struct { struct {
fn dispatch(_page: *anyopaque) anyerror!?u32 { fn dispatch(_page: *anyopaque) anyerror!?u32 {
@@ -545,7 +545,7 @@ fn scheduleCallback(self: *Window, cb: js.Function.Temp, delay_ms: u32, opts: Sc
}; };
gop.value_ptr.* = callback; gop.value_ptr.* = callback;
try page.scheduler.add(callback, ScheduleCallback.run, delay_ms, .{ try page.js.scheduler.add(callback, ScheduleCallback.run, delay_ms, .{
.name = opts.name, .name = opts.name,
.low_priority = opts.low_priority, .low_priority = opts.low_priority,
.finalizer = ScheduleCallback.cancelled, .finalizer = ScheduleCallback.cancelled,