use super::*;
#[test]
fn phase_guard_outside_scope_returns_none() {
assert!(crate::assert::current_phase_label().is_none());
let d = crate::assert::AssertDetail::new(crate::assert::DetailKind::Other, "no guard");
assert!(
d.phase.is_none(),
"AssertDetail constructed outside any PhaseGuard must stamp phase=None"
);
}
#[test]
fn phase_guard_install_step_sets_active_label() {
let _g = crate::assert::PhaseGuard::install_step(0);
assert_eq!(
crate::assert::current_phase_label().as_deref(),
Some("Step[0]"),
);
let d = crate::assert::AssertDetail::new(crate::assert::DetailKind::Other, "under Step[0]");
assert_eq!(d.phase.as_deref(), Some("Step[0]"));
}
#[test]
fn phase_guard_install_baseline_sets_active_label() {
let _g = crate::assert::PhaseGuard::install_baseline();
assert_eq!(
crate::assert::current_phase_label().as_deref(),
Some("BASELINE"),
);
}
#[test]
fn phase_guard_drop_restores_prior_label() {
{
let _outer = crate::assert::PhaseGuard::install_step(0); assert_eq!(
crate::assert::current_phase_label().as_deref(),
Some("Step[0]"),
);
{
let _inner = crate::assert::PhaseGuard::install_step(2); assert_eq!(
crate::assert::current_phase_label().as_deref(),
Some("Step[2]"),
);
} assert_eq!(
crate::assert::current_phase_label().as_deref(),
Some("Step[0]"),
"inner guard's Drop must restore the outer guard's label",
);
} assert!(
crate::assert::current_phase_label().is_none(),
"outermost guard's Drop must restore None",
);
}
#[test]
fn phase_guard_passdetail_binary_auto_stamps() {
let _g = crate::assert::PhaseGuard::install_step(1);
let p = crate::assert::PassDetail::binary("metric", "ge", "10.0", "5.0");
assert_eq!(p.phase.as_deref(), Some("Step[1]"));
}
#[test]
fn phase_guard_passdetail_unary_auto_stamps() {
let _g = crate::assert::PhaseGuard::install_step(2);
let p = crate::assert::PassDetail::unary("metric", "is_finite", "42.0");
assert_eq!(p.phase.as_deref(), Some("Step[2]"));
}
#[test]
fn phase_guard_infonote_auto_stamps() {
let _g = crate::assert::PhaseGuard::install_baseline();
let n = crate::assert::InfoNote::new("settle observed");
assert_eq!(n.phase.as_deref(), Some("BASELINE"));
}
#[test]
fn phase_guard_with_phase_builder_overrides_auto_stamp() {
let _g = crate::assert::PhaseGuard::install_step(0); let d = crate::assert::AssertDetail::new(crate::assert::DetailKind::Other, "override")
.with_phase("explicit_override");
assert_eq!(
d.phase.as_deref(),
Some("explicit_override"),
"with_phase builder must override the auto-stamp default",
);
}
#[test]
fn populate_run_ext_metrics_empty_series_inserts_nothing() {
let samples = SampleSeries::from_drained_typed(Vec::new(), None);
let mut target = std::collections::BTreeMap::new();
crate::assert::populate_run_ext_metrics(&samples, &mut target);
assert!(
target.is_empty(),
"no input samples must produce no ext_metrics entries, got {target:?}",
);
}
#[test]
fn populate_run_ext_metrics_does_not_overwrite_existing_keys() {
let samples = SampleSeries::from_drained_typed(Vec::new(), None);
let mut target = std::collections::BTreeMap::new();
target.insert("avg_dsq_depth".to_string(), 42.0);
crate::assert::populate_run_ext_metrics(&samples, &mut target);
assert_eq!(
target.get("avg_dsq_depth").copied(),
Some(42.0),
"existing key must survive populate_run_ext_metrics",
);
}
#[test]
fn build_phase_buckets_avg_imbalance_ratio_from_monitor_samples() {
use crate::monitor::{CpuSnapshot, MonitorReport, MonitorSample};
let cpu = |nr: u32| CpuSnapshot {
nr_running: nr,
..Default::default()
};
let mon = MonitorReport {
samples: vec![
MonitorSample::new(50, vec![cpu(2), cpu(2)]),
MonitorSample::new(100, vec![cpu(4), cpu(2)]),
MonitorSample::new(200, vec![cpu(6), cpu(2)]),
],
..Default::default()
};
let drained = vec![
fixture_entry("periodic_000", 1, 50),
fixture_entry("periodic_001", 1, 250),
];
let samples = SampleSeries::from_drained_typed(drained, Some(mon));
let phases = crate::assert::build_phase_buckets(&samples);
assert_eq!(phases.len(), 1, "single phase from two same-step samples");
let step0 = &phases[0];
let avg = step0
.metrics
.get("avg_imbalance_ratio")
.copied()
.expect("avg_imbalance_ratio must be populated from MonitorSamples");
assert!(
(avg - 2.0).abs() < f64::EPSILON,
"expected mean = 2.0, got {avg}",
);
}
#[test]
fn build_phase_buckets_avg_imbalance_excludes_out_of_window_monitor_samples() {
use crate::monitor::{CpuSnapshot, MonitorReport, MonitorSample};
let cpu = |nr: u32| CpuSnapshot {
nr_running: nr,
..Default::default()
};
let mon = MonitorReport {
samples: vec![
MonitorSample::new(100, vec![cpu(4), cpu(2)]),
MonitorSample::new(150, vec![cpu(4), cpu(2)]),
MonitorSample::new(200, vec![cpu(4), cpu(2)]),
MonitorSample::new(9999, vec![cpu(100), cpu(2)]),
],
..Default::default()
};
let drained = vec![
fixture_entry("periodic_000", 1, 100),
fixture_entry("periodic_001", 1, 200),
];
let samples = SampleSeries::from_drained_typed(drained, Some(mon));
let phases = crate::assert::build_phase_buckets(&samples);
let step0 = &phases[0];
let avg = step0
.metrics
.get("avg_imbalance_ratio")
.copied()
.expect("avg_imbalance_ratio populated");
assert!(
(avg - 2.0).abs() < f64::EPSILON,
"out-of-window sample must not contaminate in-window mean (got {avg})",
);
let max_imb = step0
.metrics
.get("max_imbalance_ratio")
.copied()
.expect("max_imbalance_ratio populated on captured bucket");
assert!(
(max_imb - 2.0).abs() < f64::EPSILON,
"out-of-window outlier (50) must be excluded from max_imbalance_ratio (got {max_imb})",
);
}
#[test]
fn build_phase_buckets_captured_bucket_carries_max_imbalance_and_stuck() {
use crate::monitor::{CpuSnapshot, MonitorReport, MonitorSample};
let cpu = |nr: u32, clk: u64| CpuSnapshot {
nr_running: nr,
rq_clock: clk,
..Default::default()
};
let mon = MonitorReport {
samples: vec![
MonitorSample::new(60, vec![cpu(4, 1000), cpu(2, 1000)]),
MonitorSample::new(120, vec![cpu(6, 1000), cpu(2, 1000)]),
],
..Default::default()
};
let drained = vec![
fixture_entry("periodic_000", 1, 50),
fixture_entry("periodic_001", 1, 250),
];
let samples = SampleSeries::from_drained_typed(drained, Some(mon));
let phases = crate::assert::build_phase_buckets(&samples);
assert_eq!(phases.len(), 1, "single phase");
let step0 = &phases[0];
assert!(
step0.sample_count > 0,
"bucket must be CAPTURED (not synthesized) for this test to mean anything",
);
let max_imb = step0
.metrics
.get("max_imbalance_ratio")
.copied()
.expect("captured bucket must now carry max_imbalance_ratio");
assert!(
(max_imb - 3.0).abs() < f64::EPSILON,
"max imbalance = max(2.0, 3.0) = 3.0, got {max_imb}",
);
let stuck = step0
.metrics
.get("stuck_count")
.copied()
.expect("captured bucket must now carry stuck_count");
assert!(
(stuck - 2.0).abs() < f64::EPSILON,
"two frozen-clock non-idle CPUs => stall_count 2, got {stuck}",
);
}
#[test]
fn build_phase_buckets_single_in_window_sample_has_no_stuck_count() {
use crate::monitor::{CpuSnapshot, MonitorReport, MonitorSample};
let cpu = |nr: u32, clk: u64| CpuSnapshot {
nr_running: nr,
rq_clock: clk,
..Default::default()
};
let mon = MonitorReport {
samples: vec![MonitorSample::new(100, vec![cpu(4, 1000), cpu(2, 1000)])],
..Default::default()
};
let drained = vec![
fixture_entry("periodic_000", 1, 50),
fixture_entry("periodic_001", 1, 250),
];
let samples = SampleSeries::from_drained_typed(drained, Some(mon));
let phases = crate::assert::build_phase_buckets(&samples);
let step0 = &phases[0];
let max_imb = step0
.metrics
.get("max_imbalance_ratio")
.copied()
.expect("single in-window sample still yields max_imbalance_ratio");
assert!(
(max_imb - 2.0).abs() < f64::EPSILON,
"max imbalance = 2.0 from the single sample, got {max_imb}",
);
assert!(
!step0.metrics.contains_key("stuck_count"),
"no consecutive-sample stall => stuck_count must be ABSENT, not 0: {:?}",
step0.metrics.get("stuck_count"),
);
}
#[test]
fn build_phase_buckets_stuck_count_excludes_cross_window_stall_pair() {
use crate::monitor::{CpuSnapshot, MonitorReport, MonitorSample};
let cpu = |nr: u32, clk: u64| CpuSnapshot {
nr_running: nr,
rq_clock: clk,
..Default::default()
};
let mon = MonitorReport {
samples: vec![
MonitorSample::new(100, vec![cpu(4, 1000), cpu(2, 1000)]),
MonitorSample::new(200, vec![cpu(6, 1000), cpu(2, 1000)]),
MonitorSample::new(9999, vec![cpu(8, 1000), cpu(2, 1000)]),
],
..Default::default()
};
let drained = vec![
fixture_entry("periodic_000", 1, 50),
fixture_entry("periodic_001", 1, 250),
];
let samples = SampleSeries::from_drained_typed(drained, Some(mon));
let phases = crate::assert::build_phase_buckets(&samples);
let step0 = &phases[0];
let stuck = step0
.metrics
.get("stuck_count")
.copied()
.expect("in-window stall pair yields stuck_count");
assert!(
(stuck - 2.0).abs() < f64::EPSILON,
"only the fully-in-window pair counts => stuck_count 2 (not 4 with a cross-window pair), got {stuck}",
);
}
#[test]
fn build_phase_buckets_avg_dsq_depth_from_snapshot_dsq_states() {
use crate::monitor::dump::FailureDumpReport;
use crate::monitor::scx_walker::DsqState;
use crate::scenario::snapshot::{DrainedSnapshotEntry, MissingStatsReason};
let mk_entry = |tag: &str, ms: u64| DrainedSnapshotEntry {
tag: tag.to_string(),
report: FailureDumpReport {
schema: SCHEMA_SINGLE.to_string(),
dsq_states: vec![
DsqState {
origin: "local cpu 0".to_string(),
nr: 2,
..Default::default()
},
DsqState {
origin: "local cpu 1".to_string(),
nr: 4,
..Default::default()
},
DsqState {
origin: "local cpu 2".to_string(),
nr: 6,
..Default::default()
},
],
..Default::default()
},
stats: Err(MissingStatsReason::NoSchedulerBinary),
elapsed_ms: Some(ms),
boundary_offset_ms: None,
step_index: Some(1),
};
let drained = vec![mk_entry("periodic_000", 100), mk_entry("periodic_001", 200)];
let samples = SampleSeries::from_drained_typed(drained, None);
let phases = crate::assert::build_phase_buckets(&samples);
let step0 = phases
.iter()
.find(|p| p.step_index == 1)
.expect("Step[0] bucket present");
let avg = step0
.metrics
.get("avg_dsq_depth")
.copied()
.expect("avg_dsq_depth populated from local-cpu DSQ states");
assert!(
(avg - 4.0).abs() < f64::EPSILON,
"expected per-phase avg of mean(2,4,6)=4.0, got {avg}",
);
let max = step0
.metrics
.get("max_dsq_depth")
.copied()
.expect("max_dsq_depth populated alongside avg");
assert!(
(max - 6.0).abs() < f64::EPSILON,
"expected max=6.0, got {max}"
);
}
#[test]
fn build_phase_buckets_with_stimulus_populates_iteration_rate() {
use crate::scenario::snapshot::{DrainedSnapshotEntry, MissingStatsReason};
use crate::timeline::StimulusEvent;
let mk_entry = |tag: &str, step: u16, ms: u64| DrainedSnapshotEntry {
tag: tag.to_string(),
report: fixture_report(),
stats: Err(MissingStatsReason::NoSchedulerBinary),
elapsed_ms: Some(ms),
boundary_offset_ms: None,
step_index: Some(step),
};
let drained = vec![
mk_entry("periodic_000", 1, 100),
mk_entry("periodic_001", 1, 1100),
mk_entry("periodic_002", 2, 1100),
mk_entry("periodic_003", 2, 2100),
];
let samples = SampleSeries::from_drained_typed(drained, None);
let stimulus = vec![
StimulusEvent {
elapsed_ms: 100,
label: "Step[0]".to_string(),
op_kind: None,
detail: None,
total_iterations: Some(0),
step_index: None,
is_terminal: false,
is_step_end: false,
},
StimulusEvent {
elapsed_ms: 1100,
label: "Step[1]".to_string(),
op_kind: None,
detail: None,
total_iterations: Some(1000),
step_index: None,
is_terminal: false,
is_step_end: false,
},
StimulusEvent {
elapsed_ms: 2100,
label: "end".to_string(),
op_kind: None,
detail: None,
total_iterations: Some(3000),
step_index: None,
is_terminal: false,
is_step_end: false,
},
];
let phases = crate::assert::build_phase_buckets_with_stimulus(&samples, &stimulus);
let step1 = phases
.iter()
.find(|p| p.step_index == 2)
.expect("Step[1] bucket present");
let rate = step1
.metrics
.get("iteration_rate")
.copied()
.expect("iteration_rate populated for Step[1]");
assert!(
(rate - 2000.0).abs() < f64::EPSILON,
"expected iteration_rate=2000.0 iter/s, got {rate}",
);
}
#[test]
fn build_phase_buckets_with_stimulus_remaps_by_boundary_offset_over_stamped_step() {
use crate::scenario::snapshot::{DrainedSnapshotEntry, MissingStatsReason};
use crate::timeline::StimulusEvent;
let mk = |tag: &str, offset_ms: u64| DrainedSnapshotEntry {
tag: tag.to_string(),
report: fixture_report(),
stats: Err(MissingStatsReason::NoSchedulerBinary),
elapsed_ms: Some(9_000),
boundary_offset_ms: Some(offset_ms),
step_index: Some(3),
};
let drained = vec![
mk("periodic_base", 500), mk("periodic_000", 1_500), mk("periodic_001", 2_500), mk("periodic_002", 3_500), ];
let samples = SampleSeries::from_drained_typed(drained, None);
let stim = |elapsed_ms: u64, k: u16| StimulusEvent {
elapsed_ms,
label: format!("StepStart[{k}]"),
op_kind: None,
detail: None,
total_iterations: None,
step_index: Some(k),
is_terminal: false,
is_step_end: false,
};
let stimulus = vec![stim(1000, 1), stim(2000, 2), stim(3000, 3)];
let phases = crate::assert::build_phase_buckets_with_stimulus(&samples, &stimulus);
let idxs: Vec<u16> = phases.iter().map(|p| p.step_index).collect();
assert_eq!(
idxs,
vec![0, 1, 2, 3],
"boundary_offset_ms must drive grouping (BASELINE + one capture \
per step), NOT the uniformly-wrong stamped step_index=3 which \
would collapse all four into a single bucket; got {idxs:?}",
);
for p in &phases {
assert_eq!(
p.sample_count, 1,
"each remapped bucket holds exactly its one scheduled capture; \
step_index={} count={}",
p.step_index, p.sample_count,
);
}
}
#[test]
fn build_phase_buckets_with_stimulus_synthesizes_zero_capture_step_bucket() {
use crate::scenario::snapshot::{DrainedSnapshotEntry, MissingStatsReason};
use crate::timeline::StimulusEvent;
let cap = |tag: &str, offset_ms: u64| DrainedSnapshotEntry {
tag: tag.to_string(),
report: fixture_report(),
stats: Err(MissingStatsReason::NoSchedulerBinary),
elapsed_ms: Some(9_000),
boundary_offset_ms: Some(offset_ms),
step_index: Some(1),
};
let drained = vec![cap("periodic_000", 1_500), cap("periodic_001", 3_500)];
let samples = SampleSeries::from_drained_typed(drained, None);
let start = |elapsed_ms: u64, k: u16, iters: u64| StimulusEvent {
elapsed_ms,
label: format!("StepStart[{k}]"),
op_kind: None,
detail: None,
total_iterations: Some(iters),
step_index: Some(k),
is_terminal: false,
is_step_end: false,
};
let stimulus = vec![
start(1000, 1, 0),
start(2000, 2, 1000),
start(3000, 3, 2000),
];
let phases = crate::assert::build_phase_buckets_with_stimulus(&samples, &stimulus);
let step2 = phases
.iter()
.find(|p| p.step_index == 2)
.expect("zero-capture step 2 must still produce a synthesized bucket");
assert_eq!(step2.sample_count, 0, "synthesized bucket is capture-free");
let rate = step2
.metrics
.get("iteration_rate")
.copied()
.expect("step 2's capture-independent iteration_rate must be recovered");
assert!(
(rate - 1000.0).abs() < f64::EPSILON,
"step 2 rate = (2000-1000) iters / 1000 ms = 1000/s, got {rate}",
);
let idxs: Vec<u16> = phases.iter().map(|p| p.step_index).collect();
assert_eq!(
idxs,
vec![1, 2, 3],
"captured steps 1/3 keep their single bucket; step 2 synthesized; \
sorted by step_index; got {idxs:?}",
);
}
#[test]
fn build_phase_buckets_with_stimulus_synthesized_bucket_folds_monitor_imbalance() {
use crate::monitor::{CpuSnapshot, MonitorReport, MonitorSample};
use crate::scenario::snapshot::{DrainedSnapshotEntry, MissingStatsReason};
use crate::timeline::StimulusEvent;
let cpu = |nr: u32| CpuSnapshot {
nr_running: nr,
..Default::default()
};
let mon = MonitorReport {
samples: vec![
MonitorSample::new(1000, vec![cpu(2), cpu(2)]), MonitorSample::new(2500, vec![cpu(6), cpu(2)]), ],
..Default::default()
};
let cap = |tag: &str, offset_ms: u64| DrainedSnapshotEntry {
tag: tag.to_string(),
report: fixture_report(),
stats: Err(MissingStatsReason::NoSchedulerBinary),
elapsed_ms: Some(9_000),
boundary_offset_ms: Some(offset_ms),
step_index: Some(1),
};
let drained = vec![cap("periodic_000", 1_500), cap("periodic_001", 3_500)];
let samples = SampleSeries::from_drained_typed(drained, Some(mon));
let start = |elapsed_ms: u64, k: u16| StimulusEvent {
elapsed_ms,
label: format!("StepStart[{k}]"),
op_kind: None,
detail: None,
total_iterations: None,
step_index: Some(k),
is_terminal: false,
is_step_end: false,
};
let stimulus = vec![start(1000, 1), start(2000, 2), start(3000, 3)];
let phases = crate::assert::build_phase_buckets_with_stimulus(&samples, &stimulus);
let step2 = phases
.iter()
.find(|p| p.step_index == 2)
.expect("synthesized zero-capture step 2 bucket present");
assert_eq!(step2.sample_count, 0);
let avg = step2
.metrics
.get("avg_imbalance_ratio")
.copied()
.expect("synthesized bucket must recover avg_imbalance_ratio from monitor samples");
assert!(
(avg - 3.0).abs() < f64::EPSILON,
"step 2 in-window monitor imbalance = 6 / max(1, 2) = 3.0, got {avg}",
);
assert!(
!step2.metrics.contains_key("iteration_rate"),
"None total_iterations must yield NO iteration_rate (no fabrication); got {:?}",
step2.metrics,
);
}
#[test]
fn build_phase_buckets_with_stimulus_single_captured_step_no_spurious_synthesis() {
use crate::scenario::snapshot::{DrainedSnapshotEntry, MissingStatsReason};
use crate::timeline::StimulusEvent;
let cap = DrainedSnapshotEntry {
tag: "periodic_000".to_string(),
report: fixture_report(),
stats: Err(MissingStatsReason::NoSchedulerBinary),
elapsed_ms: Some(9_000),
boundary_offset_ms: Some(1_500),
step_index: Some(1),
};
let samples = SampleSeries::from_drained_typed(vec![cap], None);
let stimulus = vec![StimulusEvent {
elapsed_ms: 1000,
label: "StepStart[1]".to_string(),
op_kind: None,
detail: None,
total_iterations: None,
step_index: Some(1),
is_terminal: false,
is_step_end: false,
}];
let phases = crate::assert::build_phase_buckets_with_stimulus(&samples, &stimulus);
let idxs: Vec<u16> = phases.iter().map(|p| p.step_index).collect();
assert_eq!(
idxs,
vec![1],
"exactly the one captured step; the synthesize loop adds no extras",
);
assert_eq!(phases[0].sample_count, 1);
}
#[test]
fn build_phase_buckets_with_stimulus_sched_died_last_step_yields_empty_present_bucket() {
use crate::scenario::snapshot::{DrainedSnapshotEntry, MissingStatsReason};
use crate::timeline::StimulusEvent;
let cap = DrainedSnapshotEntry {
tag: "periodic_000".to_string(),
report: fixture_report(),
stats: Err(MissingStatsReason::NoSchedulerBinary),
elapsed_ms: Some(9_000),
boundary_offset_ms: Some(1_500),
step_index: Some(1),
};
let samples = SampleSeries::from_drained_typed(vec![cap], None);
let start = |elapsed_ms: u64, k: u16, iters: u64| StimulusEvent {
elapsed_ms,
label: format!("StepStart[{k}]"),
op_kind: None,
detail: None,
total_iterations: Some(iters),
step_index: Some(k),
is_terminal: false,
is_step_end: false,
};
let stimulus = vec![start(1000, 1, 0), start(2000, 2, 1000)];
let phases = crate::assert::build_phase_buckets_with_stimulus(&samples, &stimulus);
let step2 = phases
.iter()
.find(|p| p.step_index == 2)
.expect("sched-died last step still gets a present bucket");
assert_eq!(step2.sample_count, 0);
assert_eq!(
step2.end_ms,
u64::MAX,
"no StepEnd / successor start -> open-ended window",
);
assert!(
!step2.metrics.contains_key("iteration_rate"),
"no right boundary -> no rate (absent, not a phantom 0); got {:?}",
step2.metrics,
);
}
#[test]
fn synthesized_zero_sample_bucket_flags_throughput_not_phantom_monitor() {
use crate::timeline::{ChangeDirection, Timeline, TimelineContext};
let captured = |k: u16, imbalance: f64, rate: f64| crate::assert::PhaseBucket {
per_cgroup: Default::default(),
step_index: k,
label: format!("Step[{}]", k.saturating_sub(1)),
start_ms: (k as u64) * 1000,
end_ms: (k as u64) * 1000 + 1000,
sample_count: 5,
metrics: std::collections::BTreeMap::from([
("avg_imbalance_ratio".to_string(), imbalance),
("iteration_rate".to_string(), rate),
]),
};
let buckets = vec![
captured(1, 1.0, 1000.0),
crate::assert::PhaseBucket {
per_cgroup: Default::default(),
step_index: 2,
label: "Step[1]".to_string(),
start_ms: 2000,
end_ms: 3000,
sample_count: 0,
metrics: std::collections::BTreeMap::from([
("avg_imbalance_ratio".to_string(), 100.0),
("iteration_rate".to_string(), 10.0),
]),
},
captured(3, 1.0, 1000.0),
];
let timeline = Timeline::from_phase_buckets(&buckets, &[], &TimelineContext::default());
assert!(
timeline
.phases
.iter()
.all(|p| !p.changes.iter().any(|c| c.metric == "imbalance")),
"a zero-sample bucket's wild imbalance must not flag a phantom change; got {:?}",
timeline
.phases
.iter()
.map(|p| (p.index, p.changes.clone()))
.collect::<Vec<_>>(),
);
let into_synth = &timeline.phases[1];
assert_eq!(
into_synth.index, 2,
"phases[1] is step_index 2 (the synthesized step)"
);
assert!(
into_synth
.changes
.iter()
.any(|c| c.metric == "throughput" && c.direction == ChangeDirection::Degraded),
"throughput collapse into the synthesized step must flag Degraded; got {:?}",
into_synth.changes,
);
let out_of_synth = &timeline.phases[2];
assert_eq!(
out_of_synth.index, 3,
"phases[2] is step_index 3 (the captured step after)"
);
assert!(
out_of_synth
.changes
.iter()
.any(|c| c.metric == "throughput" && c.direction == ChangeDirection::Improved),
"throughput recovery out of the synthesized step must flag Improved; got {:?}",
out_of_synth.changes,
);
}
#[test]
fn build_phase_buckets_with_stimulus_step_end_tie_attributes_step_local_rate() {
use crate::scenario::snapshot::{DrainedSnapshotEntry, MissingStatsReason};
use crate::timeline::StimulusEvent;
let cap = |tag: &str, offset_ms: u64| DrainedSnapshotEntry {
tag: tag.to_string(),
report: fixture_report(),
stats: Err(MissingStatsReason::NoSchedulerBinary),
elapsed_ms: Some(9_000),
boundary_offset_ms: Some(offset_ms),
step_index: Some(1),
};
let drained = vec![cap("periodic_000", 1_500), cap("periodic_001", 2_500)];
let samples = SampleSeries::from_drained_typed(drained, None);
let ev = |elapsed_ms: u64, k: u16, iters: u64, is_step_end: bool| StimulusEvent {
elapsed_ms,
label: if is_step_end {
format!("StepEnd[{k}]")
} else {
format!("StepStart[{k}]")
},
op_kind: None,
detail: None,
total_iterations: Some(iters),
step_index: Some(k),
is_terminal: false,
is_step_end,
};
let stimulus = vec![
ev(1000, 1, 0, false), ev(2000, 1, 500, true), ev(2000, 2, 9000, false), ];
let phases = crate::assert::build_phase_buckets_with_stimulus(&samples, &stimulus);
let step1 = phases
.iter()
.find(|p| p.step_index == 1)
.expect("step 1 bucket present");
let rate = step1
.metrics
.get("iteration_rate")
.copied()
.expect("step 1 iteration_rate populated");
assert!(
(rate - 500.0).abs() < f64::EPSILON,
"step 1 must get its LOCAL StepStart[1]->StepEnd[1] rate (500/s), \
not the cross-step StepStart[1]->StepStart[2] delta (9000/s); got {rate}",
);
}
#[test]
fn build_phase_buckets_with_stimulus_synthesized_end_ms_clamped_to_next_start() {
use crate::scenario::snapshot::DrainedSnapshotEntry;
use crate::timeline::StimulusEvent;
let samples = SampleSeries::from_drained_typed(Vec::<DrainedSnapshotEntry>::new(), None);
let ev = |elapsed_ms: u64, k: u16, is_step_end: bool| StimulusEvent {
elapsed_ms,
label: format!("Step{}[{k}]", if is_step_end { "End" } else { "Start" }),
op_kind: None,
detail: None,
total_iterations: Some(0),
step_index: Some(k),
is_terminal: false,
is_step_end,
};
let stimulus = vec![
ev(1000, 1, false), ev(2000, 2, false), ev(5000, 1, true), ];
let phases = crate::assert::build_phase_buckets_with_stimulus(&samples, &stimulus);
let step1 = phases
.iter()
.find(|p| p.step_index == 1)
.expect("synthesized step 1 bucket present");
assert_eq!(
step1.end_ms, 2000,
"synthesized step 1 end_ms must clamp to StepStart[2]=2000, NOT the \
corrupt StepEnd[1]=5000 that would overlap step 2; got {}",
step1.end_ms,
);
}
#[test]
fn build_phase_buckets_with_stimulus_synthesized_bucket_folds_full_monitor_set() {
use crate::monitor::{CpuSnapshot, MonitorReport, MonitorSample, ScxEventCounters};
use crate::scenario::snapshot::DrainedSnapshotEntry;
use crate::timeline::StimulusEvent;
let cpu = |nr: u32, dsq: u32, rq: u64, ev: Option<ScxEventCounters>| CpuSnapshot {
nr_running: nr,
local_dsq_depth: dsq,
rq_clock: rq,
scx_nr_running: 0,
scx_flags: 0,
event_counters: ev,
schedstat: None,
vcpu_cpu_time_ns: None,
vcpu_perf: None,
sched_domains: None,
};
let evc = |fb: i64, kl: i64| {
Some(ScxEventCounters {
select_cpu_fallback: fb,
dispatch_keep_last: kl,
..Default::default()
})
};
let mon = MonitorReport {
samples: vec![
MonitorSample {
prog_stats: None,
elapsed_ms: 1000,
cpus: vec![cpu(4, 3, 100, evc(10, 5)), cpu(2, 1, 100, None)],
},
MonitorSample {
prog_stats: None,
elapsed_ms: 1500,
cpus: vec![cpu(4, 3, 100, evc(110, 55)), cpu(2, 1, 200, None)],
},
],
..Default::default()
};
let samples = SampleSeries::from_drained_typed(Vec::<DrainedSnapshotEntry>::new(), Some(mon));
let start = |elapsed_ms: u64, k: u16| StimulusEvent {
elapsed_ms,
label: format!("StepStart[{k}]"),
op_kind: None,
detail: None,
total_iterations: None,
step_index: Some(k),
is_terminal: false,
is_step_end: false,
};
let stimulus = vec![start(1000, 1), start(2000, 2)];
let phases = crate::assert::build_phase_buckets_with_stimulus(&samples, &stimulus);
let step1 = phases
.iter()
.find(|p| p.step_index == 1)
.expect("synthesized step 1 bucket present");
assert_eq!(step1.sample_count, 0, "synthesized bucket is capture-free");
let g = |k: &str| step1.metrics.get(k).copied();
assert_eq!(g("max_imbalance_ratio"), Some(2.0), "max imbalance");
assert_eq!(g("avg_dsq_depth"), Some(2.0), "avg dsq depth");
assert_eq!(g("max_dsq_depth"), Some(3.0), "max dsq depth");
assert_eq!(
g("total_fallback"),
Some(100.0),
"fallback counter delta 110-10"
);
assert_eq!(
g("total_keep_last"),
Some(50.0),
"keep_last counter delta 55-5"
);
assert_eq!(g("avg_imbalance_ratio"), Some(2.0), "avg imbalance");
}