Skip to content

use web-time for the resource-tracker clock so max_duration works on wasm32-unknown-unknown#554

Open
Butch78 wants to merge 2 commits into
pydantic:mainfrom
Butch78:fix/portable-tracker-clock
Open

use web-time for the resource-tracker clock so max_duration works on wasm32-unknown-unknown#554
Butch78 wants to merge 2 commits into
pydantic:mainfrom
Butch78:fix/portable-tracker-clock

Conversation

@Butch78

@Butch78 Butch78 commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

LimitedTracker enforces ResourceLimits::max_duration via std::time::Instant (crates/monty/src/resource.rs). On wasm32-unknown-unknown, Instant::now() panics (time not implemented on this platform), so any run configured with a duration limit aborts. monty's wasm CI targets wasm32-wasip1, where Instant is implemented, so this path is green upstream and the gap is invisible there.

Use case

We embed the monty crate directly as a wasm32-unknown-unknown wasm-bindgen module for in-process, browser-side Python in a cell (not the WASI/worker path). There, max_duration is the only limit that stops a non-allocating runaway such as while True: passmax_allocations / max_memory never fire on it — and we can't arm it without panicking.

Change

Swap the tracker clock to web_time::Instant, a drop-in that re-exports std::time::Instant verbatim on native and WASI and is backed by performance.now() on wasm32-unknown-unknown. No behavior change on any currently-supported target.

Verification

  • Native: all resource-limit tests pass unchanged (incl. every timeout_* case and suspension_time_does_not_count_toward_max_duration).
  • wasm32-wasip1 (supported wasm target) and native compile clean; clippy -D warnings clean.
  • web_time::Instant confirmed compiling for wasm32-unknown-unknown with monty's exact usage (Instant::now(), Cell<Option<Instant>>, .elapsed()).

