pub struct SeriesField<T> { /* private fields */ }Expand description
Per-sample column extracted from a
SampleSeries. Each
slot is a SnapshotResult<T> so a missing or
type-mismatched field does NOT abort the whole projection — it
surfaces at the temporal-assertion site as a per-sample error
the caller decides how to handle.
The label, tags, and per-sample timestamps are carried so
failure-path messages name the offending sample without the
caller re-threading the series. Tags and elapsed-ms vectors
are always the same length as values.
Implementations§
Source§impl<T> SeriesField<T>
impl<T> SeriesField<T>
Sourcepub fn from_parts(
label: impl Into<String>,
tags: Vec<String>,
elapsed_ms: Vec<u64>,
values: Vec<SnapshotResult<T>>,
) -> Self
pub fn from_parts( label: impl Into<String>, tags: Vec<String>, elapsed_ms: Vec<u64>, values: Vec<SnapshotResult<T>>, ) -> Self
Build a new field. Internal — projection helpers in
crate::scenario::sample call this on the series side.
4-arg backward-compat constructor: defaults phases to all
None. New consumers that have phase context per sample
should call Self::from_parts_with_phases instead.
Sourcepub fn from_parts_with_phases(
label: impl Into<String>,
tags: Vec<String>,
elapsed_ms: Vec<u64>,
values: Vec<SnapshotResult<T>>,
phases: Vec<Option<Phase>>,
) -> Self
pub fn from_parts_with_phases( label: impl Into<String>, tags: Vec<String>, elapsed_ms: Vec<u64>, values: Vec<SnapshotResult<T>>, phases: Vec<Option<Phase>>, ) -> Self
Build a new field with explicit per-sample phase tags.
phases.len() MUST equal values.len() — the four parallel
vecs (tags / elapsed_ms / values / phases) are addressed by
the same index throughout. Like Self::from_parts, this
is intended for projection helpers in
crate::scenario::sample that already know each sample’s
step_index from the drained bridge tuple.
5-arg convenience taking MEASURED Vec<u64> timestamps; wraps
each in Some and delegates to Self::from_parts_with_phases_opt.
Test fixtures (which always model measured samples) and any
all-measured caller use this. The production projection funnel
— which threads Option<u64> from the bridge so “not measured”
stays distinct from “measured 0” — calls
Self::from_parts_with_phases_opt directly.
Sourcepub fn from_parts_with_phases_opt(
label: impl Into<String>,
tags: Vec<String>,
elapsed_ms: Vec<Option<u64>>,
values: Vec<SnapshotResult<T>>,
phases: Vec<Option<Phase>>,
) -> Self
pub fn from_parts_with_phases_opt( label: impl Into<String>, tags: Vec<String>, elapsed_ms: Vec<Option<u64>>, values: Vec<SnapshotResult<T>>, phases: Vec<Option<Phase>>, ) -> Self
None-aware constructor: elapsed_ms[i] == None means the bridge
recorded no timestamp for that sample (not measured), kept
distinct from a measured Some(0). Temporal patterns that do
timestamp math (EachClaim-free rate_within dt,
steady_within warmup gate, converges_to deadline gate) SKIP
a None-anchored sample rather than fabricating a 0 (the
silent-wrong-answer this distinction prevents).
Sourcepub fn phases_iter(&self) -> impl Iterator<Item = Option<Phase>> + '_
pub fn phases_iter(&self) -> impl Iterator<Item = Option<Phase>> + '_
Per-sample phase tag, parallel to values. None for
fixture / unstamped samples; Some(phase) for production
captures whose source row carried a step_index.
Sourcepub fn aggregate_by_phase(&self, metric: &MetricDef) -> BTreeMap<Phase, f64>
pub fn aggregate_by_phase(&self, metric: &MetricDef) -> BTreeMap<Phase, f64>
Per-phase folded reduction: extract the f64 value from
each Ok-sample (skipping per-sample errors) and route the
per-phase slice through crate::stats::aggregate_samples_for_phase
so Counter kinds get the cumulative-delta semantic while
other kinds inherit the flat-run aggregator. Returns one
entry per phase that has at least one Ok-sample with a
finite value; phases with all-err / all-NaN samples are
absent from the map (consistent with
aggregate_samples_for_phase returning None on that
input). Skips None-phase samples — fixture / unstamped
data does not have a phase key to bucket against.
The API entry point a test author uses to ask “what was the
per-phase reduction of metric X?” without having to thread
by_phase + the kind-aware reducer manually.
Sourcepub fn sum_by_phase(&self) -> BTreeMap<Phase, f64>
pub fn sum_by_phase(&self) -> BTreeMap<Phase, f64>
Per-phase SUM of the Ok-sample values — the per-phase total for a
metric whose samples are already per-read DELTAS, e.g. a
scheduler-defined scx_stats metric the scheduler deltas
server-side per reader request (one ktstr snapshot = one request
= one delta), read via series.stats(name). Returns one entry
per phase with at least one finite Ok-sample; all-err / all-NaN
phases and None-phase (fixture / unstamped) samples are
excluded, matching Self::aggregate_by_phase.
This is the crate::stats::MetricKind::DeltaSum per-phase
reduction WITHOUT requiring a crate::stats::MetricDef — the
ergonomic accessor for the common case “I read a delta-reported
scx_stats metric; give me each phase’s total.” For a registered
metric, aggregate_by_phase(&def) with def.kind == DeltaSum
gives the identical result.
Boundary: the first in-phase delta straddles the phase boundary (it spans from the last pre-phase read to the first in-phase read, so it carries a little pre-phase activity); it is attributed to the phase its read lands in — a slight left-edge over-attribution, the deliberate semantic since a per-read delta cannot be split.
Sourcepub fn for_each_phase(&self, f: impl FnMut(Phase, &[SampleTriple<'_, T>]))
pub fn for_each_phase(&self, f: impl FnMut(Phase, &[SampleTriple<'_, T>]))
Apply f once per phase, with the per-phase slice of
SampleTriple<T>s. None-phase samples (fixture /
unstamped) are skipped — callers wanting them call
Self::by_phase directly and handle the second-tuple
none_bucket themselves. Phases iterate in Phase order
(BASELINE first, then Step[0], Step[1], …) per the
BTreeMap key ordering, which lets temporal-pattern
consumers apply a per-phase reduction without restating
the by_phase unpacking at every call site.
field.for_each_phase(|phase, samples| {
// apply pattern X to samples, scoped to `phase`
});Sourcepub fn by_phase(&self) -> ByPhasePartition<'_, T>
pub fn by_phase(&self) -> ByPhasePartition<'_, T>
Partition samples by phase. None-phase samples bucket
into the returned none_bucket outside the BTreeMap; phase
values bucket by their Phase key. Each bucket retains the
per-sample SampleTriple<T> the standard Self::iter_full
yields.
Sourcepub fn values_iter(&self) -> impl Iterator<Item = &SnapshotResult<T>>
pub fn values_iter(&self) -> impl Iterator<Item = &SnapshotResult<T>>
Iterate over per-sample values (each a SnapshotResult<T>).
Sourcepub fn iter_full(
&self,
) -> impl Iterator<Item = (&str, Option<u64>, &SnapshotResult<T>)>
pub fn iter_full( &self, ) -> impl Iterator<Item = (&str, Option<u64>, &SnapshotResult<T>)>
Iterate over the full per-sample triple — (tag, elapsed_ms, &SnapshotResult<T>). Lets a caller consume the
projected column alongside its sample identity without
re-threading the source SampleSeries.
Yields entries in the same order as the underlying
Vec<SnapshotResult<T>> storage; tags and elapsed-ms
vectors are guaranteed equal-length to values by
Self::from_parts_with_phases_opt’s assert_eq! checks
(which run in both debug and release builds).
Sourcepub fn each<'v>(&self, verdict: &'v mut Verdict) -> EachClaim<'_, 'v, T>
pub fn each<'v>(&self, verdict: &'v mut Verdict) -> EachClaim<'_, 'v, T>
Open a per-sample claim builder for scalar comparators
(at_least, at_most, between). Each successful sample
runs the comparator independently; the first failure
records a detail; subsequent failures pile on so the
timeline shows every offending sample, not just the first.
Borrows the verdict mutably for the duration of the
comparator chain.
Sourcepub fn phase(&self, phase: Phase) -> Vec<SampleTriple<'_, T>> ⓘ
pub fn phase(&self, phase: Phase) -> Vec<SampleTriple<'_, T>> ⓘ
Iterate the SampleTriples for one specific phase. Sugar
for Self::by_phase().0.get(&phase).map(...) that drops
the tuple-destructure noise the user otherwise repeats at
every per-phase site. Returns an empty iterator when the
phase had no samples; callers that need to distinguish
“empty bucket” from “phase never observed” can use
Self::by_phase directly.
Sourcepub fn value_at_phase(&self, phase: Phase) -> Option<T>where
T: Copy,
pub fn value_at_phase(&self, phase: Phase) -> Option<T>where
T: Copy,
Single-value reduction for a cumulative counter or any
metric where “the last sample of the phase” is the
load-bearing value: returns the last Ok-sample’s value
for phase, or None when the phase had zero Ok-samples.
Per-sample errors are skipped so a one-off projection
hiccup doesn’t drop the whole phase. The “value at end of
phase” semantic is what cross-phase counter comparisons
(e.g. “dispatch count of Step[1] ≤ 0.85 × dispatch count
of Step[0]”) almost always want, so this primitive
removes the closure-over-by_phase-triples boilerplate the
user otherwise writes at every site.
Sourcepub fn last_per_phase(&self) -> BTreeMap<Phase, T>where
T: Copy,
pub fn last_per_phase(&self) -> BTreeMap<Phase, T>where
T: Copy,
Reduce to a per-phase last-Ok-value map. The companion to
Self::value_at_phase for callers that want every phase
at once: each present phase maps to its last successful
sample’s value. Phases with all-Err samples are absent from
the map. Matches the “cumulative-counter-at-end-of-phase”
semantic Self::aggregate_by_phase applies for Counter
metrics, but skips the kind-aware fold so callers reaching
for the raw last value don’t pay the projection cost.
Sourcepub fn first_per_phase(&self) -> BTreeMap<Phase, T>where
T: Copy,
pub fn first_per_phase(&self) -> BTreeMap<Phase, T>where
T: Copy,
Reduce to a per-phase first-Ok-value map. The symmetric
companion to Self::last_per_phase: each present phase
maps to its first successful sample’s value. Per-sample
errors are skipped so a one-off projection hiccup at the
start of a phase doesn’t drop the whole phase. Phases with
all-Err samples are absent from the map.
The load-bearing pairing is
last_per_phase - first_per_phase — the work done WITHIN
a phase, with no contribution from prior phases — surfaced
directly via Self::counter_delta_per_phase.
Sourcepub fn counter_delta_per_phase(&self) -> BTreeMap<Phase, T>
pub fn counter_delta_per_phase(&self) -> BTreeMap<Phase, T>
Per-phase cumulative-counter delta: last_per_phase(p) - first_per_phase(p) for every phase with at least one Ok
sample. The reducer A/B-compare tests reach for when a
metric is a cumulative counter still accruing from prior
phases — value_at_phase’s last-sample reading carries
the whole-run accumulation, which lets prior-phase activity
muddy the cross-phase ratio. The delta isolates the work
performed WITHIN each phase so
ratio(phase_delta(later) / phase_delta(earlier)) answers
the question the operator actually asked: “did later’s
activity drop relative to earlier’s?”
Single-Ok-sample phases yield a delta of zero (first ==
last). Phases with all-Err samples are absent. Phases that
appear in first_per_phase but not last_per_phase (the
out-of-order edge case) are also absent — the delta is
well-defined only when both endpoints are present.
The reducer is intentionally generic: the test author owns
(same_delta, cross_delta)-style compositions (e.g. fold
two counter-delta maps into a per-phase fraction) without
the framework needing a registered MetricDef. For
Counter-typed registered metrics, the equivalent
MetricDef-aware path is Self::aggregate_by_phase.
Counter regression: when a phase’s last sample reads
LOWER than its first, the reducer emits a tracing::warn!
naming the field label + phase + (first, last) and stores
T::default() for that phase (e.g. 0 for u64). The
underlying counter is assumed non-decreasing within a phase
(the common case for BPF per-event counters). A regression
can mean either an upstream-signal bug (the counter source
itself rolled back) OR a framework picker drift mid-phase
(e.g. crate::scenario::sample::SampleSeries::bpf_live_u64
resolved to different bss copies across same-phase
snapshots in a post-Op::ReplaceScheduler swap window).
Either way the regression is reported as zero progress
rather than panicking, so a single bad sample does not
crash the assertion engine.
Sourcepub fn ratio_across_phases<'v>(
&self,
verdict: &'v mut Verdict,
earlier: Phase,
later: Phase,
) -> CrossPhaseRatio<'v, T>
pub fn ratio_across_phases<'v>( &self, verdict: &'v mut Verdict, earlier: Phase, later: Phase, ) -> CrossPhaseRatio<'v, T>
Cross-phase comparator: pin value_at_phase(later) / value_at_phase(earlier) against a ceiling AND record the
computed ratio + both phase values in the verdict so
--nocapture runs surface the actual numbers without a
per-test println! boilerplate. Records a failure when
either phase had no Ok-samples, when the earlier value is
zero (no baseline), or when the ratio exceeds ceiling.
On success records an informational note carrying the
observed ratio + both values so the operator can see the
margin against the threshold.
Returns &mut Verdict for chaining.
Source§impl<T> SeriesField<T>
impl<T> SeriesField<T>
Sourcepub fn nondecreasing<'v>(&self, verdict: &'v mut Verdict) -> &'v mut Verdict
pub fn nondecreasing<'v>(&self, verdict: &'v mut Verdict) -> &'v mut Verdict
Pass when every consecutive pair satisfies
values[i] <= values[i+1]. A common shape for kernel
counters whose only legal direction is up. Per-sample
projection errors are SKIPPED — the affected pair-comparison
is dropped, the skip count is logged as a verdict Note so
coverage gaps stay visible, and the verdict is NOT flipped
on a missing-sample condition (which is structurally
missing data, not a regression). Adjacent samples on
either side of a gap are still checked against each other.
Sourcepub fn strictly_increasing<'v>(
&self,
verdict: &'v mut Verdict,
) -> &'v mut Verdict
pub fn strictly_increasing<'v>( &self, verdict: &'v mut Verdict, ) -> &'v mut Verdict
Pass when every consecutive pair satisfies
values[i] < values[i+1]. Useful for counters that MUST
advance every period (e.g. a heartbeat tick). Same skip-on-
projection-error semantics as Self::nondecreasing.
Source§impl SeriesField<f64>
impl SeriesField<f64>
Sourcepub fn rate_within<'v>(
&self,
verdict: &'v mut Verdict,
lo: f64,
hi: f64,
) -> &'v mut Verdict
pub fn rate_within<'v>( &self, verdict: &'v mut Verdict, lo: f64, hi: f64, ) -> &'v mut Verdict
Pass when every consecutive (delta_value / delta_ms) lies
in [lo, hi]. The rate is computed with millisecond
resolution from the per-sample elapsed-ms timestamps, so
a counter that should advance at ~1 unit/ms reads cleanly
as rate_within(0.5, 2.0). A zero-time delta between
adjacent samples lands as a caller-side or framework
failure (samples too close to compute a rate); the detail
names the offending pair.
Sourcepub fn steady_within<'v>(
&self,
verdict: &'v mut Verdict,
warmup_ms: u64,
tolerance: f64,
) -> &'v mut Verdict
pub fn steady_within<'v>( &self, verdict: &'v mut Verdict, warmup_ms: u64, tolerance: f64, ) -> &'v mut Verdict
Pass when every post-warmup sample (elapsed_ms >= warmup_ms) lies inside mean·(1-tolerance), mean·(1+tolerance).
tolerance is a fraction (0.10 = ±10%). The mean is
computed over the post-warmup samples only — the warmup
region is excluded so ramp-up does not bias the steady-
state baseline. Per-sample projection errors are SKIPPED
(with a verdict Note logging the count and tags); they are
treated as gaps in coverage, not band violations, so a
missing post-warmup sample does not flip the verdict.
Sourcepub fn converges_to<'v>(
&self,
verdict: &'v mut Verdict,
target: f64,
tolerance: f64,
deadline_ms: u64,
) -> &'v mut Verdict
pub fn converges_to<'v>( &self, verdict: &'v mut Verdict, target: f64, tolerance: f64, deadline_ms: u64, ) -> &'v mut Verdict
Pass when three consecutive samples land inside
[target-tolerance, target+tolerance] AT OR BEFORE
deadline_ms. The intent is “the system stabilizes near
target by the deadline” — three consecutive in-band
samples are the convergence-witness shape. Failures fire
when the deadline passes without a witness.
Sourcepub fn ratio_within<'v>(
&self,
verdict: &'v mut Verdict,
other: &SeriesField<f64>,
lo: f64,
hi: f64,
) -> &'v mut Verdict
pub fn ratio_within<'v>( &self, verdict: &'v mut Verdict, other: &SeriesField<f64>, lo: f64, hi: f64, ) -> &'v mut Verdict
Pass when every consecutive (self_value / other_value)
lies in [lo, hi]. Cross-field correlation: e.g. ensure a
per-cgroup utilization always tracks a per-cgroup runtime
within a fixed band. The two series MUST have matching
length and tags; mismatches fire a single caller-error
detail. Per-sample projection errors on EITHER lhs or rhs
are SKIPPED — the affected pair is dropped, the skip count
is logged as a verdict Note, and the verdict is NOT flipped
on missing-data conditions.
Source§impl SeriesField<bool>
impl SeriesField<bool>
Sourcepub fn always_true<'v>(&self, verdict: &'v mut Verdict) -> &'v mut Verdict
pub fn always_true<'v>(&self, verdict: &'v mut Verdict) -> &'v mut Verdict
Pass when every sample’s value is true. Per-sample
projection errors fail the assertion. Use for boolean
invariants — e.g. “scheduler is alive at every periodic
boundary” projected as snap.var("scheduler_alive").as_bool().
Trait Implementations§
Source§impl<T: Clone> Clone for SeriesField<T>
impl<T: Clone> Clone for SeriesField<T>
Source§fn clone(&self) -> SeriesField<T>
fn clone(&self) -> SeriesField<T>
1.0.0 · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read moreAuto Trait Implementations§
impl<T> Freeze for SeriesField<T>
impl<T> RefUnwindSafe for SeriesField<T>where
T: RefUnwindSafe,
impl<T> Send for SeriesField<T>where
T: Send,
impl<T> Sync for SeriesField<T>where
T: Sync,
impl<T> Unpin for SeriesField<T>where
T: Unpin,
impl<T> UnwindSafe for SeriesField<T>where
T: UnwindSafe,
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