Skip to main content

CascadeBudget

Struct CascadeBudget 

Source
#[non_exhaustive]
pub struct CascadeBudget { pub total: u64, }
Expand description

The runaway-detection budget for one scene’s change fan-out — how much work observers may generate from one batch before the drain declares the cascade non-terminating and panics.

§Why a budget at all

The drain runs until its queue is empty, which is what lets an observer read the scene and write it back. An observer whose write never converges therefore never empties it, and — unlike Signal::try_set, which recurses and so blows the stack loudly on its own — this is an iterative loop whose only other exit is an empty queue. Unchecked it is a frozen UI thread with no diagnostic. The limit is enforced in every build profile for that reason.

“Never converges” is the precise condition, and it is not the same as “unguarded”. The geometry mutators already suppress a write that changes nothing — Scene::set_local_pos returns without emitting when the position is unchanged — so an observer that keeps writing the same position settles on its own. A geometry cascade that does not settle is one computing a different value every time: an accumulating offset, a rounding drift, a spring with no rest state. The set_a11y_* mutators do not self-suppress; they bump on every call, so there an equality check before the call is a real guard.

§One trip condition, and why it is flat

Total observer-generated deliveries in a single drain, counted across both channels and every subject. Nothing else trips.

Two earlier shapes of this budget each fixed the previous one’s false positive and bought a worse problem, and the third is why this one is flat:

  • A cap on rounds aborted a legitimate 300-link settling chain, because a round is what was queued when it began, so an N-link chain costs N rounds.
  • A cap per subject moved the limit onto fan-in: a guarded relaxation over a hub passed at 4000 incident edges and aborted at 4200.
  • Scaling the per-subject cap with the scene’s entry count fixed the fan-in false positive and destroyed the guard, because the bound it resolved grew with the model.

A flat total is the only one of the three that bounds a runaway to the same amount of work whatever the scene’s size. Measured in release, changing only this budget: an unguarded write-back aborts after 100 001 deliveries in a 10 000-entry scene and in a 50 000-entry one alike, where the budget the scaled design resolved for those scenes (640 000 and 3 200 000) let the same loop run 6× and 32× longer — 3.10 s and 131 s against 520 ms and 3.95 s. A spawner allocates ~100 000 items before tripping at either size, against 640 002 and 3 200 002 under the scaled budget.

(The flat write-back’s wall clock still grows with the scene, and that is the app’s write, not the drain: Scene::set_local_pos re-buckets the moved subtree, which walks every entry. The identical runaway on the logical-AT channel, whose mutator does not, aborts in 4.8 ms at both sizes — that is what this loop itself costs. The budget bounds how many times an observer’s write runs, not what one costs.)

The default also clears every legitimate shape this tier runs by more than an order of magnitude: a 4200-edge guarded aggregate is about 4 200 deliveries, and a 2000-link chain that re-places eight port magnets and refreshes three AT properties per link is about 24 000.

§What it still does not decide

“Guarded cascade that is genuinely enormous” and “unguarded write-back” are not distinguishable from the queue alone. This budget does not pretend otherwise: it draws the line where the shapes this tier actually runs stop, reports the subject with the highest delivery count so the diagnostic points somewhere, and SceneModel::set_cascade_budget is the supported answer for a graph that genuinely lives past it. Mechanism in the framework, policy in the consumer.

§What is exempt

The caller’s own batch — everything already queued when the drain began — is not charged, however large. A bulk load, a ten-thousand-item teardown, or a scene-wide a11y re-tag inside one SceneWriteGuard is finite by construction, so charging it would make the budget a cap on batch size instead — the mistake a plain delivery counter makes, whose fix is to raise it until it catches nothing. Cascade depth and the number of distinct subjects a cascade touches are not limited either.

use teksilo_scene::{CascadeBudget, SceneModel};

let model = SceneModel::new();
model.set_cascade_budget(CascadeBudget::new(1_000_000));
assert_eq!(model.cascade_budget().total, 1_000_000);

#[non_exhaustive]: a budget is built through CascadeBudget::new or Default, never by struct literal, so a second dimension (a per-subject cap, a depth limit) can be added without breaking a caller.

Fields (Non-exhaustive)§

This struct is marked as non-exhaustive
Non-exhaustive structs could have additional fields added in future. Therefore, non-exhaustive structs cannot be constructed in external crates using the traditional Struct { .. } syntax; cannot be matched against without a wildcard ..; and struct update syntax will not work.
§total: u64

