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: u64Milliseconds since scenario start (guest monotonic clock).
label: StringHuman-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: boolTrue 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: boolTrue 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
impl StimulusEvent
Sourcepub fn from_wire(ev: &StimulusEvent) -> Self
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).
Sourcepub fn from_step_end(ev: &StimulusEvent) -> Self
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).
Sourcepub fn terminal(elapsed_ms: u64, total_iterations: u64) -> Self
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.
Sourcepub fn rate_to(&self, next: &StimulusEvent) -> Option<f64>
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.
Sourcepub fn rate_components(&self, next: &StimulusEvent) -> Option<(f64, f64)>
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.
Sourcepub fn phase(&self) -> Option<Phase>
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
impl Clone for StimulusEvent
Source§fn clone(&self) -> StimulusEvent
fn clone(&self) -> StimulusEvent
1.0.0 · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read moreSource§impl Debug for StimulusEvent
impl Debug for StimulusEvent
Source§impl<'de> Deserialize<'de> for StimulusEvent
impl<'de> Deserialize<'de> for StimulusEvent
Source§fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>where
__D: Deserializer<'de>,
fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>where
__D: Deserializer<'de>,
Auto Trait Implementations§
impl Freeze for StimulusEvent
impl RefUnwindSafe for StimulusEvent
impl Send for StimulusEvent
impl Sync for StimulusEvent
impl Unpin for StimulusEvent
impl UnwindSafe for StimulusEvent
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>
§fn in_current_span(self) -> Instrumented<Self>
fn in_current_span(self) -> 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