use super::*;
#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize, crate::Claim)]
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>,
#[serde(default)]
pub phases: Vec<PhaseBucket>,
}
impl ScenarioStats {
pub fn phase(&self, phase: Phase) -> Option<&PhaseBucket> {
self.phases.iter().find(|p| p.step_index == phase.as_u16())
}
pub fn phase_metric(
&self,
phase: Phase,
metric: impl Into<crate::stats::MetricId>,
) -> Option<f64> {
let metric = metric.into();
self.phase(phase).and_then(|p| {
p.get(metric.as_str())
.or_else(|| p.cgroup_counter_total(metric.as_str()))
})
}
pub fn cgroup_balance_ratio(&self) -> Option<f64> {
let mut min = f64::INFINITY;
let mut max = 0.0_f64;
let mut n = 0usize;
for cg in &self.cgroups {
if let Some(rate) = cg.iterations_per_worker() {
min = min.min(rate);
max = max.max(rate);
n += 1;
}
}
if n < 2 {
return None;
}
if min == 0.0 {
return Some(f64::INFINITY);
}
Some(max / min)
}
pub fn phase_cgroup_metric(
&self,
phase: Phase,
cgroup: &str,
metric: impl Into<crate::stats::MetricId>,
) -> Option<f64> {
let metric = metric.into();
self.phase(phase)
.and_then(|p| p.per_cgroup.get(cgroup))
.and_then(|pc| {
pc.get(metric.as_str())
.or_else(|| pc.cgroup_counter(metric.as_str()))
})
}
pub fn has_steps(&self) -> bool {
self.phases.iter().any(|p| p.step_index >= 1)
}
pub fn run_metric(&self, metric: impl Into<crate::stats::MetricId>) -> Option<f64> {
let id = metric.into();
if let crate::stats::MetricId::Builtin(b) = &id
&& let Some(resolved) = self.typed_sentinel_metric(*b)
{
return resolved;
}
self.ext_metrics.get(id.as_str()).copied()
}
fn typed_sentinel_metric(&self, m: crate::stats::BuiltinMetric) -> Option<Option<f64>> {
use crate::stats::BuiltinMetric as B;
Some(match m {
B::WorstSpread => self
.cgroups
.iter()
.filter_map(|c| c.spread)
.reduce(f64::max),
B::WorstMigrationRatio => self
.cgroups
.iter()
.filter(|c| c.total_iterations > 0)
.map(|c| c.migration_ratio)
.reduce(f64::max),
B::TotalMigrations => (!self.cgroups.is_empty()).then(|| {
self.cgroups
.iter()
.map(|c| c.total_migrations)
.fold(0u64, u64::saturating_add) as f64
}),
B::TotalIterations => (!self.cgroups.is_empty()).then(|| {
self.cgroups
.iter()
.map(|c| c.total_iterations)
.fold(0u64, u64::saturating_add) as f64
}),
B::WorstGapMs => self
.cgroups
.iter()
.filter(|c| c.num_workers > 0)
.map(|c| c.max_gap_ms as f64)
.reduce(f64::max),
B::WorstPageLocality => self.worst_page_locality(),
B::WorstCrossNodeMigrationRatio => self.worst_cross_node_migration_ratio(),
_ => return None,
})
}
fn numa_agg_per_cgroup(&self) -> Vec<(u64, u64, u64)> {
let mut by_cg: std::collections::BTreeMap<&str, (u64, u64, u64)> =
std::collections::BTreeMap::new();
for phase in &self.phases {
for (name, pc) in &phase.per_cgroup {
let e = by_cg.entry(name.as_str()).or_insert((0, 0, 0));
if pc.numa_pages_total > 0 {
e.0 = pc.numa_pages_local;
e.1 = pc.numa_pages_total;
}
e.2 = e.2.saturating_add(pc.cross_node_migrated);
}
}
by_cg.into_values().collect()
}
fn worst_page_locality(&self) -> Option<f64> {
self.numa_agg_per_cgroup()
.into_iter()
.filter(|&(_, total, _)| total > 0)
.map(|(local, total, _)| super::reductions::page_locality_of(local, total))
.reduce(f64::min)
}
fn worst_cross_node_migration_ratio(&self) -> Option<f64> {
self.numa_agg_per_cgroup()
.into_iter()
.filter(|&(_, total, _)| total > 0)
.map(|(_, total, migrated)| {
super::reductions::cross_node_migration_ratio_of(migrated, total)
})
.reduce(f64::max)
}
}
const TYPED_FIELD_NAMES: &[&str] = &[
"max_dsq_depth",
"max_imbalance_ratio",
"total_fallback",
"total_keep_last",
"stuck_count",
"total_iterations",
"total_migrations",
];
pub fn populate_run_ext_metrics_from_phases(
phases: &[PhaseBucket],
target: &mut std::collections::BTreeMap<String, f64>,
) {
let mut keys: std::collections::BTreeSet<&String> = std::collections::BTreeSet::new();
for phase in phases {
for key in phase.metrics.keys() {
keys.insert(key);
}
}
for key in keys {
if target.contains_key(key) {
continue;
}
let Some(def) = crate::stats::metric_def(key) else {
continue;
};
if def.kind.is_derived() {
continue;
}
if TYPED_FIELD_NAMES.contains(&key.as_str()) {
continue;
}
if key == "avg_nr_running" {
continue;
}
let pairs: Vec<(f64, usize)> = phases
.iter()
.filter_map(|phase| {
phase
.metrics
.get(key)
.copied()
.map(|v| (v, phase.sample_count.max(1)))
})
.collect();
if pairs.is_empty() {
continue;
}
if def.kind == crate::stats::MetricKind::PerPhaseDeltaSum {
let sum: f64 = pairs.iter().map(|(v, _)| v).sum();
target.insert(key.clone(), sum);
continue;
}
if let Some(reduced) = crate::stats::aggregate_samples_weighted(&pairs, def.kind) {
target.insert(key.clone(), reduced);
}
}
crate::stats::derive_rate_metrics(target);
}
pub fn populate_run_ext_all(
stats: &mut ScenarioStats,
samples: &crate::scenario::sample::SampleSeries,
) {
populate_run_ext_metrics(samples, &mut stats.ext_metrics);
populate_run_ext_metrics_from_phases(&stats.phases, &mut stats.ext_metrics);
populate_run_pooled_iterations_per_cpu_sec(stats);
populate_run_pooled_taobench(stats);
populate_run_pooled_taobench_distribution(stats);
populate_run_pooled_schbench(stats);
populate_run_pooled_schbench_distribution(stats);
populate_run_distribution_metrics(stats);
}
pub fn populate_run_pooled_iterations_per_cpu_sec(stats: &mut ScenarioStats) {
let summed_ns: u64 = stats
.cgroups
.iter()
.filter(|c| c.total_cpu_time_ns > 0)
.map(|c| c.total_cpu_time_ns)
.fold(0u64, u64::saturating_add);
if summed_ns == 0 {
return;
}
let summed_iters: u64 = stats
.cgroups
.iter()
.filter(|c| c.total_cpu_time_ns > 0)
.map(|c| c.total_iterations)
.fold(0u64, u64::saturating_add);
stats
.ext_metrics
.insert("total_iterations_pooled".to_string(), summed_iters as f64);
stats
.ext_metrics
.insert("total_cpu_time_sec".to_string(), summed_ns as f64 / 1e9);
crate::stats::derive_rate_metrics(&mut stats.ext_metrics);
}
pub fn populate_run_pooled_taobench(stats: &mut ScenarioStats) {
use crate::stats::{
TOTAL_TAOBENCH_FAST_OPS, TOTAL_TAOBENCH_GET_CMDS, TOTAL_TAOBENCH_GET_HITS,
TOTAL_TAOBENCH_OPS, TOTAL_TAOBENCH_SLOW_OPS, TOTAL_TAOBENCH_WALL_SEC,
};
let pooled = stats
.cgroups
.iter()
.filter_map(|c| c.taobench_whole.as_ref())
.fold(
None,
|acc: Option<crate::workload::taobench::run::TaobenchStats>, t| {
Some(match acc {
Some(mut a) => {
a.merge(t);
a
}
None => *t,
})
},
);
let Some(w) = pooled else {
return;
};
let c = &w;
if c.elapsed_ns == 0 {
return;
}
stats
.ext_metrics
.insert(TOTAL_TAOBENCH_OPS.to_string(), c.total_ops() as f64);
stats
.ext_metrics
.insert(TOTAL_TAOBENCH_FAST_OPS.to_string(), c.fast_ops as f64);
stats
.ext_metrics
.insert(TOTAL_TAOBENCH_SLOW_OPS.to_string(), c.slow_ops as f64);
stats.ext_metrics.insert(
TOTAL_TAOBENCH_WALL_SEC.to_string(),
c.elapsed_ns as f64 / 1e9,
);
stats
.ext_metrics
.insert(TOTAL_TAOBENCH_GET_CMDS.to_string(), c.get_cmds as f64);
stats.ext_metrics.insert(
TOTAL_TAOBENCH_GET_HITS.to_string(),
c.get_cmds.saturating_sub(c.get_misses) as f64,
);
crate::stats::derive_rate_metrics(&mut stats.ext_metrics);
}
pub fn populate_run_pooled_taobench_distribution(stats: &mut ScenarioStats) {
use crate::stats::{
TAOBENCH_SERVE_MAX_US_WHOLE, TAOBENCH_SERVE_MIN_US_WHOLE, TAOBENCH_SERVE_P50_US_WHOLE,
TAOBENCH_SERVE_P90_US_WHOLE, TAOBENCH_SERVE_P99_US_WHOLE, TAOBENCH_SERVE_P999_US_WHOLE,
};
use crate::workload::schbench::plat::{Pct, PlatStats};
let mut serve = PlatStats::default();
for phase in &stats.phases {
for pc in phase.per_cgroup.values() {
if let Some(t) = pc.taobench.as_ref() {
serve.combine(&t.serve_lat);
}
}
}
if serve.sample_count() == 0 {
return;
}
let q = serve.percentiles();
stats.ext_metrics.insert(
TAOBENCH_SERVE_P50_US_WHOLE.to_string(),
q.value_at(Pct::P50) as f64,
);
stats.ext_metrics.insert(
TAOBENCH_SERVE_P90_US_WHOLE.to_string(),
q.value_at(Pct::P90) as f64,
);
stats.ext_metrics.insert(
TAOBENCH_SERVE_P99_US_WHOLE.to_string(),
q.value_at(Pct::P99) as f64,
);
stats.ext_metrics.insert(
TAOBENCH_SERVE_P999_US_WHOLE.to_string(),
q.value_at(Pct::P999) as f64,
);
stats
.ext_metrics
.insert(TAOBENCH_SERVE_MIN_US_WHOLE.to_string(), q.min as f64);
stats
.ext_metrics
.insert(TAOBENCH_SERVE_MAX_US_WHOLE.to_string(), q.max as f64);
}
pub fn populate_run_pooled_schbench(stats: &mut ScenarioStats) {
use crate::stats::{
TOTAL_SCHBENCH_LOOPS, TOTAL_SCHBENCH_MSG_PCOUNT, TOTAL_SCHBENCH_MSG_RUN_DELAY_NS,
TOTAL_SCHBENCH_WORKER_PCOUNT, TOTAL_SCHBENCH_WORKER_RUN_DELAY_NS,
};
let mut msg_run_delay_ns: u64 = 0;
let mut msg_pcount: u64 = 0;
let mut worker_run_delay_ns: u64 = 0;
let mut worker_pcount: u64 = 0;
let mut loops: u64 = 0;
let mut any = false;
for phase in &stats.phases {
for pc in phase.per_cgroup.values() {
if let Some(s) = pc.schbench.as_ref() {
any = true;
msg_run_delay_ns = msg_run_delay_ns.saturating_add(s.msg_run_delay_ns);
msg_pcount = msg_pcount.saturating_add(s.msg_pcount);
worker_run_delay_ns = worker_run_delay_ns.saturating_add(s.worker_run_delay_ns);
worker_pcount = worker_pcount.saturating_add(s.worker_pcount);
loops = loops.saturating_add(s.loop_count);
}
}
}
if !any {
return;
}
stats
.ext_metrics
.insert(TOTAL_SCHBENCH_LOOPS.to_string(), loops as f64);
if msg_pcount > 0 {
stats.ext_metrics.insert(
TOTAL_SCHBENCH_MSG_RUN_DELAY_NS.to_string(),
msg_run_delay_ns as f64,
);
stats
.ext_metrics
.insert(TOTAL_SCHBENCH_MSG_PCOUNT.to_string(), msg_pcount as f64);
}
if worker_pcount > 0 {
stats.ext_metrics.insert(
TOTAL_SCHBENCH_WORKER_RUN_DELAY_NS.to_string(),
worker_run_delay_ns as f64,
);
stats.ext_metrics.insert(
TOTAL_SCHBENCH_WORKER_PCOUNT.to_string(),
worker_pcount as f64,
);
}
crate::stats::derive_rate_metrics(&mut stats.ext_metrics);
}
pub fn populate_run_pooled_schbench_distribution(stats: &mut ScenarioStats) {
use crate::stats::{
SCHBENCH_REQUEST_MAX_US_WHOLE, SCHBENCH_REQUEST_MIN_US_WHOLE,
SCHBENCH_REQUEST_P50_US_WHOLE, SCHBENCH_REQUEST_P90_US_WHOLE,
SCHBENCH_REQUEST_P99_US_WHOLE, SCHBENCH_REQUEST_P999_US_WHOLE, SCHBENCH_RPS_MAX_WHOLE,
SCHBENCH_RPS_MIN_WHOLE, SCHBENCH_RPS_P20_WHOLE, SCHBENCH_RPS_P50_WHOLE,
SCHBENCH_RPS_P90_WHOLE, SCHBENCH_WAKEUP_MAX_US_WHOLE, SCHBENCH_WAKEUP_MIN_US_WHOLE,
SCHBENCH_WAKEUP_P50_US_WHOLE, SCHBENCH_WAKEUP_P90_US_WHOLE, SCHBENCH_WAKEUP_P99_US_WHOLE,
SCHBENCH_WAKEUP_P999_US_WHOLE,
};
use crate::workload::schbench::plat::{Pct, PlatStats};
let mut wakeup = PlatStats::default();
let mut request = PlatStats::default();
let mut rps = PlatStats::default();
for phase in &stats.phases {
for pc in phase.per_cgroup.values() {
if let Some(s) = pc.schbench.as_ref() {
wakeup.combine(&s.wakeup);
request.combine(&s.request);
rps.combine(&s.rps);
}
}
}
if wakeup.sample_count() > 0 {
let q = wakeup.percentiles();
stats.ext_metrics.insert(
SCHBENCH_WAKEUP_P50_US_WHOLE.to_string(),
q.value_at(Pct::P50) as f64,
);
stats.ext_metrics.insert(
SCHBENCH_WAKEUP_P90_US_WHOLE.to_string(),
q.value_at(Pct::P90) as f64,
);
stats.ext_metrics.insert(
SCHBENCH_WAKEUP_P99_US_WHOLE.to_string(),
q.value_at(Pct::P99) as f64,
);
stats.ext_metrics.insert(
SCHBENCH_WAKEUP_P999_US_WHOLE.to_string(),
q.value_at(Pct::P999) as f64,
);
stats
.ext_metrics
.insert(SCHBENCH_WAKEUP_MIN_US_WHOLE.to_string(), q.min as f64);
stats
.ext_metrics
.insert(SCHBENCH_WAKEUP_MAX_US_WHOLE.to_string(), q.max as f64);
}
if request.sample_count() > 0 {
let q = request.percentiles();
stats.ext_metrics.insert(
SCHBENCH_REQUEST_P50_US_WHOLE.to_string(),
q.value_at(Pct::P50) as f64,
);
stats.ext_metrics.insert(
SCHBENCH_REQUEST_P90_US_WHOLE.to_string(),
q.value_at(Pct::P90) as f64,
);
stats.ext_metrics.insert(
SCHBENCH_REQUEST_P99_US_WHOLE.to_string(),
q.value_at(Pct::P99) as f64,
);
stats.ext_metrics.insert(
SCHBENCH_REQUEST_P999_US_WHOLE.to_string(),
q.value_at(Pct::P999) as f64,
);
stats
.ext_metrics
.insert(SCHBENCH_REQUEST_MIN_US_WHOLE.to_string(), q.min as f64);
stats
.ext_metrics
.insert(SCHBENCH_REQUEST_MAX_US_WHOLE.to_string(), q.max as f64);
}
if rps.sample_count() > 0 {
let r = rps.percentiles();
stats.ext_metrics.insert(
SCHBENCH_RPS_P20_WHOLE.to_string(),
r.value_at(Pct::P20) as f64,
);
stats.ext_metrics.insert(
SCHBENCH_RPS_P50_WHOLE.to_string(),
r.value_at(Pct::P50) as f64,
);
stats.ext_metrics.insert(
SCHBENCH_RPS_P90_WHOLE.to_string(),
r.value_at(Pct::P90) as f64,
);
stats
.ext_metrics
.insert(SCHBENCH_RPS_MIN_WHOLE.to_string(), r.min as f64);
stats
.ext_metrics
.insert(SCHBENCH_RPS_MAX_WHOLE.to_string(), r.max as f64);
}
}
pub fn populate_run_distribution_metrics(stats: &mut ScenarioStats) {
let mut wake_pool: Vec<(u64, f64)> = Vec::new();
let mut timer_pool: Vec<(u64, f64)> = Vec::new();
let mut run_delay_pool: Vec<u64> = Vec::new();
let mut wake_carriers: std::collections::BTreeSet<&str> = std::collections::BTreeSet::new();
let mut timer_carriers: std::collections::BTreeSet<&str> = std::collections::BTreeSet::new();
let mut run_delay_carriers: std::collections::BTreeSet<&str> =
std::collections::BTreeSet::new();
for phase in &stats.phases {
for (cgname, pcg) in &phase.per_cgroup {
if !pcg.wake_latencies_ns.is_empty() {
let len = pcg.wake_latencies_ns.len() as u64;
debug_assert!(
pcg.wake_sample_total >= len,
"wake_sample_total ({}) < reservoir len ({}): malformed carrier",
pcg.wake_sample_total,
len,
);
let w = pcg.wake_sample_total.max(len) as f64 / len as f64;
wake_pool.extend(pcg.wake_latencies_ns.iter().map(|&v| (v, w)));
wake_carriers.insert(cgname.as_str());
}
if !pcg.timer_latencies_ns.is_empty() {
let len = pcg.timer_latencies_ns.len() as u64;
debug_assert!(
pcg.timer_sample_total >= len,
"timer_sample_total ({}) < reservoir len ({}): malformed carrier",
pcg.timer_sample_total,
len,
);
let w = pcg.timer_sample_total.max(len) as f64 / len as f64;
timer_pool.extend(pcg.timer_latencies_ns.iter().map(|&v| (v, w)));
timer_carriers.insert(cgname.as_str());
}
if !pcg.run_delays_ns.is_empty() {
run_delay_pool.extend_from_slice(&pcg.run_delays_ns);
run_delay_carriers.insert(cgname.as_str());
}
}
}
wake_pool.sort_unstable_by_key(|&(v, _)| v);
timer_pool.sort_unstable_by_key(|&(v, _)| v);
run_delay_pool.sort_unstable();
populate_run_distribution_metrics_from(
&mut stats.ext_metrics,
crate::stats::METRICS.iter().filter_map(|m| {
matches!(
m.kind,
crate::stats::MetricKind::Distribution { .. }
| crate::stats::MetricKind::WorstLowest { .. }
| crate::stats::MetricKind::WakeLatencyTailRatio
)
.then_some((m.name, m.kind))
}),
&wake_pool,
&wake_carriers,
&timer_pool,
&timer_carriers,
&run_delay_pool,
&run_delay_carriers,
&stats.cgroups,
stats.total_iterations,
);
if let Some(v) = stats.worst_page_locality() {
stats
.ext_metrics
.insert("worst_page_locality".to_string(), v);
}
if let Some(v) = stats.worst_cross_node_migration_ratio() {
stats
.ext_metrics
.insert("worst_cross_node_migration_ratio".to_string(), v);
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn populate_run_distribution_metrics_from<'a>(
target: &mut std::collections::BTreeMap<String, f64>,
metrics: impl Iterator<Item = (&'a str, crate::stats::MetricKind)>,
wake_pool: &[(u64, f64)],
wake_carriers: &std::collections::BTreeSet<&str>,
timer_pool: &[(u64, f64)],
timer_carriers: &std::collections::BTreeSet<&str>,
run_delay_pool: &[u64],
run_delay_carriers: &std::collections::BTreeSet<&str>,
cgroups: &[CgroupStats],
run_total_iterations: u64,
) {
use crate::stats::{MetricKind, SampleSource, WorstLowestDenominator, WorstLowestNumerator};
for (name, kind) in metrics {
let value: Option<f64> = match kind {
MetricKind::Distribution { source, reduction } => {
let (mut v, carriers): (Option<f64>, &std::collections::BTreeSet<&str>) =
match source {
SampleSource::WakeLatencyNs => (
(!wake_pool.is_empty())
.then(|| reduce_weighted_sorted_distribution(wake_pool, reduction)),
wake_carriers,
),
SampleSource::TimerLatencyNs => (
(!timer_pool.is_empty()).then(|| {
reduce_weighted_sorted_distribution(timer_pool, reduction)
}),
timer_carriers,
),
SampleSource::RunDelayNs => (
(!run_delay_pool.is_empty())
.then(|| reduce_sorted_distribution(run_delay_pool, reduction)),
run_delay_carriers,
),
};
for cg in cgroups {
if !carriers.contains(cg.cgroup_name.as_str()) && cg.measured_for(source) {
let r = distribution_cgroup_reduction(cg, source, reduction);
v = Some(v.map_or(r, |acc| acc.max(r)));
}
}
v
}
MetricKind::WorstLowest {
numerator: WorstLowestNumerator::Iterations,
denominator,
} => {
let mut worst: Option<f64> = None;
for cg in cgroups {
let per_cg = match denominator {
WorstLowestDenominator::NumWorkers => cg.iterations_per_worker(),
WorstLowestDenominator::CpuTimeNs => cg.iterations_per_cpu_sec(),
WorstLowestDenominator::NumaTotal => None,
};
if let Some(v) = per_cg
&& worst.is_none_or(|w| v < w)
{
worst = Some(v);
}
}
worst
}
MetricKind::WorstLowest {
numerator: WorstLowestNumerator::NumaLocal,
..
} => None,
MetricKind::WakeLatencyTailRatio => {
if run_total_iterations < crate::stats::WAKE_LATENCY_TAIL_RATIO_MIN_ITERATIONS {
None
} else {
let mut worst: Option<f64> = None;
for cg in cgroups {
let r = cg.wake_latency_tail_ratio();
if r > 0.0 {
worst = Some(worst.map_or(r, |w| w.max(r)));
}
}
worst
}
}
_ => None,
};
if let Some(v) = value.filter(|v| v.is_finite()) {
target.insert(name.to_string(), v);
}
}
}
pub(crate) fn reduce_sorted_distribution(
sorted: &[u64],
reduction: crate::stats::SampleReduction,
) -> f64 {
use crate::stats::SampleReduction;
match reduction {
SampleReduction::P99 => percentile(sorted, 0.99) as f64 / 1000.0,
SampleReduction::P999 => percentile(sorted, 0.999) as f64 / 1000.0,
SampleReduction::Median => percentile(sorted, 0.5) as f64 / 1000.0,
SampleReduction::Cv => {
let n = sorted.len() as f64;
let mean_ns = sorted.iter().sum::<u64>() as f64 / n;
if mean_ns > 0.0 {
let variance = sorted
.iter()
.map(|&v| (v as f64 - mean_ns).powi(2))
.sum::<f64>()
/ n;
variance.sqrt() / mean_ns
} else {
0.0
}
}
SampleReduction::Mean => {
sorted.iter().map(|&v| v as f64).sum::<f64>() / sorted.len() as f64 / 1000.0
}
SampleReduction::Worst => *sorted.last().expect("non-empty by caller") as f64 / 1000.0,
}
}
pub(crate) fn weighted_percentile(sorted: &[(u64, f64)], p: f64) -> u64 {
if sorted.is_empty() {
return 0;
}
debug_assert!(
sorted.windows(2).all(|w| w[0].0 <= w[1].0),
"weighted_percentile() requires value-sorted input",
);
let total: f64 = sorted.iter().map(|&(_, w)| w).sum();
let target = (total * p).ceil().max(1.0);
let mut cum = 0.0;
for &(v, w) in sorted {
cum += w;
if cum >= target {
return v;
}
}
sorted.last().map(|&(v, _)| v).unwrap_or(0)
}
pub(crate) fn reduce_weighted_sorted_distribution(
sorted: &[(u64, f64)],
reduction: crate::stats::SampleReduction,
) -> f64 {
use crate::stats::SampleReduction;
match reduction {
SampleReduction::P99 => weighted_percentile(sorted, 0.99) as f64 / 1000.0,
SampleReduction::P999 => weighted_percentile(sorted, 0.999) as f64 / 1000.0,
SampleReduction::Median => weighted_percentile(sorted, 0.5) as f64 / 1000.0,
SampleReduction::Cv => {
let total_w: f64 = sorted.iter().map(|&(_, w)| w).sum();
if total_w <= 0.0 {
return 0.0;
}
let mean_ns = sorted.iter().map(|&(v, w)| v as f64 * w).sum::<f64>() / total_w;
if mean_ns > 0.0 {
let variance = sorted
.iter()
.map(|&(v, w)| w * (v as f64 - mean_ns).powi(2))
.sum::<f64>()
/ total_w;
variance.sqrt() / mean_ns
} else {
0.0
}
}
SampleReduction::Mean => {
let total_w: f64 = sorted.iter().map(|&(_, w)| w).sum();
if total_w <= 0.0 {
return 0.0;
}
sorted.iter().map(|&(v, w)| v as f64 * w).sum::<f64>() / total_w / 1000.0
}
SampleReduction::Worst => sorted.last().map(|&(v, _)| v).unwrap_or(0) as f64 / 1000.0,
}
}
fn distribution_cgroup_reduction(
cg: &CgroupStats,
source: crate::stats::SampleSource,
reduction: crate::stats::SampleReduction,
) -> f64 {
use crate::stats::{SampleReduction, SampleSource};
match source {
SampleSource::WakeLatencyNs => match reduction {
SampleReduction::P99 => cg.p99_wake_latency_us,
SampleReduction::Median => cg.median_wake_latency_us,
SampleReduction::Cv => cg.wake_latency_cv,
SampleReduction::P999 | SampleReduction::Mean | SampleReduction::Worst => {
debug_assert!(false, "no CgroupStats wake reduction for {reduction:?}");
f64::NAN
}
},
SampleSource::RunDelayNs => match reduction {
SampleReduction::Mean => cg.mean_run_delay_us,
SampleReduction::Worst => cg.worst_run_delay_us,
SampleReduction::P99
| SampleReduction::P999
| SampleReduction::Median
| SampleReduction::Cv => {
debug_assert!(
false,
"no CgroupStats run-delay reduction for {reduction:?}"
);
f64::NAN
}
},
SampleSource::TimerLatencyNs => match reduction {
SampleReduction::Median => cg.median_timer_latency_us,
SampleReduction::P99 => cg.p99_timer_latency_us,
SampleReduction::P999 => cg.p999_timer_latency_us,
SampleReduction::Worst => cg.worst_timer_latency_us,
SampleReduction::Cv | SampleReduction::Mean => {
debug_assert!(false, "no CgroupStats timer reduction for {reduction:?}");
f64::NAN
}
},
}
}
pub fn populate_run_ext_metrics(
samples: &crate::scenario::sample::SampleSeries,
target: &mut std::collections::BTreeMap<String, f64>,
) {
for metric_def in crate::stats::METRICS {
if target.contains_key(metric_def.name) {
continue;
}
if TYPED_FIELD_NAMES.contains(&metric_def.name) {
continue;
}
let readings: Vec<f64> = samples
.iter_samples()
.filter_map(|s| metric_def.read_sample(&s))
.collect();
if readings.is_empty() {
continue;
}
if let Some(reduced) = crate::stats::aggregate_samples_for_phase(metric_def, &readings) {
target.insert(metric_def.name.to_string(), reduced);
}
}
if target.contains_key("total_hardirqs") {
let irq_elapsed_ms: Vec<u64> = samples
.iter_samples()
.filter(|s| !s.snapshot.per_cpu_time().is_empty())
.filter_map(|s| s.elapsed_ms)
.collect();
if let (Some(first), Some(last)) = (
irq_elapsed_ms.iter().min().copied(),
irq_elapsed_ms.iter().max().copied(),
) && last > first
{
let wall_ms = (last - first) as f64;
target
.entry("total_phase_wall_ns".to_string())
.or_insert(wall_ms * 1_000_000.0);
target
.entry("total_phase_wall_sec".to_string())
.or_insert(wall_ms / 1000.0);
}
}
crate::stats::derive_rate_metrics(target);
}
#[allow(clippy::doc_lazy_continuation)]
pub(crate) fn derive_phase_metrics(phases: &mut [PhaseBucket]) {
use crate::workload::schbench::run::SchbenchPhaseStats;
for bucket in phases.iter_mut() {
let mut pooled: Option<SchbenchPhaseStats> = None;
let mut pooled_taobench: Option<crate::workload::taobench::run::TaobenchPhaseStats> = None;
for pc in bucket.per_cgroup.values_mut() {
write_carrier_scalars(pc);
if let Some(s) = pc.schbench.as_ref() {
write_schbench_scalars(s, &mut pc.metrics);
match pooled.take() {
Some(mut acc) => {
acc.merge(s);
pooled = Some(acc);
}
None => pooled = Some(s.clone()),
}
}
if let Some(t) = pc.taobench.as_ref() {
write_taobench_scalars(t, &mut pc.metrics);
match pooled_taobench.take() {
Some(mut acc) => {
acc.merge(t);
pooled_taobench = Some(acc);
}
None => pooled_taobench = Some(t.clone()),
}
}
}
if let Some(p) = pooled {
write_schbench_scalars(&p, &mut bucket.metrics);
}
if let Some(t) = pooled_taobench {
write_taobench_scalars(&t, &mut bucket.metrics);
}
}
}
fn write_carrier_scalars(pc: &mut PhaseCgroupStats) {
use crate::assert::reductions::{
cross_node_migration_ratio_of, iterations_per_cpu_sec_of, iterations_per_worker_of,
migration_ratio_of, page_locality_of,
};
let wake = pc.wake_summary().zip(pc.wake_cv());
let timer = pc.timer_summary();
let run_delay = pc.run_delay_summary();
let off_cpu = pc.off_cpu_summary();
let migration_ratio = migration_ratio_of(pc.total_migrations, pc.total_iterations);
let ipw = iterations_per_worker_of(pc.num_workers, pc.total_iterations);
let ipcs = iterations_per_cpu_sec_of(pc.num_workers, pc.total_cpu_time_ns, pc.total_iterations);
let numa = if pc.numa_pages_total > 0 {
Some((
page_locality_of(pc.numa_pages_local, pc.numa_pages_total),
cross_node_migration_ratio_of(pc.cross_node_migrated, pc.numa_pages_total),
))
} else {
None
};
let m = &mut pc.metrics;
if let Some(((p99, median), cv)) = wake {
m.insert("p99_wake_latency_us".to_string(), p99);
m.insert("median_wake_latency_us".to_string(), median);
m.insert("wake_latency_cv".to_string(), cv);
}
if let Some((median, p99, p999)) = timer {
m.insert("median_timer_latency_us".to_string(), median);
m.insert("p99_timer_latency_us".to_string(), p99);
m.insert("p999_timer_latency_us".to_string(), p999);
}
if let Some((mean, worst)) = run_delay {
m.insert("mean_run_delay_us".to_string(), mean);
m.insert("max_run_delay_us".to_string(), worst);
}
if let Some((avg, min, max, spread)) = off_cpu {
m.insert("avg_off_cpu_pct".to_string(), avg);
m.insert("min_off_cpu_pct".to_string(), min);
m.insert("max_off_cpu_pct".to_string(), max);
m.insert("off_cpu_spread_pct".to_string(), spread);
}
m.insert("migration_ratio".to_string(), migration_ratio);
if let Some(v) = ipw {
m.insert("iterations_per_worker".to_string(), v);
}
if let Some(v) = ipcs {
m.insert("iterations_per_cpu_sec".to_string(), v);
}
if let Some((locality, cross_node)) = numa {
m.insert("page_locality".to_string(), locality);
m.insert("cross_node_migration_ratio".to_string(), cross_node);
}
}
fn write_schbench_scalars(
p: &crate::workload::schbench::run::SchbenchPhaseStats,
out: &mut std::collections::BTreeMap<String, f64>,
) {
use crate::stats::{
SCHBENCH_LOOP_COUNT, SCHBENCH_REQUEST_MAX_US, SCHBENCH_REQUEST_MIN_US,
SCHBENCH_REQUEST_P50_US, SCHBENCH_REQUEST_P90_US, SCHBENCH_REQUEST_P99_US,
SCHBENCH_REQUEST_P999_US, SCHBENCH_RPS_MAX, SCHBENCH_RPS_MIN, SCHBENCH_RPS_P20,
SCHBENCH_RPS_P50, SCHBENCH_RPS_P90, SCHBENCH_SCHED_DELAY_MSG_US,
SCHBENCH_SCHED_DELAY_WORKER_US, SCHBENCH_WAKEUP_MAX_US, SCHBENCH_WAKEUP_MIN_US,
SCHBENCH_WAKEUP_P50_US, SCHBENCH_WAKEUP_P90_US, SCHBENCH_WAKEUP_P99_US,
SCHBENCH_WAKEUP_P999_US,
};
use crate::workload::schbench::plat::Pct;
if p.wakeup.sample_count() > 0 {
let q = p.wakeup.percentiles();
out.insert(
SCHBENCH_WAKEUP_P50_US.to_string(),
q.value_at(Pct::P50) as f64,
);
out.insert(
SCHBENCH_WAKEUP_P90_US.to_string(),
q.value_at(Pct::P90) as f64,
);
out.insert(
SCHBENCH_WAKEUP_P99_US.to_string(),
q.value_at(Pct::P99) as f64,
);
out.insert(
SCHBENCH_WAKEUP_P999_US.to_string(),
q.value_at(Pct::P999) as f64,
);
out.insert(SCHBENCH_WAKEUP_MIN_US.to_string(), q.min as f64);
out.insert(SCHBENCH_WAKEUP_MAX_US.to_string(), q.max as f64);
}
if p.request.sample_count() > 0 {
let q = p.request.percentiles();
out.insert(
SCHBENCH_REQUEST_P50_US.to_string(),
q.value_at(Pct::P50) as f64,
);
out.insert(
SCHBENCH_REQUEST_P90_US.to_string(),
q.value_at(Pct::P90) as f64,
);
out.insert(
SCHBENCH_REQUEST_P99_US.to_string(),
q.value_at(Pct::P99) as f64,
);
out.insert(
SCHBENCH_REQUEST_P999_US.to_string(),
q.value_at(Pct::P999) as f64,
);
out.insert(SCHBENCH_REQUEST_MIN_US.to_string(), q.min as f64);
out.insert(SCHBENCH_REQUEST_MAX_US.to_string(), q.max as f64);
}
if p.rps.sample_count() > 0 {
let r = p.rps.percentiles();
out.insert(SCHBENCH_RPS_P20.to_string(), r.value_at(Pct::P20) as f64);
out.insert(SCHBENCH_RPS_P50.to_string(), r.value_at(Pct::P50) as f64);
out.insert(SCHBENCH_RPS_P90.to_string(), r.value_at(Pct::P90) as f64);
out.insert(SCHBENCH_RPS_MIN.to_string(), r.min as f64);
out.insert(SCHBENCH_RPS_MAX.to_string(), r.max as f64);
}
if p.msg_pcount > 0 {
let mean_us = p.msg_run_delay_ns as f64 / p.msg_pcount as f64 / 1000.0;
out.insert(SCHBENCH_SCHED_DELAY_MSG_US.to_string(), mean_us);
}
if p.worker_pcount > 0 {
let mean_us = p.worker_run_delay_ns as f64 / p.worker_pcount as f64 / 1000.0;
out.insert(SCHBENCH_SCHED_DELAY_WORKER_US.to_string(), mean_us);
}
out.insert(SCHBENCH_LOOP_COUNT.to_string(), p.loop_count as f64);
}
fn write_taobench_scalars(
p: &crate::workload::taobench::run::TaobenchPhaseStats,
out: &mut std::collections::BTreeMap<String, f64>,
) {
use crate::stats::{
TAOBENCH_FAST_QPS, TAOBENCH_HIT_RATE, TAOBENCH_HIT_RATIO, TAOBENCH_SLOW_QPS,
TAOBENCH_TOTAL_QPS,
};
let c = &p.counters;
let total = c.total_ops();
if c.elapsed_ns > 0 {
let secs = c.elapsed_ns as f64 / 1e9;
out.insert(TAOBENCH_TOTAL_QPS.to_string(), total as f64 / secs);
out.insert(TAOBENCH_FAST_QPS.to_string(), c.fast_ops as f64 / secs);
out.insert(TAOBENCH_SLOW_QPS.to_string(), c.slow_ops as f64 / secs);
}
if total > 0 {
out.insert(
TAOBENCH_HIT_RATIO.to_string(),
c.fast_ops as f64 / total as f64,
);
}
if c.get_cmds > 0 {
out.insert(
TAOBENCH_HIT_RATE.to_string(),
1.0 - (c.get_misses as f64 / c.get_cmds as f64),
);
}
write_taobench_serve_scalars(&p.serve_lat, out);
}
fn write_taobench_serve_scalars(
serve: &crate::workload::schbench::plat::PlatStats,
out: &mut std::collections::BTreeMap<String, f64>,
) {
use crate::stats::{
TAOBENCH_SERVE_MAX_US, TAOBENCH_SERVE_MIN_US, TAOBENCH_SERVE_P50_US, TAOBENCH_SERVE_P90_US,
TAOBENCH_SERVE_P99_US, TAOBENCH_SERVE_P999_US,
};
use crate::workload::schbench::plat::Pct;
if serve.sample_count() == 0 {
return;
}
let q = serve.percentiles();
out.insert(
TAOBENCH_SERVE_P50_US.to_string(),
q.value_at(Pct::P50) as f64,
);
out.insert(
TAOBENCH_SERVE_P90_US.to_string(),
q.value_at(Pct::P90) as f64,
);
out.insert(
TAOBENCH_SERVE_P99_US.to_string(),
q.value_at(Pct::P99) as f64,
);
out.insert(
TAOBENCH_SERVE_P999_US.to_string(),
q.value_at(Pct::P999) as f64,
);
out.insert(TAOBENCH_SERVE_MIN_US.to_string(), q.min as f64);
out.insert(TAOBENCH_SERVE_MAX_US.to_string(), q.max as f64);
}