StimulusEvent

Struct StimulusEvent 

Source
pub struct StimulusEvent {
    pub elapsed_ms: u64,
    pub label: String,
    pub op_kind: Option<String>,
    pub detail: Option<String>,
    pub total_iterations: Option<u64>,
    pub step_index: Option<u16>,
    pub is_terminal: bool,
    pub is_step_end: bool,
}
Expand description

A discrete event during scenario execution that may cause observable changes in scheduler behavior. Generated by step executors on the guest side and carried in the VM output alongside monitor samples.

Fields§

§elapsed_ms: u64

Milliseconds since scenario start (guest monotonic clock).

§label: String

Human-readable label. Produced as "StepStart[k]" by Self::from_wire (the 0-indexed scenario Step ordinal), "ScenarioEnd" by Self::terminal, and the "BASELINE"/"Step[k]" bucket label by the phase_from_bucket placeholder. Test fixtures may carry any label.

§op_kind: Option<String>

What kind of operation triggered this event.

§detail: Option<String>

Additional context (e.g. “4 cpus”, “cgroup=cg_0”).

§total_iterations: Option<u64>

Cumulative worker iterations at this event. Some(_) for every event built from the wire (the wire counter is always present — see Self::from_wire); a cumulative counter for which Some(0) is a legitimate “no iterations accumulated yet” baseline, NOT a missing sample. None only for synthetic / placeholder events that carry no counter (the phase_from_bucket fallback and test fixtures). Used to compute per-phase throughput (iterations/s) as the delta between consecutive events.

SEMANTICS: this is the sum of the iteration counters of the worker handles ALIVE at the event instant (step-local + Backdrop). Each step emits BOTH a StepStart event (counter at the step’s start) and a StepEnd event (Self::is_step_end, counter at the step’s end-of-hold), so the per-phase iteration_rate is the STEP-LOCAL delta StepEnd[k] - StepStart[k] — each step’s OWN workers measured start-to-end. That works for workers respawned per step (the cross-step StepStart[k+1] - StepStart[k] delta reads fresh~0 - fresh~0 and is dropped) AND is more accurate for persistent (Backdrop) workers (it excludes the inter-step teardown/respawn wall-time the cross-step window spanned). Bucket k is sourced ONLY by its StepStart[k] -> StepEnd[k] pair: the iteration_rate attribution loop in crate::assert::build_phase_buckets_with_stimulus skips any is_step_end prev, so a stalled step whose step-local delta is zero (StepEnd[k] == StepStart[k]) reports its MEASURED-ZERO rate Some(0.0) (see Self::rate_to) rather than leaking the inter-step gap rate from the StepEnd[k] -> StepStart[k+1] pair. The monitor-only Timeline::build fallback (no snapshot captures) computes the SAME step-local StepStart[k] -> StepEnd[k] rate — the StepEnd events reach it too (they are emitted independent of captures) — and falls back to cross-step (or the terminal for the last step) only when a step has no StepEnd (sched-died / legacy data); StepEnd is filtered only from that path’s phase LAYOUT, not its rate.

§step_index: Option<u16>

1-indexed scenario step this event belongs to (the same encoding the bridge stamps: 1..=N for Step ordinals), or None for non-step events (including the terminal scenario-end boundary; see is_terminal). Carried explicitly from the wire StimulusPayload.step_index so the periodic-capture phase attribution can map a capture’s workload-relative boundary offset onto the guest’s own step timeline without parsing the human-readable label.

§is_terminal: bool

True only for the synthetic scenario-end boundary the eval walker appends from the ScenarioEnd wire frame’s final total_iterations. On a CLEAN run the last step emits its own StepEnd[N], which supplies that step’s iteration_rate right boundary in BOTH rate consumers — the snapshot path (crate::assert::build_phase_buckets_with_stimulus, the StepStart[N] -> StepEnd[N] pair) and the monitor-only Timeline::build fallback (which looks up each step’s StepEnd by step_index) — and the terminal is then NOT consumed for a rate: the snapshot path’s attribution loop skips the (StepEnd[N], terminal) pair via its is_step_end guard (before rate_components is reached), and Timeline::build reaches for the terminal only when a step’s StepEnd lookup misses. The terminal is consumed as a step’s rate boundary ONLY for legacy/synthetic data that carries a ScenarioEnd frame but no StepEnd frames (fresh guest output always pairs them). A sched-died step is NOT such a case: its early return skips BOTH the StepEnd emission AND send_scenario_end, so neither frame exists and the dead step reports no rate via the no-successor path. It is NOT a step start: step_index is None so it seeds no crate::assert::PhaseBucket (excluded from the step-start timeline), and Timeline::build skips it when laying out phase boundaries so it never renders a phantom trailing phase.

