mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-08 16:30:41 +00:00
Merge pull request #6190 from dotCooCoo/fix/plugin-bulk-memory-fuel
Plug-ins: a run keeps the fuel left in a slice at a bulk memory instruction
This commit is contained in:
@@ -239,6 +239,12 @@ impl Plugin {
|
||||
|
||||
/// Calls `f` with at most `fuel` instructions, in slices, checking the deadline and the
|
||||
/// caller's cancellation ([`vectorcraft_doc::cancel`]) between them.
|
||||
///
|
||||
/// wasmi pauses before an instruction that the store's fuel can't pay for, and charges nothing
|
||||
/// for it. It charges a `memory.fill` or `memory.copy` for its whole length before writing, so
|
||||
/// up to that cost can be left in the store at a pause. Each slice tops the store up to the
|
||||
/// slice size, keeping that fuel, and a run ends with the instruction budget only when the
|
||||
/// store's fuel and the rest of the budget together can't pay for the next instruction.
|
||||
fn call<P: WasmParams, R: WasmResults>(&self, live: &mut Live, f: &TypedFunc<P, R>, params: P, fuel: u64, deadline: &Deadline) -> Result<R> {
|
||||
let mut left = fuel;
|
||||
let give = left.min(FUEL_SLICE);
|
||||
@@ -251,14 +257,16 @@ impl Plugin {
|
||||
TypedResumableCall::HostTrap(_) => return Err(Error::Trap("unexpected host trap".into())),
|
||||
TypedResumableCall::OutOfFuel(inv) => {
|
||||
let need = inv.required_fuel();
|
||||
if left == 0 || need > left {
|
||||
let held = live.store.get_fuel().map_err(wasm_err)?;
|
||||
let total = held.saturating_add(left);
|
||||
if need > total {
|
||||
return Err(Error::Limit("instruction budget".into()));
|
||||
}
|
||||
deadline.check()?;
|
||||
vectorcraft_doc::cancel::check().map_err(|_| Error::Cancelled)?;
|
||||
let give = left.min(FUEL_SLICE.max(need));
|
||||
left -= give;
|
||||
live.store.set_fuel(give).map_err(wasm_err)?;
|
||||
let fill = total.min(FUEL_SLICE.max(need)).max(held);
|
||||
left -= fill - held;
|
||||
live.store.set_fuel(fill).map_err(wasm_err)?;
|
||||
call = inv.resume(&mut live.store).map_err(wasm_err)?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,6 +155,45 @@ fn budgets_stop_runaway_plugins() {
|
||||
assert_eq!(e, Error::Limit("instruction budget".into()));
|
||||
}
|
||||
|
||||
/// A filter that runs `op`, an 8 MiB bulk memory instruction costing about 131,000 instructions,
|
||||
/// 160 times (about 21 million instructions in all), or forever when `endless`.
|
||||
fn bulk_module(op: &str, endless: bool) -> Vec<u8> {
|
||||
let bulk = match op {
|
||||
"fill" => "(memory.fill (i32.const 1024) (i32.const 7) (i32.const 8388608))",
|
||||
"copy" => "(memory.copy (i32.const 1024) (i32.const 2048) (i32.const 8387584))",
|
||||
_ => panic!("no bulk operation `{op}`"),
|
||||
};
|
||||
let run = if endless {
|
||||
format!("(loop $work {bulk} (br $work)) {}", tpl::ECHO)
|
||||
} else {
|
||||
format!(
|
||||
"(local $n i32) (local.set $n (i32.const 160)) \
|
||||
(loop $work {bulk} (local.set $n (i32.sub (local.get $n) (i32.const 1))) (br_if $work (local.get $n))) {}",
|
||||
tpl::ECHO
|
||||
)
|
||||
};
|
||||
build(&tpl::module(MANIFEST, &run, "", 130))
|
||||
}
|
||||
|
||||
fn bulk_run(op: &str, endless: bool, fuel_base: u64) -> vectorcraft_plugins::Result<Vec<u8>> {
|
||||
let p = Plugin::load(&bulk_module(op, endless), Limits { fuel_base, fuel_per_byte: 0, ..Limits::default() })?;
|
||||
p.run(INPUT.as_bytes(), b"{}")
|
||||
}
|
||||
|
||||
/// A bulk memory instruction that the fuel left in a slice of the budget can't pay for pauses the
|
||||
/// run without using that fuel, and the run keeps it: 160 fills or copies that cross the first
|
||||
/// slice finish within 21 million instructions, though the pause left about 75,000 of them in the
|
||||
/// slice. With fewer instructions than the work takes, or with a loop that never ends, the run
|
||||
/// still ends with the instruction budget.
|
||||
#[test]
|
||||
fn a_bulk_memory_instruction_at_the_end_of_a_slice_keeps_the_run_s_fuel() {
|
||||
for op in ["fill", "copy"] {
|
||||
assert_eq!(bulk_run(op, false, 21_000_000), Ok(INPUT.as_bytes().to_vec()), "{op} within its budget");
|
||||
assert_eq!(bulk_run(op, false, 15_000_000), Err(Error::Limit("instruction budget".into())), "{op} over its budget");
|
||||
assert_eq!(bulk_run(op, true, 21_000_000), Err(Error::Limit("instruction budget".into())), "endless {op}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wall_clock_budget_stops_runaway_plugins() {
|
||||
let spin = ok_module("(loop $l (br $l)) (i64.const 0)");
|
||||
|
||||
@@ -1478,12 +1478,14 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- **Envelope names given as another type:** `object.envelope.makeWithWarp`, `resetWithWarp` and `options` refuse a `style` or `orientation` other than text or null, such as `5`, and an `orientation` other than `horizontal` or `vertical`; `object.envelope.options` refuses such a `preserveShape`. `object.envelope.options` checks the warp keys with no envelope or only mesh envelopes selected, too. Each leaves the document, the options new envelopes get and the undo history as they were.
|
||||
|
||||
- **Exporter plug-ins (G17):** a WebAssembly plug-in of kind `exporter` adds a write-only file format. File › Export › Export As lists it after the built-in formats, with Use Artboards greyed out (the plug-in writes the whole document) and its parameters in a generated dialog once the file is picked; `document.export` writes it by its format id, `plugin.<id>`, or by one of its extensions, and built-in formats keep theirs. The plug-in is given the native file's JSON (large images without their data) and `_context` (`mode`, `extension`, `name`), with 10 G instructions plus 4,000 per byte of input and 300 s, and Background Export's Cancel stops it between two slices of that budget. A decline or invalid output writes nothing; its warnings come back with the export's. Regressions: `crates/engine/tests/plugin_export.rs`, `crates/plugins/tests/abi.rs`, `crates/ui-egui/src/dialogs/tests_plugin_export.rs`. Scores and hours unchanged.
|
||||
- **Plug-in instruction budget at a bulk memory instruction:** a run that pauses where one slice of its instruction budget ends keeps the instructions the slice still held. A `memory.fill` or `memory.copy` that the slice can't pay for in full runs on the next slice, so a run whose budget covers its work finishes; a run ends with `Limit("instruction budget")` only when the instructions it has left can't pay for the next instruction. Regressions: `crates/plugins/tests/abi.rs`. Scores and hours unchanged.
|
||||
|
||||
- **Plug-in parameters as their manifest presents them (G17):** each parameter may have a `label`, a `unit` shown after its value, `help` on its label and a `helpUrl` page opened from a button, a `group` (sections that fold, after the parameters outside any group) and a `visibleWhen` that shows it only while the checkboxes and choices it names have one of the listed values (a hidden parameter still goes to the plug-in). `string` and `numberList` parameters join number, int, bool and choice, and a plug-in declares up to 256 parameters, at most 64 outside groups. The filter, live-effect and exporter dialogs use these keys, and the Appearance panel's effect editor the labels, units, help and conditions; hosts from before them ignore them. Regressions: `crates/plugins/src/dialog.rs`, `crates/plugins/src/manifest.rs`, `crates/ui-egui/src/dialogs/tests_plugin_fields.rs`, `crates/ui-egui/src/panels/appearance.rs`. Scores and hours unchanged.
|
||||
- **Live-effect plug-ins read their object's paints (G17):** a live effect is given the object's `fills` and `strokes` as a filter gets them, read only, and `_context.item` (`"object"`, or the fill or stroke whose own effects hold it); the result is cached by paint too, so a change of paint runs it again. Regressions: `crates/engine/src/tests/plugins.rs`, `crates/plugins/src/effect.rs`. Scores and hours unchanged.
|
||||
- **Filter plug-ins set effects and stroke styles (G17):** a filter is given each object's effect records and its strokes' `join`, `cap` and `miterLimit`, and may return them; the records are admitted as `effect.apply` admits them (a known effect or installed live-effect plug-in, parameters it takes, one 3D effect that fits, no texture read from a file), and one refused refuses the output. Regressions: `crates/engine/src/tests/plugins.rs`, `crates/plugins/src/objects/tests.rs`. Scores and hours unchanged.
|
||||
- **Plug-in data kept with the document (G17):** a filter writes JSON of at most 1 MiB with `documentData` (one undo step with its objects; `null` removes it) and reads it back in `_context.documentData`, which no other filter is given. It is the document's foreign data (`unknown.pluginData`, by plug-in id), as the perspective grid is, so the native file keeps it with no section of its own; an exporter is given the whole document, so it can read every plug-in's. Regressions: `crates/engine/src/tests/plugins.rs`, `crates/engine/tests/plugin_export.rs` (the exporter), `crates/doc/src/plugin_data.rs`, `crates/plugins/src/objects/tests.rs`. Scores and hours unchanged.
|
||||
- **Heavy filter plug-ins (G17):** a filter's manifest may ask for `"budget": "heavy"`: 1 G instructions (20 times a light run's) and 300 s a run, for routing, packing or simulation. Install Plug-in… and File › Open ask the user first, `plugin.install` needs `allowHeavy: true` (`plugin.inspect` tells beforehand), the Additional Plug-ins Folder skips such filters, and the plug-in store keeps the answer. Its dialog has no Preview; the app runs it on a thread of its own with a progress window (Cancel, Continue in Background) and applies the result as `plugin.run` with the same parameters, one undo step, unless the document or the plug-in changed meanwhile. Regressions: `crates/plugins/tests/abi.rs`, `crates/plugins/src/manifest.rs`, `crates/engine/src/tests/plugins.rs`, `crates/ui-egui/src/tests/plugin_runs.rs`. Scores and hours unchanged.
|
||||
- **Plug-in instruction budget at a bulk memory instruction:** a run that pauses where one slice of its instruction budget ends keeps the instructions the slice still held. A `memory.fill` or `memory.copy` that the slice can't pay for in full runs on the next slice, so a run whose budget covers its work finishes; a run ends with `Limit("instruction budget")` only when the instructions it has left can't pay for the next instruction. Regressions: `crates/plugins/tests/abi.rs`. Scores and hours unchanged.
|
||||
- **Plug-in permissions (G17):** a manifest may list `permissions` (`doc.read`, `doc.write`, `state`, `effect.render`, `export.write`; the first three follow storytold/photocraft#662) and an `api` level; without the list a plug-in gets what its kind implies and installs as before. Install Plug-in… and File › Open show the name, id, version, file SHA-256 and each permission in plain words, with control and bidirectional characters escaped, and install nothing on Cancel; `plugin.install` needs `grant`, and folders install only granted plug-ins. The grant is kept with the module's hash, a module asking for more asks again, and a filter run refuses output its permissions don't allow. Regressions: `crates/plugins/src/permissions.rs`, `crates/plugins/src/manifest.rs`, `crates/engine/src/tests/plugins.rs`, `crates/engine/tests/plugin_fuzz.rs`, `crates/ui-egui/src/tests/plugins.rs`. Scores and hours unchanged.
|
||||
- **Plug-in progress, Cancel and log lines (G17):** at plug-in API level 2 a plug-in with the `progress` permission imports `env.vc_progress`, `env.vc_cancelled` and `env.vc_log`. A heavy filter's and an export's progress windows show its progress as a percentage bar, their Cancel reaches it through `vc_cancelled`, and its log lines go to the app's log with its id, capped per line and per run. Installing checks every import and names the one it refuses. Regressions: `crates/plugins/src/host.rs`, `crates/plugins/tests/abi.rs`, `crates/engine/src/tests/plugins.rs`, `crates/engine/tests/plugin_fuzz.rs`, `crates/ui-egui/src/tests/plugin_runs.rs`. Scores and hours unchanged.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user