I see the time code is in flux for the 0.19 subprocess model (#483) — happy to retarget or adjust timing to suit.


Summary by cubic

Switch the resource tracker to web-time::Instant so max_duration works on wasm32-unknown-unknown without panicking. No behavior change on native or wasm32-wasip1; restores duration limits for browser-embedded monty.

  • Bug Fixes

    • Replace std::time::Instant with web_time::Instant in the tracker to avoid Instant::now() panic on wasm32-unknown-unknown.
    • Enables reliable timeouts for non-allocating runaways (e.g., while True: pass) in browser builds.
  • Dependencies

    • Add web-time v1.1 as a target-gated dependency for wasm32-unknown-unknown; import web_time::Instant only there and use std::time::Instant everywhere else.

Written for commit ba9ba9a. Summary will update on new commits.

Review in cubic

…wasm32-unknown-unknown

`LimitedTracker` enforces `ResourceLimits::max_duration` via `std::time::Instant`.
On `wasm32-unknown-unknown`, `Instant::now()` panics ("time not implemented on
this platform"), so any run configured with a duration limit aborts. monty's
wasm CI targets `wasm32-wasip1`, where `Instant` is implemented, so this path is
green upstream and the gap is invisible there.

Consumers that embed the `monty` crate directly as a `wasm32-unknown-unknown`
wasm-bindgen module (in-process, browser-side Python) can't currently arm a
duration limit without panicking — and `max_duration` is the only limit that
stops a non-allocating runaway such as `while True: pass` (`max_allocations` /
`max_memory` never fire on it).

Swap the tracker clock to `web_time::Instant`, a drop-in that re-exports
`std::time::Instant` verbatim on native and WASI and is backed by
`performance.now()` on `wasm32-unknown-unknown`. No behavior change on any
currently-supported target; all resource-limit tests pass unchanged and
`wasm32-wasip1` still compiles.
Copilot AI review requested due to automatic review settings July 10, 2026 11:47

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR makes Monty’s ResourceLimits::max_duration enforcement work on wasm32-unknown-unknown by switching the resource tracker clock from std::time::Instant (which panics on that target) to web_time::Instant (which is backed by performance.now() in browsers and re-exports std::time::Instant on native/WASI).

Changes:

  • Replace std::time::Instant usage in the resource tracker with web_time::Instant to avoid Instant::now() panics on browser wasm builds.
  • Add the web-time dependency to the monty crate.
  • Update Cargo.lock to include the new dependency.

Reviewed changes

Copilot reviewed 2 out of 3 changed files in this pull request and generated 2 comments.

File Description
crates/monty/src/resource.rs Switches the tracker clock to web_time::Instant so duration limits don’t panic on wasm32-unknown-unknown.
crates/monty/Cargo.toml Adds the web-time dependency to support a portable monotonic clock.
Cargo.lock Records the resolved web-time dependency in the lockfile.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread crates/monty/src/resource.rs Outdated
Comment on lines +3 to +9
// `web_time::Instant` is `std::time::Instant` on native and WASI targets, and a
// `performance.now()`-backed clock on `wasm32-unknown-unknown`, where the std
// `Instant::now()` panics ("time not implemented on this platform"). This keeps
// the `max_duration` time limit working when `monty` is embedded directly in a
// browser wasm module. Time behavior on all currently-supported targets is
// unchanged (it re-exports std verbatim there).
use web_time::Instant;
Comment thread crates/monty/Cargo.toml Outdated
Comment on lines +45 to +50
# Portable monotonic clock: a drop-in for `std::time::Instant` that re-exports
# std unchanged on native and WASI, and is backed by `performance.now()` on
# `wasm32-unknown-unknown` (where `std::time::Instant::now()` panics). Keeps the
# `max_duration` resource limit usable when `monty` is embedded directly in a
# browser wasm module rather than run over WASI.
web-time = "1.1"

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 3 files

Re-trigger cubic

@codecov

codecov Bot commented Jul 10, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@codspeed-hq

codspeed-hq Bot commented Jul 10, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 26 untouched benchmarks
⏩ 16 skipped benchmarks1


Comparing Butch78:fix/portable-tracker-clock (ba9ba9a) with main (fe8dc5e)

Open in CodSpeed

Footnotes

  1. 16 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@davidhewitt

Copy link
Copy Markdown
Contributor

This seems reasonable to me. Should we make it target-specific dependency just to avoid it where it's unnecessary?

I'm also curious about your use-case. It sounds like the work I just did in #525 will probably be relevant for you, which uses wasip1 in a web worker to do the actual execution for better isolation?

Per review: web_time::Instant is only needed where std::time::Instant panics,
i.e. wasm32-unknown-unknown. Gate the dependency and the import on that target
so every other target (native, WASI) uses std directly and never pulls web-time
into its graph. Verified: web-time is absent from the native and wasm32-wasip1
dependency graphs and present only under wasm32-unknown-unknown; both compile and
all resource-limit tests still pass.
@Butch78

Butch78 commented Jul 10, 2026

Copy link
Copy Markdown
Contributor Author

Good tip, I made it target specific!

On the use-case @davidhewitt : I embed the monty crate directly as a wasm32-unknown-unknown wasm-bindgen module for in-process, browser-side Python in a Spreadsheet cell. It runs synchronously on the eval thread with its own linear memory, and max_duration is the only limit that stops a non-allocating runaway like while True: pass (which never trips max_allocations/max_memory), hence needing the clock not to panic.

The results have been amazing, I'm actually lazy loading the binary and even with a bad connection It works nearly instantly

I'm also run a second tier for the harder cases: I'm experimenting with monty-cpython in a container on Cloudflare (via monty-pool's subprocess model) that browser sessions escalate to, so a user whose cell needs full CPython (numpy, pandas, unsupported stdlib) gets a real interpreter instead of hitting the in-browser sandbox's limits. I'm already tracking the 0.19 beta line (currently pinned at 0.0.19-beta.2), so your subprocess / monty-cpython work is very much in my plan. The in-browser monty wasm stays the fast default, and anything it cannot run transparently falls through to CPython-in-container.

And yes, #525 looks very relevant. The wasip1-in-a-web-worker model would give us a real hard-kill (terminating the worker) and a working clock for free, which is compelling but the problem is Cloudfare don't really support wasip1 yet....

I am are currently using the in-process wasm-bindgen embedding, so moving to that shape is a larger change we are weighing; this fix unblocks the cooperative time budget on our current architecture in the meantime.

I had a call with @samuelcolvin and he recommended focusing on browser first, I'm very close to that and would love to have call with you if wanted to see a demo @davidhewitt I'm hoping to soon have a beta out so people can try :)

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.

3 participants