use super::*;
#[test]
fn phase_bucket_json_round_trips_all_fields() {
let mut metrics = BTreeMap::new();
metrics.insert("worst_spread".to_string(), 0.42);
metrics.insert("dsq_depth_max".to_string(), 12.0);
let bucket = PhaseBucket {
per_cgroup: Default::default(),
step_index: 7,
label: "Step[6]".to_string(),
start_ms: 1500,
end_ms: u64::MAX,
sample_count: 42,
metrics,
};
let json = serde_json::to_string(&bucket).expect("serialize");
let back: PhaseBucket = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back, bucket);
}
#[test]
fn phase_bucket_per_cgroup_round_trips_and_defaults_empty() {
use super::PhaseCgroupStats;
use std::collections::BTreeSet;
assert!(
PhaseBucket::default().per_cgroup.is_empty(),
"the structural carrier defaults to an empty per_cgroup map",
);
let mut bucket = PhaseBucket {
per_cgroup: Default::default(),
step_index: 1,
label: "Step[0]".to_string(),
start_ms: 0,
end_ms: 1000,
sample_count: 3,
metrics: BTreeMap::new(),
};
bucket.per_cgroup.insert(
"cg_0".to_string(),
PhaseCgroupStats {
num_workers: 3,
cpus_used: BTreeSet::from([2, 5, 6]),
wake_latencies_ns: vec![10, 20, 30],
wake_sample_total: 3,
timer_latencies_ns: vec![],
timer_sample_total: 0,
run_delays_ns: vec![1_500, 2_500],
off_cpu_pcts: vec![1.5, 11.0, 22.5],
total_migrations: 7,
total_iterations: 4200,
total_cpu_time_ns: 9_000_000,
numa_pages_local: 90,
numa_pages_total: 100,
cross_node_migrated: 4,
max_gap_ms: 13,
max_gap_cpu: 2,
stripped: false,
metrics: std::collections::BTreeMap::new(),
schbench: None,
taobench: None,
},
);
let json = serde_json::to_string(&bucket).expect("serialize");
let back: PhaseBucket = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back, bucket);
assert_eq!(back.per_cgroup["cg_0"].total_iterations, 4200);
assert_eq!(back.per_cgroup["cg_0"].wake_latencies_ns, vec![10, 20, 30]);
assert_eq!(back.per_cgroup["cg_0"].off_cpu_pcts, vec![1.5, 11.0, 22.5]);
assert_eq!(back.per_cgroup["cg_0"].cpus_used, BTreeSet::from([2, 5, 6]),);
}
#[test]
fn phase_bucket_empty_metrics_round_trips_as_empty_object() {
let bucket = PhaseBucket {
per_cgroup: Default::default(),
step_index: 0,
label: "BASELINE".to_string(),
start_ms: 0,
end_ms: 100,
sample_count: 0,
metrics: BTreeMap::new(),
};
let json = serde_json::to_string(&bucket).expect("serialize");
assert!(
json.contains(r#""metrics":{}"#),
"empty metrics must serialize as present `metrics: {{}}`, got: {json}"
);
let back: PhaseBucket = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back, bucket);
}
#[test]
fn phase_bucket_step_index_u16_max_round_trips() {
let bucket = PhaseBucket {
per_cgroup: Default::default(),
step_index: u16::MAX,
label: "Step[65534]".to_string(),
start_ms: 0,
end_ms: 1,
sample_count: 0,
metrics: BTreeMap::new(),
};
let json = serde_json::to_string(&bucket).expect("serialize");
let back: PhaseBucket = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back.step_index, u16::MAX);
assert_eq!(back, bucket);
}
#[test]
fn phase_bucket_empty_label_round_trips_as_present_field() {
let bucket = PhaseBucket {
per_cgroup: Default::default(),
step_index: 0,
label: String::new(),
start_ms: 0,
end_ms: 0,
sample_count: 0,
metrics: BTreeMap::new(),
};
let json = serde_json::to_string(&bucket).expect("serialize");
assert!(
json.contains(r#""label":"""#),
"empty label must serialize as present `label: \"\"`, got: {json}"
);
let back: PhaseBucket = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back.label, "");
assert_eq!(back, bucket);
}
#[test]
fn phase_bucket_get_distinguishes_absent_from_zero() {
let mut metrics = BTreeMap::new();
metrics.insert("present".to_string(), 0.0);
let bucket = PhaseBucket {
per_cgroup: Default::default(),
step_index: 1,
label: "Step[0]".to_string(),
start_ms: 0,
end_ms: 1000,
sample_count: 10,
metrics,
};
assert_eq!(bucket.get("present"), Some(0.0));
assert_eq!(bucket.get("absent"), None);
}
#[test]
fn scenario_stats_default_has_empty_phases() {
let stats = ScenarioStats::default();
assert!(stats.phases.is_empty());
assert_eq!(stats.phase(crate::assert::Phase::BASELINE), None);
assert_eq!(
stats.phase_metric(crate::assert::Phase::BASELINE, "any"),
None
);
}
#[test]
fn scenario_stats_phase_lookup_by_step_index_not_position() {
let mut metrics_baseline = BTreeMap::new();
metrics_baseline.insert("worst_spread".to_string(), 0.10);
let mut metrics_step2 = BTreeMap::new();
metrics_step2.insert("worst_spread".to_string(), 0.42);
let stats = ScenarioStats {
phases: vec![
PhaseBucket {
per_cgroup: Default::default(),
step_index: 0,
label: "BASELINE".to_string(),
start_ms: 0,
end_ms: 100,
sample_count: 2,
metrics: metrics_baseline,
},
PhaseBucket {
per_cgroup: Default::default(),
step_index: 3,
label: "Step[2]".to_string(),
start_ms: 200,
end_ms: 300,
sample_count: 5,
metrics: metrics_step2,
},
],
..Default::default()
};
assert_eq!(
stats
.phase(crate::assert::Phase::BASELINE)
.map(|p| p.step_index),
Some(0)
);
assert_eq!(
stats
.phase(crate::assert::Phase::step(2))
.map(|p| p.step_index),
Some(3)
);
assert_eq!(stats.phase(crate::assert::Phase::step(0)), None);
assert_eq!(stats.phase(crate::assert::Phase::step(1)), None);
}
#[test]
fn scenario_stats_phase_metric_resolves_typed_lookup() {
let mut metrics = BTreeMap::new();
metrics.insert("worst_spread".to_string(), 0.42);
metrics.insert("dsq_depth_max".to_string(), 12.0);
let stats = ScenarioStats {
phases: vec![PhaseBucket {
per_cgroup: Default::default(),
step_index: 1,
label: "Step[0]".to_string(),
start_ms: 100,
end_ms: 200,
sample_count: 3,
metrics,
}],
..Default::default()
};
assert_eq!(
stats.phase_metric(crate::assert::Phase::step(0), "worst_spread"),
Some(0.42)
);
assert_eq!(
stats.phase_metric(
crate::assert::Phase::step(0),
crate::stats::BuiltinMetric::WorstSpread
),
Some(0.42),
);
assert_eq!(
stats.phase_metric(crate::assert::Phase::step(0), "dsq_depth_max"),
Some(12.0)
);
assert_eq!(
stats.phase_metric(crate::assert::Phase::step(0), "absent"),
None
);
assert_eq!(
stats.phase_metric(crate::assert::Phase::step(98), "worst_spread"),
None
);
}
#[test]
fn scenario_stats_phase_step_resolves_to_step_bucket() {
let stats = ScenarioStats {
phases: vec![
PhaseBucket {
step_index: 0, label: "BASELINE".to_string(),
..Default::default()
},
PhaseBucket {
step_index: 1, label: "Step[0]".to_string(),
..Default::default()
},
PhaseBucket {
step_index: 2, label: "Step[1]".to_string(),
..Default::default()
},
],
..Default::default()
};
assert_eq!(
stats
.phase(crate::assert::Phase::step(0))
.map(|p| p.label.as_str()),
Some("Step[0]")
);
assert_eq!(
stats
.phase(crate::assert::Phase::step(1))
.map(|p| p.label.as_str()),
Some("Step[1]")
);
assert_eq!(stats.phase(crate::assert::Phase::step(99)), None);
assert_eq!(stats.phase(crate::assert::Phase::step(u16::MAX)), None);
}
#[test]
fn scenario_stats_run_metric_resolves_ext_family_sentinel_free() {
let mut ext = BTreeMap::new();
ext.insert("worst_run_delay_us".to_string(), 48.0);
ext.insert("worst_iterations_per_cpu_sec".to_string(), 12345.0);
ext.insert("worst_wake_latency_cv".to_string(), 0.0);
ext.insert("my_custom_metric".to_string(), 7.0);
let stats = ScenarioStats {
ext_metrics: ext,
worst_spread: 0.99,
..Default::default()
};
assert_eq!(stats.run_metric("worst_run_delay_us"), Some(48.0));
assert_eq!(
stats.run_metric("worst_iterations_per_cpu_sec"),
Some(12345.0)
);
assert_eq!(stats.run_metric("worst_wake_latency_cv"), Some(0.0));
assert_eq!(stats.run_metric("my_custom_metric"), Some(7.0));
assert_eq!(stats.run_metric("worst_p99_wake_latency_us"), None);
assert_eq!(stats.run_metric("totally_made_up"), None);
assert_eq!(stats.run_metric("worst_spread"), None);
assert_eq!(stats.worst_spread, 0.99);
assert_eq!(stats.run_metric("max_imbalance_ratio"), None);
}
#[test]
fn run_metric_typed_cgroup_fields_distinguish_measured_zero_from_absent() {
use crate::stats::BuiltinMetric as B;
let cg =
|spread: Option<f64>, iters: u64, migr: u64, gap_ms: u64, workers: usize| CgroupStats {
num_workers: workers,
spread,
total_iterations: iters,
total_migrations: migr,
migration_ratio: if iters > 0 {
migr as f64 / iters as f64
} else {
0.0
},
max_gap_ms: gap_ms,
..Default::default()
};
let m = ScenarioStats {
cgroups: vec![cg(Some(0.0), 1000, 0, 0, 4)],
..Default::default()
};
assert_eq!(m.run_metric(B::WorstSpread), Some(0.0));
assert_eq!(m.run_metric(B::WorstMigrationRatio), Some(0.0));
assert_eq!(m.run_metric(B::TotalMigrations), Some(0.0));
assert_eq!(m.run_metric(B::TotalIterations), Some(1000.0));
assert_eq!(m.run_metric(B::WorstGapMs), Some(0.0));
let a = ScenarioStats {
cgroups: vec![cg(None, 0, 0, 0, 0)],
..Default::default()
};
assert_eq!(a.run_metric(B::WorstSpread), None);
assert_eq!(a.run_metric(B::WorstMigrationRatio), None);
assert_eq!(a.run_metric(B::WorstGapMs), None);
assert_eq!(a.run_metric(B::TotalMigrations), Some(0.0));
assert_eq!(a.run_metric(B::TotalIterations), Some(0.0));
let e = ScenarioStats::default();
assert_eq!(e.run_metric(B::TotalMigrations), None);
assert_eq!(e.run_metric(B::TotalIterations), None);
let two = ScenarioStats {
cgroups: vec![cg(Some(0.1), 100, 5, 3, 2), cg(Some(0.4), 100, 1, 9, 2)],
..Default::default()
};
assert_eq!(
two.run_metric(B::WorstSpread),
Some(0.4),
"max spread across cgroups"
);
assert_eq!(
two.run_metric(B::WorstGapMs),
Some(9.0),
"max gap across cgroups"
);
assert_eq!(
two.run_metric(B::TotalMigrations),
Some(6.0),
"5 + 1 summed"
);
}
#[test]
fn run_metric_numa_fields_latest_residency_summed_migrations_none_aware() {
use crate::stats::BuiltinMetric as B;
let pcg = |local: u64, total: u64, migr: u64| PhaseCgroupStats {
numa_pages_local: local,
numa_pages_total: total,
cross_node_migrated: migr,
..Default::default()
};
let phase = |step: u16, cgs: &[(&str, PhaseCgroupStats)]| {
let mut b = PhaseBucket {
step_index: step,
..Default::default()
};
for (n, pc) in cgs {
b.per_cgroup.insert(n.to_string(), pc.clone());
}
b
};
let g = ScenarioStats {
phases: vec![
phase(1, &[("A", pcg(900, 1000, 10))]),
phase(2, &[("A", pcg(250, 1000, 20))]),
],
..Default::default()
};
assert_eq!(
g.run_metric(B::WorstPageLocality),
Some(0.25),
"LATEST residency 250/1000, NOT summed (which would give 0.575)"
);
let cross = g.run_metric(B::WorstCrossNodeMigrationRatio).unwrap();
assert!(
(cross - 0.03).abs() < 1e-9,
"summed migrations 30 / latest total 1000 = 0.03; got {cross}"
);
let z = ScenarioStats {
phases: vec![phase(
1,
&[("A", pcg(0, 1000, 0)), ("B", pcg(1000, 1000, 0))],
)],
..Default::default()
};
assert_eq!(
z.run_metric(B::WorstPageLocality),
Some(0.0),
"measured 0.0 (all off-node) is the worst — wins the lowest, not skipped"
);
let n = ScenarioStats {
phases: vec![phase(1, &[("A", pcg(0, 0, 0))])],
..Default::default()
};
assert_eq!(
n.run_metric(B::WorstPageLocality),
None,
"no NUMA pages -> not measured"
);
assert_eq!(n.run_metric(B::WorstCrossNodeMigrationRatio), None);
let churn = ScenarioStats {
phases: vec![phase(1, &[("A", pcg(500, 1000, 3000))])],
..Default::default()
};
let r = churn.run_metric(B::WorstCrossNodeMigrationRatio).unwrap();
assert!(
(r - 3.0).abs() < 1e-9,
"3000 migrations / 1000 pages = 3.0 (>1.0 churn); got {r}"
);
assert_eq!(
churn.run_metric(B::WorstPageLocality),
Some(0.5),
"page_locality 500/1000 stays a bounded [0,1] fraction"
);
let latest_unmeasured = ScenarioStats {
phases: vec![
phase(1, &[("A", pcg(700, 1000, 0))]),
phase(2, &[("A", pcg(0, 0, 0))]),
],
..Default::default()
};
assert_eq!(
latest_unmeasured.run_metric(B::WorstPageLocality),
Some(0.7),
"a later not-measured (total == 0) phase must not erase phase 1's measured residency",
);
let p = ScenarioStats {
cgroups: vec![CgroupStats::default()],
..Default::default()
};
assert_eq!(
p.run_metric(B::WorstPageLocality),
None,
"no phases -> no NUMA carriers"
);
}
#[test]
fn scenario_stats_accessors_accept_typed_and_string_metric_ids() {
use crate::stats::BuiltinMetric;
let mut ext = BTreeMap::new();
ext.insert("worst_run_delay_us".to_string(), 48.0);
let stats = ScenarioStats {
ext_metrics: ext,
..Default::default()
};
let owned = "worst_run_delay_us".to_string();
assert_eq!(stats.run_metric(BuiltinMetric::WorstRunDelayUs), Some(48.0));
assert_eq!(stats.run_metric("worst_run_delay_us"), Some(48.0));
assert_eq!(stats.run_metric(owned.clone()), Some(48.0));
assert_eq!(stats.run_metric(&owned), Some(48.0));
assert_eq!(stats.run_metric("scx_runtime_only_key"), None);
}