Observer-generated deliveries one drain may make before it panics.

Flat: not scaled by the scene’s entry count, by design — see the type’s own docs for the measurements that settled that. Default 100 000.

Zero is not a usable budget and is clamped to 1 by CascadeBudget::new and by SceneModel::set_cascade_budget.

Implementations§

Source§

impl CascadeBudget

Source

pub fn new(total: u64) -> Self

A budget of total observer-generated deliveries per drain, clamped up to the smallest usable value.

The clamp is here rather than at the trip so that a nonsensical budget is rejected where it is written, instead of producing a panic whose numbers describe no cascade that happened.

Trait Implementations§

Source§

impl Clone for CascadeBudget

Source§

fn clone(&self) -> CascadeBudget

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Copy for CascadeBudget

Source§

impl Debug for CascadeBudget

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Default for CascadeBudget

Source§

fn default() -> Self

Returns the “default value” for a type. Read more
Source§

impl Eq for CascadeBudget

Source§

impl PartialEq for CascadeBudget

Source§

fn eq(&self, other: &CascadeBudget) -> bool

Equality operator ==. Read more
1.0.0 (const: unstable) · Source§

fn ne(&self, other: &Rhs) -> bool

Inequality operator !=. Read more
Source§

impl StructuralPartialEq for CascadeBudget

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
§

impl<T> AnyEq for T
where T: Any + PartialEq,

§

fn equals(&self, other: &(dyn Any + 'static)) -> bool

§

fn as_any(&self) -> &(dyn Any + 'static)

Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
§

impl<T> Downcast for T
where T: Any,

§

fn into_any(self: Box<T>) -> Box<dyn Any>

Convert Box<dyn Trait> (where Trait: Downcast) to Box<dyn Any>. Box<dyn Any> can then be further downcast into Box<ConcreteType> where ConcreteType implements Trait.
§

fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>

Convert Rc<Trait> (where Trait: Downcast) to Rc<Any>. Rc<Any> can then be further downcast into Rc<ConcreteType> where ConcreteType implements Trait.
§

fn as_any(&self) -> &(dyn Any + 'static)

Convert &Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot generate &Any’s vtable from &Trait’s.
§

fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)

Convert &mut Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot generate &mut Any’s vtable from &mut Trait’s.
§

impl<T> Downcast<T> for T

§

fn downcast(&self) -> &T

§

impl<T> DowncastSync for T
where T: Any + Send + Sync,

§

fn into_any_arc(self: Arc<T>) -> Arc<dyn Any + Send + Sync> ⓘ

Convert Arc<Trait> (where Trait: Downcast) to Arc<Any>. Arc<Any> can then be further downcast into Arc<ConcreteType> where ConcreteType implements Trait.
§

impl<Q, K> Equivalent<K> for Q
where Q: Eq + ?Sized, K: Borrow<Q> + ?Sized,

§

fn equivalent(&self, key: &K) -> bool

Checks if this value is equivalent to the given key. Read more
§

impl<Q, K> Equivalent<K> for Q
where Q: Eq + ?Sized, K: Borrow<Q> + ?Sized,

§

fn equivalent(&self, key: &K) -> bool

Compare self to key and return true if they are equal.
§

impl<T> ErasedDestructor for T
where T: 'static,

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

§

impl<T> Instrument for T

§

fn instrument(self, span: Span) -> Instrumented<Self> ⓘ

Instruments this type with the provided [Span], returning an Instrumented wrapper. Read more
§

fn in_current_span(self) -> Instrumented<Self> ⓘ

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ

Converts 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 more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
where F: FnOnce(&Self) -> bool,

Converts 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
§

impl<T> NoneValue for T
where T: Default,

§

type NoneType = T

§

fn null_value() -> T

The none-equivalent value.
§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
§

impl<T> Upcast<T> for T

§

fn upcast(&self) -> Option<&T>

§

impl<T> WasmNotSend for T
where T: Send,

§

impl<T> WasmNotSendSync for T
where T: WasmNotSend + WasmNotSync,

§

impl<T> WasmNotSync for T
where T: Sync,

§

impl<T> WithSubscriber for T

§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self> ⓘ
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a [WithDispatch] wrapper. Read more
§

fn with_current_subscriber(self) -> WithDispatch<Self> ⓘ

Attaches the current default Subscriber to this type, returning a [WithDispatch] wrapper. Read more