§is_step_end: bool

True for a per-step END event (decoded from a crate::vmm::wire::MsgType::StepEnd frame via Self::from_step_end). It carries the SAME 1-indexed step_index as its StepStart and its step’s end-of-hold total_iterations, so crate::assert::build_phase_buckets_with_stimulus’s elapsed-sorted windows(2) pairs StepStart[k] -> StepEnd[k] first and or_insert keeps that step-local rate. NOT a step start, so Timeline::build (the monitor-only fallback’s index-based cross-step pairing) filters it out of its step-start list to avoid a phantom phase.

Implementations§

Source§

impl StimulusEvent

Source

pub fn from_wire(ev: &StimulusEvent) -> Self

Build a timeline event from a deserialized wire stimulus event. Centralizes the wire→timeline mapping so the production eval path (evaluate_vm_result) and out-of-tree consumers — post_vm callbacks folding VmResult::stimulus_timeline() (which calls this internally) through crate::assert::build_phase_buckets_with_stimulus — produce identical events. The wire step_index is the bridge 1-indexed convention (Step[k] -> k + 1, BASELINE owns 0); the human label renders the 0-indexed Scenario-Step ordinal (step_index - 1) to match the PhaseBucket Step[k] labels, while the step_index field keeps the 1-indexed wire value for phase-bucket remap. total_iterations is carried verbatim as Some(_): the wire field is a cumulative counter that is always populated (the guest sums live worker iterations at every step boundary), so 0 is a legitimate baseline reading — the FIRST step’s frame fires right after its workers spawn and genuinely reads ~0. Collapsing that 0 to None (the old behavior) made the (first, second) delta pair fail the Some/Some guard in both rate consumers, silently dropping the first step’s iteration_rate; carrying Some(0) lets the delta compute the first step’s throughput for the PERSISTENT (Backdrop) population (see the total_iterations field doc for the persistent-vs- step-local semantics this delta measures).

Source

pub fn from_step_end(ev: &StimulusEvent) -> Self

Build a per-step END event from a crate::vmm::wire::MsgType::StepEnd frame (reuses the crate::vmm::wire::StimulusEvent wire body). Carries the SAME 1-indexed step_index as the step’s StepStart and the step’s end-of-hold total_iterations, with is_step_end set. Elapsed-sorted, a step’s events order StepStart[k] (start) < StepEnd[k] (end-of-hold) < StepStart[k+1], so crate::assert::build_phase_buckets_with_stimulus’s windows(2) pairs StepStart[k] -> StepEnd[k] first and or_insert keeps that step-local rate. is_terminal is false (it is a real per-step boundary, not the scenario-end terminal).

Source

pub fn terminal(elapsed_ms: u64, total_iterations: u64) -> Self

Build the synthetic terminal boundary event from the ScenarioEnd wire frame’s final cumulative total_iterations and scenario-relative elapsed_ms. Appended once, after every per-step Self::from_wire event. On a clean run StepEnd[N] supplies the last step’s iteration_rate right boundary in both rate consumers and the terminal is not consumed for a rate; it is consumed as a step’s boundary ONLY for legacy/synthetic data with a ScenarioEnd frame but no StepEnd frames (a sched-died step has neither, since the early return skips both emissions) — see the Self::is_terminal field doc. step_index is None (it is not a step start — it seeds no crate::assert::PhaseBucket) and is_terminal is set so Timeline::build treats it as a right boundary only, never a phase. elapsed_ms is in the same guest-monotonic frame as the step events (both come from scenario_start.elapsed()), so the last-step duration is well-formed.

Source

pub fn rate_to(&self, next: &StimulusEvent) -> Option<f64>

