Skip to content

browser: virtual time budget - #3450

Open
arrufat wants to merge 2 commits into
mainfrom
virtual-time
Open

arrufat wants to merge 2 commits into
mainfrom
virtual-time

Conversation

@arrufat

@arrufat arrufat commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Opt-in --virtual-time-budget-ms <INT> (common option; fetch, mcp, agent): when the page is idle except for pending timers, advance the page's clock to the next scheduler task instead of sleeping, up to a per-navigation budget, then fall back to real time. Same model as Chrome's --virtual-time-budget with pauseIfNetworkFetchesPending.

Rules

  • Jump only to the next due task, never "fire everything now": timers keep their order, nested timers land relative to the jumped clock, and backoff loops stay bounded by the budget.
  • Never advance while a transfer is in flight, a rate-limited request is parked, or V8 has background work. The one place that decides to sleep (Runner._tick's idle return, plus the idle arms of waitForSelector/waitForScript) is the one place the clock moves.
  • The budget resets when a root document becomes live and is spent per navigation; after it is spent the caller's real timeout bounds the wait exactly as today.

Clocks

src/browser/VirtualTime.zig is the page's clock: scheduler due times, performance.now, event timestamps, resource timing, the network-idle hold and the intersection burst guard read it. Watchdog, HTTP client, rate limiter and server stay on the boot clock. Date.now() follows too, through v8__Platform__SetClockOffsetMillis from lightpanda-io/zig-v8-fork#203: debounce/throttle helpers re-arm setTimeout against Date.now() and would spin until real time caught up otherwise.

Scope

Serve/CDP is deliberately untouched: the CDP loop never reaches the idle sleep (.done is inert there and the driver fd keeps the HTTP client polling), so Emulation.setVirtualTimePolicy would be a separate change. Ignored-by-serve is documented in the help text.

On a page that keeps fetching (eu.gymshark.com's listing fires an Algolia request as soon as the previous one returns) the budget never engages, by design. It targets pages that wait on timers after the network quiets.

Numbers

ReleaseFast build against a release V8 carrying the fork shim, fetch --dump html --wait-until done --wait-ms 15000, with and without --virtual-time-budget-ms 30000:

Page Without With
developer.mozilla.org/en-US/ (3 runs) 1.65 s, 1.64 s, 1.76 s 0.61 s, 0.50 s, 0.51 s
local fixture, one 3 s timer (src/browser/tests/runner/virtual_time_clocks.html) 3.04 s 0.03 s
eu.gymshark.com men's listing 15.1 s, at the cap 15.1 s, at the cap

MDN waits on three 1 s timers after the network quiets; the dumps differ only in per-render Lit markers and ad rotation. On the fixture performance.now, Event.timeStamp and Date.now all report 3000 ms inside the callback in both modes. Gymshark never goes network-idle, so the clock never moves. Pages that re-arm polling timers forever (bbc.com, web.dev) drain the budget without reaching done, as without the flag.

Depends on

lightpanda-io/zig-v8-fork#203, pinned by commit in build.zig.zon. CI needs a fork tag with the new export before this links; then bump zig-v8 in .github/actions/install/action.yml and repoint build.zig.zon at the merge commit.

Opt-in --virtual-time-budget-ms: when the page is idle except for pending
timers, jump the page clock to the next scheduler task instead of sleeping,
up to a per-navigation budget, then fall back to real time. Timers fire in
order and in-flight transfers are never skipped. Timers, performance.now,
event timestamps, resource timing, the network-idle hold and the
intersection burst guard read the page clock; the watchdog, HTTP client,
rate limiter and server keep the boot clock. Date.now stays real until the
V8 platform clock is offset too. Serve is unaffected.
Each virtual time jump also moves the offset on V8's wall clock, through
the fork's v8__Platform__SetClockOffsetMillis, so Date.now() keeps pace
with the timers it drives. Pins zig-v8-fork at lightpanda-io/zig-v8-fork#203;
the prebuilt archive needs a tag bump once that lands.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant