From af7498d28378eeb16ef879e4bffec7ac0cf8e940 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Wed, 25 Feb 2026 13:55:35 +0800 Subject: [PATCH 1/3] Run the MessageLoop [a lot] more. Depends on: https://github.com/lightpanda-io/zig-v8-fork/pull/152 We previously ran the message loop every 250ms. This commit changes it to run on every tick (much more frequently). It also runs microtasks after draining the message loop (since it can generate microtasks). Also, we use to run microtasks after each script execution. Now we drain the message Loop + microtasks. We still only drain the microtasks when executing v8 callbacks. As part of this change, we also adjust our wait time based on whether or not there are pending background tasks in v8 in order to try to execute them (in general) and in a timely manner. The goal is to ensure that tasks v8 enqueued on the foreground thread are executed promptly. This change is particularly useful for calls to webassembly as compilation happens in the background and eventually requires the message loop to be drained to continue. Previously, if a script did `await WebAssembly.instantiate(....)`, there was a good chance we'd never finish the code - we'd wait too long to run the message loop AND, after running it, we wouldn't necessarily resolve the promise. --- build.zig.zon | 5 +++-- src/browser/Browser.zig | 20 +++++++++++++++++--- src/browser/Page.zig | 8 ++++---- src/browser/ScriptManager.zig | 3 +-- src/browser/Session.zig | 20 +++++++++++++++++--- src/browser/js/Context.zig | 2 +- src/browser/js/Env.zig | 21 +++++++++++++++++---- src/browser/js/Local.zig | 6 ++++++ src/cdp/cdp.zig | 9 +++++---- 9 files changed, 71 insertions(+), 23 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 0d115d53..a1ab877c 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -6,8 +6,9 @@ .minimum_zig_version = "0.15.2", .dependencies = .{ .v8 = .{ - .url = "https://github.com/lightpanda-io/zig-v8-fork/archive/refs/tags/v0.3.0.tar.gz", - .hash = "v8-0.0.0-xddH69R6BADRXsnhjA8wNnfKfLQACF1I7CSTZvsMAvp8", + .url = "https://github.com/lightpanda-io/zig-v8-fork/archive/8c7e5df8b93e7cbd42f8f1c4ac24aaa7f05cd098.tar.gz", + .hash = "v8-0.0.0-xddH64J7BAC81mkf6G9RbEJxS-W3TIRl5iFnShwbqCqy", + }, //.v8 = .{ .path = "../zig-v8-fork" }, .@"boringssl-zig" = .{ diff --git a/src/browser/Browser.zig b/src/browser/Browser.zig index 6572b20f..503306d3 100644 --- a/src/browser/Browser.zig +++ b/src/browser/Browser.zig @@ -92,10 +92,24 @@ pub fn runMicrotasks(self: *Browser) void { } pub fn runMacrotasks(self: *Browser) !?u64 { - return try self.env.runMacrotasks(); + const env = &self.env; + + const time_to_next = try self.env.runMacrotasks(); + env.pumpMessageLoop(); + + // either of the above could have queued more microtasks + env.runMicrotasks(); + + return time_to_next; } -pub fn runMessageLoop(self: *const Browser) void { - self.env.pumpMessageLoop(); +pub fn hasBackgroundTasks(self: *Browser) bool { + return self.env.hasBackgroundTasks(); +} +pub fn waitForBackgroundTasks(self: *Browser) void { + self.env.waitForBackgroundTasks(); +} + +pub fn runIdleTasks(self: *const Browser) void { self.env.runIdleTasks(); } diff --git a/src/browser/Page.zig b/src/browser/Page.zig index 11ff9ebc..cf3d2523 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -311,12 +311,12 @@ pub fn init(self: *Page, id: u32, session: *Session, parent: ?*Page) !void { if (comptime builtin.is_test == false) { // HTML test runner manually calls these as necessary try self.js.scheduler.add(session.browser, struct { - fn runMessageLoop(ctx: *anyopaque) !?u32 { + fn runIdleTasks(ctx: *anyopaque) !?u32 { const b: *@import("Browser.zig") = @ptrCast(@alignCast(ctx)); - b.runMessageLoop(); - return 250; + b.runIdleTasks(); + return 200; } - }.runMessageLoop, 250, .{ .name = "page.messageLoop" }); + }.runIdleTasks, 200, .{ .name = "page.runIdleTasks", .low_priority = true }); } } diff --git a/src/browser/ScriptManager.zig b/src/browser/ScriptManager.zig index b536e2db..deed426d 100644 --- a/src/browser/ScriptManager.zig +++ b/src/browser/ScriptManager.zig @@ -862,8 +862,7 @@ pub const Script = struct { } defer { - // We should run microtasks even if script execution fails. - local.runMicrotasks(); + local.runMacrotasks(); // also runs microtasks _ = page.js.scheduler.run() catch |err| { log.err(.page, "scheduler", .{ .err = err }); }; diff --git a/src/browser/Session.zig b/src/browser/Session.zig index c9b4db48..c8d701cc 100644 --- a/src/browser/Session.zig +++ b/src/browser/Session.zig @@ -257,7 +257,7 @@ fn _wait(self: *Session, page: *Page, wait_ms: u32) !WaitResult { std.debug.assert(http_client.intercepted == 0); } - const ms = ms_to_next_task orelse blk: { + const ms: u64 = ms_to_next_task orelse blk: { if (wait_ms - ms_remaining < 100) { if (comptime builtin.is_test) { return .done; @@ -267,6 +267,14 @@ fn _wait(self: *Session, page: *Page, wait_ms: u32) !WaitResult { // background jobs. break :blk 50; } + + if (browser.hasBackgroundTasks()) { + // _we_ have nothing to run, but v8 is working on + // background tasks. We'll wait for them. + browser.waitForBackgroundTasks(); + break :blk 20; + } + // No http transfers, no cdp extra socket, no // scheduled tasks, we're done. return .done; @@ -292,8 +300,14 @@ fn _wait(self: *Session, page: *Page, wait_ms: u32) !WaitResult { // an cdp_socket registered with the http client). // We should continue to run lowPriority tasks, so we // minimize how long we'll poll for network I/O. - const ms_to_wait = @min(200, @min(ms_remaining, ms_to_next_task orelse 200)); - if (try http_client.tick(ms_to_wait) == .cdp_socket) { + var ms_to_wait = @min(200, ms_to_next_task orelse 200); + if (ms_to_wait > 10 and browser.hasBackgroundTasks()) { + // if we have bakcground tasks, we don't want ot wait too + // long for a message from the client. We want to go back + // to the top of the loop and run macrotasks. + ms_to_wait = 10; + } + if (try http_client.tick(@min(ms_remaining, ms_to_wait)) == .cdp_socket) { // data on a socket we aren't handling, return to caller return .cdp_socket; } diff --git a/src/browser/js/Context.zig b/src/browser/js/Context.zig index 1b804ced..ebcc56fc 100644 --- a/src/browser/js/Context.zig +++ b/src/browser/js/Context.zig @@ -237,7 +237,7 @@ pub fn deinit(self: *Context) void { env.isolate.notifyContextDisposed(); // There can be other tasks associated with this context that we need to // purge while the context is still alive. - env.pumpMessageLoop(); + _ = env.pumpMessageLoop(); v8.v8__MicrotaskQueue__DELETE(self.microtask_queue); } diff --git a/src/browser/js/Env.zig b/src/browser/js/Env.zig index e0e060cb..2f1b6b1e 100644 --- a/src/browser/js/Env.zig +++ b/src/browser/js/Env.zig @@ -397,10 +397,23 @@ pub fn pumpMessageLoop(self: *const Env) void { const isolate = self.isolate.handle; const platform = self.platform.handle; - while (v8.v8__Platform__PumpMessageLoop(platform, isolate, false)) { - if (comptime IS_DEBUG) { - log.debug(.browser, "pumpMessageLoop", .{}); - } + while (v8.v8__Platform__PumpMessageLoop(platform, isolate, false)) {} +} + +pub fn hasBackgroundTasks(self: *const Env) bool { + return v8.v8__Isolate__HasPendingBackgroundTasks(self.isolate.handle); +} + +pub fn waitForBackgroundTasks(self: *Env) void { + var hs: v8.HandleScope = undefined; + v8.v8__HandleScope__CONSTRUCT(&hs, self.isolate.handle); + defer v8.v8__HandleScope__DESTRUCT(&hs); + + const isolate = self.isolate.handle; + const platform = self.platform.handle; + while (v8.v8__Isolate__HasPendingBackgroundTasks(isolate)) { + _ = v8.v8__Platform__PumpMessageLoop(platform, isolate, true); + self.runMicrotasks(); } } diff --git a/src/browser/js/Local.zig b/src/browser/js/Local.zig index af9febf4..1e2d3505 100644 --- a/src/browser/js/Local.zig +++ b/src/browser/js/Local.zig @@ -82,6 +82,12 @@ pub fn createTypedArray(self: *const Local, comptime array_type: js.ArrayType, s return .init(self, size); } +pub fn runMacrotasks(self: *const Local) void { + const env = self.ctx.env; + env.pumpMessageLoop(); + env.runMicrotasks(); // macrotasks can cause microtasks to queue +} + pub fn runMicrotasks(self: *const Local) void { self.ctx.env.runMicrotasks(); } diff --git a/src/cdp/cdp.zig b/src/cdp/cdp.zig index c486e794..8134d3f3 100644 --- a/src/cdp/cdp.zig +++ b/src/cdp/cdp.zig @@ -436,17 +436,18 @@ pub fn BrowserContext(comptime CDP_T: type) type { pub fn deinit(self: *Self) void { const browser = &self.cdp.browser; + const env = &browser.env; // Drain microtasks makes sure we don't have inspector's callback // in progress before deinit. - browser.env.runMicrotasks(); + env.runMicrotasks(); // resetContextGroup detach the inspector from all contexts. // It append async tasks, so we make sure we run the message loop // before deinit it. - browser.env.inspector.?.resetContextGroup(); - browser.runMessageLoop(); - browser.env.inspector.?.stopSession(); + env.inspector.?.resetContextGroup(); + _ = env.pumpMessageLoop(); + env.inspector.?.stopSession(); // abort all intercepted requests before closing the sesion/page // since some of these might callback into the page/scriptmanager From 7a417435cc782fb8a9e5eb532857f0c9c69c9b19 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Thu, 26 Feb 2026 10:53:16 +0800 Subject: [PATCH 2/3] Update src/browser/Session.zig Co-authored-by: Pierre Tachoire --- src/browser/Session.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/browser/Session.zig b/src/browser/Session.zig index c8d701cc..e919b480 100644 --- a/src/browser/Session.zig +++ b/src/browser/Session.zig @@ -302,7 +302,7 @@ fn _wait(self: *Session, page: *Page, wait_ms: u32) !WaitResult { // minimize how long we'll poll for network I/O. var ms_to_wait = @min(200, ms_to_next_task orelse 200); if (ms_to_wait > 10 and browser.hasBackgroundTasks()) { - // if we have bakcground tasks, we don't want ot wait too + // if we have background tasks, we don't want to wait too // long for a message from the client. We want to go back // to the top of the loop and run macrotasks. ms_to_wait = 10; From aedb823b4d81b575c2ef6bd4e4887a8a84f69bac Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Thu, 26 Feb 2026 10:55:02 +0800 Subject: [PATCH 3/3] update v8 dep --- .github/actions/install/action.yml | 2 +- Dockerfile | 2 +- build.zig.zon | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/actions/install/action.yml b/.github/actions/install/action.yml index 9e47eac2..6c98f2e5 100644 --- a/.github/actions/install/action.yml +++ b/.github/actions/install/action.yml @@ -13,7 +13,7 @@ inputs: zig-v8: description: 'zig v8 version to install' required: false - default: 'v0.3.0' + default: 'v0.3.1' v8: description: 'v8 version to install' required: false diff --git a/Dockerfile b/Dockerfile index 1aa5d592..75be1c9e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ FROM debian:stable-slim ARG MINISIG=0.12 ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U ARG V8=14.0.365.4 -ARG ZIG_V8=v0.3.0 +ARG ZIG_V8=v0.3.1 ARG TARGETPLATFORM RUN apt-get update -yq && \ diff --git a/build.zig.zon b/build.zig.zon index a1ab877c..b2819e04 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -6,7 +6,7 @@ .minimum_zig_version = "0.15.2", .dependencies = .{ .v8 = .{ - .url = "https://github.com/lightpanda-io/zig-v8-fork/archive/8c7e5df8b93e7cbd42f8f1c4ac24aaa7f05cd098.tar.gz", + .url = "https://github.com/lightpanda-io/zig-v8-fork/archive/refs/tags/v0.3.1.tar.gz", .hash = "v8-0.0.0-xddH64J7BAC81mkf6G9RbEJxS-W3TIRl5iFnShwbqCqy", },