pub struct SampleSeries { /* private fields */ }Expand description
Ordered collection of Samples drained from a
SnapshotBridge after a VM
run completes. Owns the underlying tuples so projection
closures can borrow into the reports / stats without copying.
Test authors construct a SampleSeries from
super::snapshot::SnapshotBridge::drain_ordered_with_stats
via Self::from_drained; non-periodic tags (e.g. Op::CaptureSnapshot
captures) coexist in the drain output and are tolerated by the
projection helpers — the typical pattern is to pre-filter to
periodic tags via Self::periodic_only before asserting.
Implementations§
Source§impl SampleSeries
impl SampleSeries
Sourcepub fn bpf<T, F>(&self, label: impl Into<String>, project: F) -> SeriesField<T>
pub fn bpf<T, F>(&self, label: impl Into<String>, project: F) -> SeriesField<T>
Project the series along the BPF axis. The closure receives
each sample’s Snapshot and returns a
SnapshotResult<T> — typically a typed value extracted
via snap.var(...).as_u64() or
snap.map(...).at(...).get(...).as_u64(). Errors flow
through into the resulting SeriesField as per-sample
Err slots so a temporal-assertion pattern can decide
whether to fail or skip on a missing field.
label is owned (impl Into<String>) and lands in
crate::assert::temporal::SeriesField::label for failure-
message rendering. Callers may pass a &'static str literal
or a runtime-built String (for auto-discovered struct or
JSON key names).
Sourcepub fn bpf_live_u64(&self, name: &str) -> SeriesField<u64>
pub fn bpf_live_u64(&self, name: &str) -> SeriesField<u64>
Project the live scheduler’s <obj>.<section> global
variable named name as u64. Per-row equivalent of
snap.live_var(name).as_u64(), but with the placeholder
short-circuit baked in.
Why this exists. Single-binary tests can use
series.bpf("label", |s| s.var(name).as_u64()) directly —
var() auto-disambiguates via the active-scheduler walker
when multiple maps share a global symbol (e.g. post-
Op::ReplaceScheduler with two scheduler instances). The
bpf_live_u64 helper saves the closure boilerplate AND
makes the live-resolution semantics visible in the call
site’s NAME, not buried in a projector body. The label on
the resulting SeriesField is the name argument
verbatim.
Sourcepub fn bpf_live_i64(&self, name: &str) -> SeriesField<i64>
pub fn bpf_live_i64(&self, name: &str) -> SeriesField<i64>
Sibling of Self::bpf_live_u64 projecting as i64.
Sourcepub fn bpf_live_f64(&self, name: &str) -> SeriesField<f64>
pub fn bpf_live_f64(&self, name: &str) -> SeriesField<f64>
Sibling of Self::bpf_live_u64 projecting as f64.
Sourcepub fn live_bpf_vars_via<const N: usize, P>(
&self,
names: [&str; N],
picker: P,
) -> [SeriesField<u64>; N]
pub fn live_bpf_vars_via<const N: usize, P>( &self, names: [&str; N], picker: P, ) -> [SeriesField<u64>; N]
Per-snapshot co-picked BPF projection of N counters from the
SAME global-section map. Lifts Snapshot::live_vars_via to
the series level: for each sample, calls the picker ONCE per
snapshot and projects the resulting N SnapshotFields as
u64 into N parallel SeriesFields.
Why this exists. The single-name Self::bpf closure shape
forces tests that need two co-picked counters (e.g.
nr_cross_dispatch + nr_same_dispatch from the same
scheduler bss copy after Op::ReplaceScheduler) to call the
picker TWICE per snapshot — once for each derived
SeriesField — paying picker cost 2N instead of N. The
per-snapshot dedup happens here: one live_vars_via call per
row, eagerly split into N u64 vectors before any
SeriesField materializes.
Lifetime / coverage gaps surface per field. If a snapshot
is a placeholder, every field’s slot for that row carries the
same crate::scenario::snapshot::SnapshotError::PlaceholderSample.
If live_vars_via fails (no candidate map has all N names,
or the picker returns None), every field’s slot carries the
same underlying crate::scenario::snapshot::SnapshotError —
the failure is shared, not split. Per-field .as_u64() casts
that fail (the picked field doesn’t render as a u64) surface
as per-field
crate::scenario::snapshot::SnapshotError::TypeMismatch
without contaminating sibling fields.
The label routed onto each resulting SeriesField is the
caller-supplied name from names at the matching position.
Sourcepub fn bpf_map<'a>(&'a self, map_name: &'a str) -> BpfMapProjector<'a>
pub fn bpf_map<'a>(&'a self, map_name: &'a str) -> BpfMapProjector<'a>
Auto-project a top-level BPF map’s struct members. The
returned BpfMapProjector auto-discovers struct member
names at sample 0 and exposes them via .field_u64(name) /
.field_i64(name) / .field_f64(name) — a caller that
wants every scalar field of a BSS struct without
enumerating each one by hand calls
series.bpf_map("scx_obj.bss").at(0) and then
.field_u64("nr_dispatched") for the field of interest.
Top-level scalar fields only. The auto-projector reads
directly-named struct members (e.g. "nr_dispatched",
"stall"). Nested struct members (e.g. "ctx.weight") and
deeper paths are NOT auto-discoverable through the typed
field_* helpers — for those, use the manual closure
projection SampleSeries::bpf with
|snap| snap.var("ctx").get("weight").as_u64() (or the
equivalent map-walking shape). Per-CPU maps are also out
of scope: they require an explicit .cpu(N) narrow on
the Snapshot accessor surface, so callers route
through the manual closure path for those as well.
Source§impl SampleSeries
impl SampleSeries
Sourcepub fn host(&self) -> Option<HostView<'_>>
pub fn host(&self) -> Option<HostView<'_>>
Borrowed view over the per-sample host-side per-CPU snapshot
data captured into each FailureDumpReport::per_cpu_time.
Returns None when the series is empty; otherwise yields a
HostView that exposes the per-CPU timeline (rows sorted
by elapsed-ms) and a closure-based projector compatible with
the temporal-assertion patterns in
crate::assert::temporal.
Orthogonal to Self::monitor: host() is the per-sample
per-CPU TIMELINE; monitor() is the per-VM-run cross-CPU
AGGREGATE. Tests that want both perspectives chain them
independently from the same series.
The returned HostView<'_> borrows from this series, so the
series must outlive any projection chained off the view.
Source§impl SampleSeries
impl SampleSeries
Sourcepub fn monitor(&self) -> Option<MonitorView<'_>>
pub fn monitor(&self) -> Option<MonitorView<'_>>
Borrowed view over the per-VM-run host monitor report
associated with this series. None when the monitor did
not run (host-only tests, early VM failure, or
Self::from_drained was called with None monitor).
Monitor is per-series — aggregates inside the returned
MonitorView refer to THAT series’ monitoring window
only; no cross-series merge is supported. A test that
constructs two SampleSeries from two VM runs gets two
independent monitors.
The returned MonitorView<'_> borrows from this series,
so the series must outlive any projection chained off the
view (e.g. series.monitor().map(|m| m.scx_events()?.total_pairs()) — the whole chain is bound
by series’s lifetime).
Source§impl SampleSeries
impl SampleSeries
Sourcepub fn stats<T, F>(
&self,
label: impl Into<String>,
project: F,
) -> SeriesField<T>
pub fn stats<T, F>( &self, label: impl Into<String>, project: F, ) -> SeriesField<T>
Project the series along the stats axis. The closure
receives each sample’s stats JSON (when present) and
returns a SnapshotResult<T>. Samples whose stats is
Err(reason) get a Err(MissingStats { tag, reason }) slot —
temporal assertions surface that as a per-sample
missing-stats failure rather than vacuously skipping it,
so a coverage gap is never silent and the operator sees
the why (no scheduler binary configured, relay timed
out, scheduler returned errno, etc.).
label is owned (impl Into<String>) and matches the
shape of Self::bpf — pass a literal or a runtime-built
String for auto-discovered keys.
Sourcepub fn stats_live_u64(&self, path: &str) -> SeriesField<u64>
pub fn stats_live_u64(&self, path: &str) -> SeriesField<u64>
Project the live scheduler’s stats JSON field at path as
u64. Per-row equivalent of series.stats(label, |s| s.get(path).as_u64()) with the boilerplate elided. Mirrors
Self::bpf_live_u64 for naming parity across axes.
Why “live” applies — per-request freshness, not a buffer.
Each periodic snapshot issues a FRESH scx_stats request
just before the freeze rendezvous fires; the response in
row.stats came from whichever scheduler was alive at
request-issue time. There is no relay buffer of “the last
stats we saw” — a stale-pre-swap response cannot land in
a post-swap sample. After Op::ReplaceScheduler the host
reconnects to the new scheduler’s scx_stats endpoint
before the next periodic boundary issues its request, so
post-swap samples carry the new scheduler’s data. The
_live suffix matches the BPF axis naming for cross-axis
vocabulary consistency AND describes the actual freshness
guarantee — same semantic across both axes.
Sourcepub fn stats_live_i64(&self, path: &str) -> SeriesField<i64>
pub fn stats_live_i64(&self, path: &str) -> SeriesField<i64>
Sibling of Self::stats_live_u64 projecting as i64.
Sourcepub fn stats_live_f64(&self, path: &str) -> SeriesField<f64>
pub fn stats_live_f64(&self, path: &str) -> SeriesField<f64>
Sibling of Self::stats_live_u64 projecting as f64.
Sourcepub fn stats_path<'a>(&'a self, path: &str) -> StatsPathProjector<'a>
pub fn stats_path<'a>(&'a self, path: &str) -> StatsPathProjector<'a>
Auto-project a stats-JSON sub-tree. The returned
StatsPathProjector resolves the tree at sample 0 and
exposes object keys via .key(name) (for nested layer /
cgroup objects) or .field(name) (for scalar leaves).
path may be empty — series.stats_path("") projects from
the root and is the canonical entry for system-level stats
fields like busy, antistall, system_cpu_util_ewma,
etc.
Source§impl SampleSeries
impl SampleSeries
Sourcepub fn from_drained(
drained: Vec<(String, FailureDumpReport, Option<Value>, Option<u64>)>,
monitor: Option<MonitorReport>,
) -> Self
pub fn from_drained( drained: Vec<(String, FailureDumpReport, Option<Value>, Option<u64>)>, monitor: Option<MonitorReport>, ) -> Self
Build a series from the bridge’s drained tuple. Every entry
is preserved in the order the bridge surfaced, including
non-periodic tags — callers that want the periodic-only
view chain .periodic_only().
monitor is the per-VM-run MonitorReport (typically
result.monitor.clone() from a VmResult). Pass None
when the monitor did not run (host-only tests, early VM
failure). Surfaced via Self::monitor for typed projection
of the summary + scx_events + per-sample timelines.
Sourcepub fn from_drained_typed(
drained: Vec<DrainedSnapshotEntry>,
monitor: Option<MonitorReport>,
) -> Self
pub fn from_drained_typed( drained: Vec<DrainedSnapshotEntry>, monitor: Option<MonitorReport>, ) -> Self
Production-path constructor: takes the typed
Result<serde_json::Value, MissingStatsReason>
shape returned by
SnapshotBridge::drain_ordered_with_stats,
preserving the specific failure mode (relay error, scheduler
errno, watchdog cancellation, etc.). Use this when the caller
has access to the bridge drain output; tests prefer
Self::from_drained which accepts the simpler Option
shape and collapses absent → NoSchedulerBinary.
Sourcepub fn empty() -> Self
pub fn empty() -> Self
Empty series. Useful for tests and for the no-periodic- capture case where every assertion vacuously passes.
Sourcepub fn periodic_only(self) -> Self
pub fn periodic_only(self) -> Self
Filter the series to entries whose tag begins with
"periodic_". Periodic captures are the only entries the
temporal-assertion patterns are designed for; on-demand
Op::CaptureSnapshot and watchpoint-fire captures share the
bridge’s tag namespace and would otherwise mix into the
timeline as off-cadence outliers. Consumes self because
the filter rebuilds the owning row vec — when a borrowed
view is needed instead, see Self::periodic_ref which
iterates the same rows without taking ownership.
Sourcepub fn periodic_ref(&self) -> impl Iterator<Item = Sample<'_>>
pub fn periodic_ref(&self) -> impl Iterator<Item = Sample<'_>>
Borrowed equivalent of Self::periodic_only: yields a
borrowed-view iterator over Samples whose tag starts
with "periodic_", without consuming the series. Use when
a single test asserts on both periodic-only and
all-captures views from the same series.
Sourcepub fn iter_samples(&self) -> impl Iterator<Item = Sample<'_>>
pub fn iter_samples(&self) -> impl Iterator<Item = Sample<'_>>
Sourcepub fn by_stamped_phase(&self) -> BTreeMap<u16, Vec<Sample<'_>>>
pub fn by_stamped_phase(&self) -> BTreeMap<u16, Vec<Sample<'_>>>
Group samples by the RAW bridge-stamped scenario phase. The
returned map is keyed by step_index (1-indexed phase encoding
— 0 is BASELINE, 1..=N align with scenario Step ordinals);
each entry is the ordered run of samples that fell in that
phase, preserving the iteration order produced by
Self::iter_samples.
Samples that lack a stamped step index (the unstamped
fixture path via
super::snapshot::SnapshotBridge::capture /
super::snapshot::SnapshotBridge::store /
super::snapshot::SnapshotBridge::store_with_stats) fall
under key 0 per the “no stamped index” fallback — the same
bucket BASELINE samples land in. The fixture / BASELINE
collision is acceptable because both flavours represent
pre-first-Step (or unstamped) state from the bucketer’s
perspective; production callers that need to distinguish
can inspect Sample::step_index directly.
CAVEAT — prefer Self::by_stimulus_phase when a stimulus
timeline is available: the bridge stamp is the step active at
(deferred) FIRE time, so under the dump-prerequisite gate a
burst of captures can all stamp the same late CURRENT_STEP
and collapse every sample into one phase. by_stimulus_phase
re-derives the phase from each sample’s timing-independent
boundary_offset_ms, which is immune to the burst.
The phase-aware aggregator consumes this map to compute
per-phase metric reductions (Counter last - first delta,
Gauge / Peak / Timestamp via crate::stats::aggregate_samples).
Sourcepub fn by_stimulus_phase(
&self,
stimulus_events: &[StimulusEvent],
) -> BTreeMap<u16, Vec<Sample<'_>>>
pub fn by_stimulus_phase( &self, stimulus_events: &[StimulusEvent], ) -> BTreeMap<u16, Vec<Sample<'_>>>
Group samples by the guest step whose stimulus window contains
each sample’s workload-relative boundary_offset_ms, rather
than the raw bridge-stamped step_index
(Self::by_stamped_phase). The returned map uses the same
1-indexed phase key (0 = BASELINE, 1..=N = Step ordinals)
and preserves Self::iter_samples order within each bucket.
This is the correct grouping whenever a stimulus timeline is
available: boundary_offset_ms is derived from the scheduled
boundary, NOT the (deferred) fire time, so it survives the
dump-prerequisite-gate burst that makes every periodic capture
stamp the same late CURRENT_STEP (the phases.len() == 1
collapse by_stamped_phase is subject to). Samples with no
boundary_offset_ms (on-demand / fixture captures) fall back
to their stamped step_index.
Unlike the folded scalar crate::assert::PhaseBuckets that
crate::assert::build_phase_buckets_with_stimulus returns,
this keeps the per-sample Sample views (full Snapshot / dsq
access) per phase. build_phase_buckets_with_stimulus itself
is built on this method.
Trait Implementations§
Source§impl Clone for SampleSeries
impl Clone for SampleSeries
Source§fn clone(&self) -> SampleSeries
fn clone(&self) -> SampleSeries
1.0.0 · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read moreAuto Trait Implementations§
impl Freeze for SampleSeries
impl RefUnwindSafe for SampleSeries
impl Send for SampleSeries
impl Sync for SampleSeries
impl Unpin for SampleSeries
impl UnwindSafe for SampleSeries
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