pub struct ScenarioStats {
pub cgroups: Vec<CgroupStats>,
pub total_workers: usize,
pub total_cpus: usize,
pub total_migrations: u64,
pub worst_spread: f64,
pub worst_gap_ms: u64,
pub worst_gap_cpu: usize,
pub worst_migration_ratio: f64,
pub total_iterations: u64,
pub ext_metrics: BTreeMap<String, f64>,
pub phases: Vec<PhaseBucket>,
}Expand description
Aggregated statistics across all cgroups in a scenario.
Fields§
§cgroups: Vec<CgroupStats>Per-cgroup stats, one entry per cgroup.
total_workers: usizeSum of workers across all cgroups.
total_cpus: usizeSum of per-cgroup distinct CPU counts (not deduplicated across cgroups).
total_migrations: u64Sum of migration counts across all cgroups.
worst_spread: f64Worst spread across any cgroup (highest).
worst_gap_ms: u64Worst gap across any cgroup (highest, ms). Paired with
worst_gap_cpu — both come from the same cgroup.
worst_gap_cpu: usizeCPU where the worst gap occurred across all cgroups. Paired
with worst_gap_ms — both come from the same cgroup.
worst_migration_ratio: f64Worst migration ratio across any cgroup (highest).
total_iterations: u64Sum of iteration counts across all cgroups.
ext_metrics: BTreeMap<String, f64>Extensible metrics for the generic comparison pipeline. Populated from per-cgroup ext_metrics (worst value across cgroups).
phases: Vec<PhaseBucket>Per-phase metric buckets in step-index order. A scenario
with N Steps populates N + 1 entries: phase 0 is the
BASELINE settle window before Step 0 fires, phases
1..=N align with Step 0..Step N-1 in scenario order
(1-indexed Steps so the BASELINE encoding doesn’t collide
with first-Step’s index).
Empty when the scenario produced no periodic captures
(Default::default() yields vec![]). The existing
flat-bucket scalars on this struct are independent of the
per-phase view — they remain the “all phases merged”
reading, unchanged in semantics by the introduction of
phases.
Auto-populated by the framework: scenarios that fire
periodic captures (via
crate::test_support::KtstrTestEntry::num_snapshots or
crate::scenario::ops::Op::CaptureSnapshot) have this
field populated automatically inside
crate::test_support::eval’s evaluate_vm_result —
test code never needs to call
crate::assert::build_phase_buckets manually. The auto-
populate path drains the snapshot bridge from the
crate::vmm::VmResult returned by the framework and folds
the per-sample readings through
crate::stats::aggregate_samples_for_phase per metric.
Single-phase scenarios that fire no captures leave this
vec![]; the flat-bucket scalars on this struct cover the
single-phase case.
See PhaseBucket for the per-phase shape.
Implementations§
Source§impl ScenarioStats
impl ScenarioStats
Sourcepub fn phase(&self, phase: Phase) -> Option<&PhaseBucket>
pub fn phase(&self, phase: Phase) -> Option<&PhaseBucket>
Look up the phase bucket for a Phase — Phase::BASELINE for the
pre-first-Step settle window, Phase::step(k) for the test author’s
0-indexed Step k. The typed Phase keeps the 1-indexed encoding (Step 0
lives at the underlying step_index = 1) invisible at the call site.
Returns None when no bucket with that phase exists (single-phase
scenario, the scenario didn’t reach that Step, or a phase past the last).
Sourcepub fn phase_metric(
&self,
phase: Phase,
metric: impl Into<MetricId>,
) -> Option<f64>
pub fn phase_metric( &self, phase: Phase, metric: impl Into<MetricId>, ) -> Option<f64>
Shortcut: look up a single metric value in a specific
phase by phase-index. Returns None when:
(a) the phase is absent (no bucket with step_index in
Self::phases),
(b) the phase exists but had no finite samples for that
metric, OR
(c) metric is a dynamic crate::stats::MetricId key (a
scheduler-runtime / payload string) not present in this phase’s
stores. A built-in crate::stats::BuiltinMetric cannot be a
typo — that is a compile error — so this case needs a dynamic key.
Two stores are checked: PhaseBucket::metrics (via
PhaseBucket::get) and, failing that, the cross-cgroup phase
SUM of a per-cgroup Counter (PhaseBucket::cgroup_counter_total)
for "total_migrations" / "total_iterations" / "total_cpu_time_ns" —
registered Counters with no per-sample source, so they live ONLY in the
per-cgroup carriers; without this fallback the pooled lookup
returned a silent None while the per-cgroup
Self::phase_cgroup_metric surfaced the value. Symmetric with
crate::vmm::VmResult::phase_metric.
Sentinel-free: Some(0.0) means the reducer produced a
real zero from finite samples, NOT “missing data”. See
PhaseBucket::metrics for the registry source. When
debugging an unexpected None on a dynamic key, check
crate::stats::MetricId::def().is_some() to tell an unregistered
key from absent data (built-in ids always resolve).
Pass Phase::BASELINE for the settle window or Phase::step(k) for the
test author’s 0-indexed Step k — the typed Phase hides the 1-indexed
encoding (see Self::phase).
Sourcepub fn cgroup_balance_ratio(&self) -> Option<f64>
pub fn cgroup_balance_ratio(&self) -> Option<f64>
Cross-cgroup balance: the ratio of the busiest cell’s per-worker
throughput to the quietest’s — max / min over each cgroup’s
CgroupStats::iterations_per_worker. The bread-and-butter
scheduler-fairness assertion (every balance test hand-rolls this
max/min over self.cgroups today).
No-worker cgroups (iterations_per_worker() == None) are SKIPPED: a
0-worker cell is a config condition, not a balance signal. Returns
None when fewer than two cgroups have workers (a ratio needs two);
check the cgroup count separately if every declared cell must have
workers. A cell that ran workers but completed zero iterations
(measured Some(0.0)) drives the ratio to f64::INFINITY so
starvation SURFACES rather than vanishing — matching the
None-vs-Some(0.0) discipline of
CgroupStats::iterations_per_worker. For an explicit starvation
gate, check min > 0 over the same cgroups separately.
Whole-run aggregate: this reads self.cgroups, which sums over all
phases. For a single phase’s per-cgroup balance in a multi-phase
scenario, read each cgroup’s per-phase throughput (total_iterations
/ num_workers) from crate::vmm::VmResult::phase_cgroup — the
per-phase per-cgroup carriers now landed in
PhaseBucket::per_cgroup.
Sourcepub fn phase_cgroup_metric(
&self,
phase: Phase,
cgroup: &str,
metric: impl Into<MetricId>,
) -> Option<f64>
pub fn phase_cgroup_metric( &self, phase: Phase, cgroup: &str, metric: impl Into<MetricId>, ) -> Option<f64>
Per-cgroup analog of Self::phase_metric: look up metric for a named
cgroup in a Phase (Phase::BASELINE / Phase::step(k)), via that
cgroup’s per-phase carrier (PhaseCgroupStats::get), falling back to
PhaseCgroupStats::cgroup_counter for the per-cgroup Counters
total_migrations/total_iterations/total_cpu_time_ns. None when the
phase has no bucket, no carrier for cgroup, the carrier carried no finite
value for the metric, OR the metric is an unregistered dynamic key (a
built-in id is typo-proof; an unregistered crate::stats::MetricId has no
crate::stats::MetricId::def, same as Self::phase_metric). The
N-cgroups-to-N-queryable-sets surface on the AssertResult-holding path (the
in-VM post_vm path uses crate::vmm::VmResult::phase_cgroup_metric).
Sourcepub fn has_steps(&self) -> bool
pub fn has_steps(&self) -> bool
True iff the scenario produced at least one Step-phase
bucket (any phase with step_index >= 1). False when
phases is empty OR contains only BASELINE (the
pre-first-Step settle window).
Use this to fail a phase-aware assertion BEFORE calling
Self::phase with a Phase::step(k) on a scenario that
silently never advanced past BASELINE: a test that declared
no Steps, OR a scenario that bailed in setup before any
Step ran, would otherwise see Self::phase return
None for every Step and the test would either panic on
.expect(...) or pass vacuously.
anyhow::ensure!(
r.stats.has_steps(),
"scenario produced no Step-phase buckets — \
declare a Step or read Phase::BASELINE",
);
let throughput = r.stats.phase_metric(Phase::step(0), "throughput");Sourcepub fn run_metric(&self, metric: impl Into<MetricId>) -> Option<f64>
pub fn run_metric(&self, metric: impl Into<MetricId>) -> Option<f64>
Run-level value for a metric by registry name, for the
ext-sourced metric family that carries no typed
ScenarioStats field.
Resolves Self::ext_metrics — the run-level map the
framework fills post-merge with every metric whose value has no
typed struct field: the pooled wake-latency / run-delay
distributions and worst-cgroup iteration efficiencies
(the MetricKind::Distribution / MetricKind::WorstLowest
registry kinds — worst_p99_wake_latency_us, worst_run_delay_us,
worst_iterations_per_cpu_sec, …), the derived rates
(iteration_rate, and the pooled iterations_per_cpu_sec —
distinct from the worst_iterations_per_cpu_sec selector above),
the per-thread-group system_time_ns / user_time_ns, and
avg_imbalance_ratio / avg_dsq_depth. This is the
run-level analogue of Self::phase_metric for that family:
code holding the run’s AssertResult reads
r.stats.run_metric("worst_run_delay_us") instead of reaching
into the raw ext_metrics map by string key (ScenarioStats is
the AssertResult::stats field — the value a test body, or a
callback that builds an AssertResult via collect_all /
execute_scenario, holds). A post_vm callback instead receives
a VmResult, which has NO stats field and no run-level
Distribution surface — compare those cross-run via cargo ktstr perf-delta.
The ext family is populated only by the #[ktstr_test] eval
flow’s post-merge producer
(populate_run_distribution_metrics). An AssertResult built
by a DIRECT host assertion (assert_not_starved /
AssertPlan::assert_cgroup, which never run that producer)
carries the per-cgroup values on Self::cgroups but none of
these run-level roll-ups, so run_metric returns None for them
on that path — read the per-cgroup CgroupStats field directly
(e.g. r.stats.cgroups[i].p99_wake_latency_us) there.
Sentinel-free, matching Self::phase_metric: None means
the metric is absent from this run (no contributing cgroup or
carrier, or a name not present in the map); Some(0.0) is a
real measured zero. Check crate::stats::MetricId::def on a dynamic
key to tell an unregistered key from genuinely-absent data (built-in
ids always resolve). (The map also carries any
user-defined extensible-metric keys, plus the framework-internal
Rate-component Counters — total_phase_iterations /
total_phase_duration_sec / total_iterations_pooled /
total_cpu_time_sec, the numerator/denominator plumbing behind
iteration_rate / iterations_per_cpu_sec — all of which resolve
here too; prefer the derived rate over its raw components.)
RESOLVED here None-aware, ahead of the ext lookup, via a typed dispatch
(typed_sentinel_metric): the cross-cgroup metrics worst_spread,
worst_migration_ratio, worst_gap_ms, total_migrations,
total_iterations, worst_page_locality,
worst_cross_node_migration_ratio. The dispatch RE-DERIVES each from the
per-cgroup (self.cgroups) / per-phase (self.phases[].per_cgroup, the
NUMA metrics) carriers — None when no carrier measured it, Some(0.0)
for a measured zero — preserving the sentinel-free contract. The 5
non-NUMA metrics are 0.0-sentinel typed struct fields (a not-measured
carrier coerces to 0.0, indistinguishable from a measured zero, so the
re-derivation recovers the distinction); the two NUMA roll-ups
(worst_page_locality, worst_cross_node_migration_ratio) have no struct
field and re-pool purely from the per-phase NUMA carriers.
(worst_wake_latency_tail_ratio resolves via the ext lookup as the
WakeLatencyTailRatio key.)
NOT resolved here (not in ext_metrics, no typed dispatch):
- the monitor-sourced run-level metrics (
max_imbalance_ratio,max_dsq_depth,stuck_count,total_fallback,total_keep_last), whichScenarioStatsdoes not hold run-level — read those per-phase viaSelf::phase_metric.
So this resolves the ext-sourced family AND the typed cross-cgroup fields
(via the dispatch); only the monitor-sourced run-level metrics remain
unresolved here (ScenarioStats holds them only per-phase).
Trait Implementations§
Source§impl Clone for ScenarioStats
impl Clone for ScenarioStats
Source§fn clone(&self) -> ScenarioStats
fn clone(&self) -> ScenarioStats
1.0.0 · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read moreSource§impl Debug for ScenarioStats
impl Debug for ScenarioStats
Source§impl Default for ScenarioStats
impl Default for ScenarioStats
Source§fn default() -> ScenarioStats
fn default() -> ScenarioStats
Source§impl<'de> Deserialize<'de> for ScenarioStats
impl<'de> Deserialize<'de> for ScenarioStats
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>,
Source§impl PartialEq for ScenarioStats
impl PartialEq for ScenarioStats
Source§impl ScenarioStatsClaim for ScenarioStats
impl ScenarioStatsClaim for ScenarioStats
fn claim_cgroups<'a>( &'a self, verdict: &'a mut Verdict, ) -> SeqClaim<'a, CgroupStats>
fn claim_total_workers<'a>( &'a self, verdict: &'a mut Verdict, ) -> ClaimBuilder<'a, usize>
fn claim_total_cpus<'a>( &'a self, verdict: &'a mut Verdict, ) -> ClaimBuilder<'a, usize>
fn claim_total_migrations<'a>( &'a self, verdict: &'a mut Verdict, ) -> ClaimBuilder<'a, u64>
fn claim_worst_spread<'a>( &'a self, verdict: &'a mut Verdict, ) -> ClaimBuilder<'a, f64>
fn claim_worst_gap_ms<'a>( &'a self, verdict: &'a mut Verdict, ) -> ClaimBuilder<'a, u64>
fn claim_worst_gap_cpu<'a>( &'a self, verdict: &'a mut Verdict, ) -> ClaimBuilder<'a, usize>
fn claim_worst_migration_ratio<'a>( &'a self, verdict: &'a mut Verdict, ) -> ClaimBuilder<'a, f64>
fn claim_total_iterations<'a>( &'a self, verdict: &'a mut Verdict, ) -> ClaimBuilder<'a, u64>
fn claim_phases<'a>( &'a self, verdict: &'a mut Verdict, ) -> SeqClaim<'a, PhaseBucket>
Source§impl Serialize for ScenarioStats
impl Serialize for ScenarioStats
impl StructuralPartialEq for ScenarioStats
Auto Trait Implementations§
impl Freeze for ScenarioStats
impl RefUnwindSafe for ScenarioStats
impl Send for ScenarioStats
impl Sync for ScenarioStats
impl Unpin for ScenarioStats
impl UnwindSafe for ScenarioStats
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