Merge pull request #6584 from dotCooCoo/fix/web-plugin-budget-clock

Plug-ins: the web build times the first-view budget and each run's limit
This commit is contained in:
BFlat
2026-11-11 19:14:55 +07:00
committed by GitHub
10 changed files with 44 additions and 37 deletions
+4
View File
@@ -22,6 +22,10 @@ craft-vectorcraft-format = { workspace = true }
sha2 = { version = "0.10", default-features = false }
thiserror = { workspace = true }
wasmi = { workspace = true }
# The monotonic clock of run deadlines and the first-view budget: `std::time::Instant` on the
# desktop, the page's `performance.now()` in the web build. It is MIT OR Apache-2.0 and already in
# the build through eframe.
web-time = "1"
[dev-dependencies]
wat = { workspace = true }
+14 -11
View File
@@ -86,20 +86,15 @@ pub struct Refreshed {
pub failed: usize,
}
/// Milliseconds on a monotonic clock, `None` where there is none (the web build).
/// Milliseconds on a monotonic clock, `None` from a clock that cannot be read.
pub type Clock = Arc<dyn Fn() -> Option<u64> + Send + Sync>;
/// Milliseconds since the first reading in this process (native builds); `None` on the web build,
/// whose `std` has no monotonic clock.
/// Milliseconds since the first reading in this process, from `web_time::Instant`: the system's
/// monotonic clock on the desktop and the page's `performance.now()` in the web build.
fn monotonic() -> Option<u64> {
#[cfg(not(target_arch = "wasm32"))]
{
static START: std::sync::OnceLock<std::time::Instant> = std::sync::OnceLock::new();
let start = START.get_or_init(std::time::Instant::now);
Some(u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX))
}
#[cfg(target_arch = "wasm32")]
None
static START: std::sync::OnceLock<web_time::Instant> = std::sync::OnceLock::new();
let start = START.get_or_init(web_time::Instant::now);
Some(u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX))
}
#[derive(Clone, Copy, Debug, PartialEq)]
@@ -579,6 +574,14 @@ mod tests {
});
}
/// The default clock, which the web build also reads, gives the first view its budget.
#[test]
fn the_default_clock_gives_the_first_view_a_budget() {
let d = Deferrals::new();
assert!(matches!(d.allowance(), Allowance::Until { ms: OPENING_BUDGET_MS, .. }));
assert!(!d.passed(u64::MAX));
}
/// A stage drawn with several inputs (the paths of a group) holds one record for each, and is
/// listed and counted once.
#[test]
+1 -2
View File
@@ -24,8 +24,7 @@ use crate::{Error, Kind, Limits, Plugin, Result, registry};
/// document: 200 times a filter's base budget.
pub const FUEL: u64 = 10_000_000_000;
/// Wall-clock budget of one export, in milliseconds (native builds; the web build has no clock,
/// and the instruction budget alone bounds it there).
/// Wall-clock budget of one export, in milliseconds.
pub const WALL_TIME_MS: u64 = 300_000;
/// Most warnings an export reports.
+14 -14
View File
@@ -321,29 +321,20 @@ pub struct Report {
/// The limit a run past its wall-clock deadline exceeded ([`Error::Limit`]).
pub(crate) const TIME_BUDGET: &str = "time budget";
/// When a run must stop (native: a wall-clock deadline).
/// When a run must stop: a deadline on the monotonic clock (`web_time::Instant`, the page's
/// `performance.now()` in the web build).
#[derive(Clone, Copy)]
pub(crate) struct Deadline {
#[cfg(not(target_arch = "wasm32"))]
at: std::time::Instant,
at: web_time::Instant,
}
impl Deadline {
pub(crate) fn new(wall_time_ms: u64) -> Self {
#[cfg(target_arch = "wasm32")]
let _ = wall_time_ms;
Self {
#[cfg(not(target_arch = "wasm32"))]
at: std::time::Instant::now() + std::time::Duration::from_millis(wall_time_ms),
}
Self { at: web_time::Instant::now() + std::time::Duration::from_millis(wall_time_ms) }
}
pub(crate) fn passed(&self) -> bool {
#[cfg(not(target_arch = "wasm32"))]
if std::time::Instant::now() >= self.at {
return true;
}
false
web_time::Instant::now() >= self.at
}
pub(crate) fn check(&self) -> Result<()> {
@@ -816,6 +807,15 @@ mod tests {
Run::new("test", true, false, Deadline::new(60_000))
}
/// A deadline reads the monotonic clock that the web build also reads: one of 0 ms has
/// passed, and one a day away has not.
#[test]
fn a_deadline_passes_on_the_monotonic_clock() {
assert!(Deadline::new(0).passed());
assert!(matches!(Deadline::new(0).check(), Err(Error::Limit(l)) if l == TIME_BUDGET));
assert!(!Deadline::new(86_400_000).passed());
}
/// Progress is clamped: negative counts are 0 and `done` never passes a known total.
#[test]
fn progress_is_clamped() {
+1 -1
View File
@@ -9,7 +9,7 @@
//! permission): no WASI, file system, network, clock or randomness. A module that
//! imports anything else is rejected;
//! - an **instruction budget** (fuel) per call, metered in slices so a wall-clock deadline is
//! also enforced (native builds), so infinite loops end with an error. A filter may ask for a
//! also enforced, so infinite loops end with an error. A filter may ask for a
//! larger one ([`Budget::Heavy`]), which the host lets the user allow before installing it;
//! - a **linear-memory cap**, a **recursion-depth cap** and wasmi's strict module limits;
//! - a **fresh instance per run**, so no state leaks between calls; what a plug-in keeps is only
+3 -3
View File
@@ -29,7 +29,7 @@ const FUEL_COSTS: CustomFuelCosts = CustomFuelCosts { bytes_copied_per_fuel: 64,
/// the host learns that only once the run has ended.
pub const HEAVY_FUEL: u64 = 1_000_000_000;
/// Wall-clock budget of a heavy filter's run, in milliseconds (native builds).
/// Wall-clock budget of a heavy filter's run, in milliseconds.
pub const HEAVY_WALL_TIME_MS: u64 = 300_000;
/// The limit a run reports when its input and parameters together are past half its memory.
@@ -56,8 +56,8 @@ pub struct Limits {
pub fuel_per_byte: u64,
/// Deepest call nesting.
pub max_recursion_depth: usize,
/// Wall-clock budget for one run, in milliseconds. Native builds only: the web build has no
/// monotonic clock in `std`, so the fuel budget alone bounds it there.
/// Wall-clock budget for one run, in milliseconds. The web build measures it with the page's
/// `performance.now()`.
pub wall_time_ms: u64,
/// The most a heavy filter ([`crate::Budget::Heavy`]) is given instead of `fuel_base` and
/// `wall_time_ms`: what the host allows, which the module's manifest can ask for but never