pub struct PluginBudget {
pub fuel: u64,
pub epoch_deadline: u64,
}Expand description
Per-call resource budget. Both limits are hard: whichever is hit first traps the call cleanly.
Fields§
§fuel: u64Fuel (≈ units of work) a single lifecycle call may consume before it
traps with TrapKind::Fuel.
epoch_deadline: u64Milliseconds of WALL CLOCK a single call may run before it traps with
TrapKind::Epoch.
OA.0b made that literal. It used to be enforced by counting deadline-
callback firings, which equals elapsed milliseconds only while the
guest executes guest code continuously — one epoch checkpoint crossed
per [EPOCH_TICK_INTERVAL]. A guest that calls host imports in a loop
is suspended for most of its wall clock and crosses far fewer, so the
budget stretched by the ratio of host time to guest time: an org agenda
scan ran 6.8 s against this set to 1_000 and never tripped.
The new accounting is stricter, and one consequence is worth
knowing. Time the OS spends descheduling the thread now counts
against the call, where firing-count quietly forgave it. That is the
correct measure for the thing this guards — the editor was stalled
either way — but if a sync grammar contribution ever false-trips under
load, the fix is to raise Self::grammar’s deadline, NOT to go back
to counting firings. Fuel is the primary bound on that path by design.
Implementations§
Source§impl PluginBudget
impl PluginBudget
Sourcepub fn grammar() -> Self
pub fn grammar() -> Self
The Reflex-class budget for the synchronous grammar trampoline
(PH7.7c; plugin-host.md §7 + audit F1). Grammar apply / parse_args
run on the keystroke path (the PH7.7 fork), so — unlike the generous
lifecycle/async default (~1s epoch) — a plugin
contribution must not stall the keystroke.
Fuel is the primary Reflex bound. A grammar guest runs on the sync
linker with no async host import, so it cannot block (no I/O to await) —
it can only compute or spin, and both are fuel-bounded. 10M fuel is
~one display frame of compute (Cranelift-compiled), ample for a real
motion’s arithmetic but a hard cap on a runaway loop; that is what keeps a
plugin motion off the keystroke’s critical path.
Epoch is a jitter-proof wall-clock backstop, not the tripwire. The
ticker granularity is 1ms ([EPOCH_TICK_INTERVAL]); a 2-tick deadline
false-positives when the OS deschedules the dispatch thread mid-call
(observed under a criterion warmup’s millions of iterations). So the epoch
is set generously — it must never trip on scheduling jitter, and only
catch the pathological case fuel somehow misses (near-impossible for a
sync compute guest). A trap of either kind is caught by
the trampoline → the contribution is a no-op with a warn
(CommandError::Plugin), never a
hang. Armed before every guest call — distinct from the lifecycle/producer
budget by design (audit F1).
50ms was not jitter-proof, so this is 250. A trivial org
:org-clock-in — read a line, rewrite it, emit an event — overran 50ms
and trapped whenever the machine was saturated (reproduced with one busy
loop per core). That is precisely the false-trip this deadline’s own
prose said must not happen, and the fix it prescribed: raise the
deadline, never go back to counting firings.
250ms costs nothing real. Fuel remains the actual bound at ~one frame of
compute, so a runaway still dies on fuel in milliseconds; the epoch only
has to exceed the worst descheduling window, and 5× the observed failure
is that with room. The number is also no longer a cliff: since the strike
count in [Quarantine::trip], one overrun costs that call rather than
the plugin.
Sourcepub fn event() -> Self
pub fn event() -> Self
The budget for a plugin event handler (on-event, PH7.8c; §7 “major-
mode event handler”). Unlike grammar’s Reflex budget, event delivery is
off the keystroke path (async, on the plugin’s own actor task), and a
handler runs on the async linker — so it may legitimately await a
capability-gated host-services call. A tight sub-frame epoch would
false-trip such a suspend, so the epoch is a generous backstop (~1s,
the lifecycle default) and fuel is the primary bound: 100M ≈ ~10
frames of compute, ample for a real hook (recolour a gutter, index a
symbol) yet a hard cap on a runaway loop. A trap is caught per-delivery by
the EventActor → the delivery is
skipped with a warn, the plugin stays subscribed, other subscribers are
untouched (§8). The marshalling+dispatch overhead itself is the CI-gated
< 250µs p99 row (PH7.8d), distinct from this runaway guard.
5s, not 1s, and the reason is what the epoch measures. Since OA.0b
the deadline counts wall time including the time the guest sits BLOCKED
in a host import — correct, because a guest looping over imports was
otherwise unbounded (an agenda scan ran 6.8 s against a 1 s deadline
without tripping). The cost is that a handler is now charged for host
work it cannot make faster: org’s roam scan opens with one walk of the
corpus, and under a saturated machine that single import took 1106 ms —
so a healthy plugin trapped, and since a trap kills the instance
([Quarantine::trip]) the whole index died with it.
Raising it does not weaken what the guard is for. Fuel still caps guest COMPUTE at ~10 frames, so a spinning handler dies in milliseconds regardless; the epoch’s remaining job is bounding a guest that blocks in imports forever, and 5 s bounds that just as surely as 1 s. What it buys is that legitimate work — which a guest already self-limits to 250 ms per delivery — is 20× inside the budget instead of within noise of it.
Sourcepub fn decoration() -> Self
pub fn decoration() -> Self
The budget for a plugin decoration producer (gutter-decorations,
PH7.9). Like the event budget, decoration production is off the render
path (async, on the plugin’s actor task, triggered by an edit / scroll /
diagnostic change) and the producer runs on the async linker (it may
await a host-services call — a git-gutter source reading the repo). So
the epoch is a generous ~1 s backstop and fuel is the primary bound
(100M ≈ ~10 frames of compute — ample for a real diff/annotate pass, a
hard cap on a runaway). A trap is caught per-call by the
DecorationActor → the trigger
yields no decorations and the cached snapshot keeps its prior value (§8, no
flicker). The §7 < 50 µs p99 “segment update” gate is on the marshalling
- dispatch overhead, distinct from this runaway guard.
Sourcepub fn context() -> Self
pub fn context() -> Self
TC.2: budget for a context-scopes produce call. Deliberately the same
shape as Self::decoration and for the same reason — both are async
producers the host drives off the render path, so the guard is against a
runaway, not against latency. Context is the more expensive of the two
(a whole-buffer tree-sitter query rather than a per-line walk), and the
budget is generous enough that a legitimate query on a large file
finishes well inside it; context.max-file-lines is what bounds the
intended work, this is what bounds the unintended.
Trait Implementations§
Source§impl Clone for PluginBudget
impl Clone for PluginBudget
Source§fn clone(&self) -> PluginBudget
fn clone(&self) -> PluginBudget
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read moreimpl Copy for PluginBudget
Source§impl Debug for PluginBudget
impl Debug for PluginBudget
Auto Trait Implementations§
impl Freeze for PluginBudget
impl RefUnwindSafe for PluginBudget
impl Send for PluginBudget
impl Sync for PluginBudget
impl Unpin for PluginBudget
impl UnsafeUnpin for PluginBudget
impl UnwindSafe for PluginBudget
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
§impl<T> Instrument for T
impl<T> Instrument for T
§fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read more