Iterations-per-second from this event to next: (next.total_iterations - self.total_iterations) over the guest-clock elapsed-ms delta between them. Returns None ONLY when the measurement is genuinely undefined: either event lacks a total_iterations sample, the window is zero-length, or the count went BACKWARD (next < self — a counter reset; the delta is unmeasurable, not zero). The backward case is reachable only for the guard-skipped cross-step pairing or legacy/synthetic data, NOT for the live step-local StepStart[k] -> StepEnd[k] pair: teardown runs after StepEnd is emitted, so the handle set is stable within a step and the per-worker counters are monotone across the pair.

MEASURED ZERO is distinct from not-measured: a step whose workers made exactly zero forward progress over a positive hold (next == self) returns Some(0.0), not None. Zero throughput is a real, measured value — the strongest degradation signal — so it must surface, not vanish. With Some(0.0) a phase that collapsed to zero IS visible to the throughput-degradation detector (Timeline::build / Timeline::from_phase_buckets): when the prior phase had a positive rate (before > 0.0), the relative delta is -1.0 and the drop is flagged. (A phase that was already zero before is still not relatively comparable — the detector’s before > 0.0 gate avoids a div-by-zero — but an unchanged zero is not a degradation.)

This is the SINGLE iteration-rate formula, shared via its decomposition Self::rate_components by crate::assert::build_phase_buckets_with_stimulus (per-step windows attributed by step_index) and Timeline::build (per-phase windows attributed by index) — the two callers pair events differently but must compute the rate identically. The per-step metric producer inserts the rate_components pair (the iteration_rate Rate’s total_phase_iterations / total_phase_duration_sec components); rate_to (the quotient) is the display/comparison form used by Timeline::build and the result-helper ratios.

Source

pub fn rate_components(&self, next: &StimulusEvent) -> Option<(f64, f64)>

The (iteration_delta, window_seconds) components of Self::rate_to — same None conditions (missing total_iterations, backward count, or zero-length window). The per-phase metric pipeline inserts these as the total_phase_iterations / total_phase_duration_sec Counter components rather than the ready ratio, so the iteration_rate Rate re-pools across phases/runs as Σdelta / Σseconds, not a mean of per-phase ratios. The ms→s /1000 lives HERE (the seconds component) because derive_rate_metrics does a bare num/den with no scaling.

Source

pub fn phase(&self) -> Option<Phase>

The scenario Phase this event belongs to, or None for the terminal scenario-end boundary (which seeds no phase). Use THIS — not the raw Self::step_index field — to key per-phase lookups. step_index carries the bridge 1-indexed wire convention (Step k -> Some(k + 1)) while label renders the 0-indexed k, so reading the field directly invites the 0-vs-1 off-by-one this method removes: it maps the wire value onto the same Phase newtype the ScenarioStats / PhaseBucket accessors are keyed by (Phase::step(k)). Step events carry step_index >= 1, so the saturating_sub(1) is exact.

Trait Implementations§

Source§

impl Clone for StimulusEvent

Source§

fn clone(&self) -> StimulusEvent

Returns a duplicate of the value. Read more
1.0.0 · Source§

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

Performs copy-assignment from source. Read more
Source§

impl Debug for StimulusEvent

Source§

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

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

impl<'de> Deserialize<'de> for StimulusEvent

Source§

fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>
where __D: Deserializer<'de>,

Deserialize this value from the given Serde deserializer. Read more
Source§

impl Serialize for StimulusEvent

Source§

fn serialize<__S>(&self, __serializer: __S) -> Result<__S::Ok, __S::Error>
where __S: Serializer,

Serialize this value into the given Serde serializer. Read more

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
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
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
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> Pointable for T

§

const ALIGN: usize

The alignment of pointer.
§

type Init = T

The type for initializers.
§

unsafe fn init(init: <T as Pointable>::Init) -> usize

Initializes a with the given initializer. Read more
§

unsafe fn deref<'a>(ptr: usize) -> &'a T

Dereferences the given pointer. Read more
§

unsafe fn deref_mut<'a>(ptr: usize) -> &'a mut T

Mutably dereferences the given pointer. Read more
§

unsafe fn drop(ptr: usize)

Drops the object pointed to by the given pointer. Read more
§

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

§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns [Action::Follow] only if self and other return Action::Follow. Read more
§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns [Action::Follow] if either self or other returns Action::Follow. Read more
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<V, T> VZip<V> for T
where V: MultiLane<T>,

§

fn vzip(self) -> V

§

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

impl<T> DeserializeOwned for T
where T: for<'de> Deserialize<'de>,

§

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

§

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