use std::collections::BTreeMap;
use crate::ctprof::{CgroupStats, CtprofSnapshot};
use super::{
Aggregated, CTPROF_DERIVED_METRICS, CTPROF_METRICS, CompareOptions, CtprofDiff,
DerivedMetricDef, DerivedRow, DiffRow, FudgedPair, GroupBy, SortKey, ThreadGroup,
aggregate::merge_aggregated_into,
cgroup_merge::{merge_cgroup_cpu, merge_cgroup_memory, merge_cgroup_pids, merge_psi},
format_value_cell,
groups::{
build_cgroup_key_map, build_groups, build_row, collect_smaps_rollup,
collect_smaps_rollup_hierarchical, compile_flatten_patterns, flatten_cgroup_path,
},
pattern::{pattern_counts_union, pattern_display_label, pattern_key},
};
pub(super) fn sort_diff_rows_by_keys(
rows: &mut [DiffRow],
derived_rows: &mut [DerivedRow],
sort_keys: &[SortKey],
) {
debug_assert!(
!sort_keys.is_empty(),
"sort_diff_rows_by_keys called with empty sort_keys; \
caller must short-circuit before invoking the multi-key \
sort path",
);
use std::collections::{BTreeMap, BTreeSet};
let metric_idx: BTreeMap<&'static str, usize> = CTPROF_METRICS
.iter()
.enumerate()
.map(|(i, m)| (m.name, i))
.collect();
let derived_idx: BTreeMap<&'static str, usize> = CTPROF_DERIVED_METRICS
.iter()
.enumerate()
.map(|(i, m)| (m.name, i))
.collect();
let mut group_metrics: BTreeMap<String, BTreeMap<&'static str, f64>> = BTreeMap::new();
for row in rows.iter() {
if let Some(d) = row.delta {
group_metrics
.entry(row.group_key.clone())
.or_default()
.insert(row.metric_name, d);
}
}
for row in derived_rows.iter() {
if let Some(d) = row.delta {
group_metrics
.entry(row.group_key.clone())
.or_default()
.insert(row.metric_name, d);
}
}
let mut unique_groups: BTreeSet<String> = group_metrics.keys().cloned().collect();
for row in rows.iter() {
unique_groups.insert(row.group_key.clone());
}
for row in derived_rows.iter() {
unique_groups.insert(row.group_key.clone());
}
let mut groups_with_tuples: Vec<(String, Vec<f64>)> = unique_groups
.into_iter()
.map(|g| {
let metrics = group_metrics.get(&g);
let tuple: Vec<f64> = sort_keys
.iter()
.map(|k| {
metrics
.and_then(|m| m.get(k.metric).copied())
.unwrap_or(if k.descending {
f64::NEG_INFINITY
} else {
f64::INFINITY
})
})
.collect();
(g, tuple)
})
.collect();
groups_with_tuples.sort_by(|(ga, ta), (gb, tb)| {
for (i, key) in sort_keys.iter().enumerate() {
let (va, vb) = (ta[i], tb[i]);
let ord = if key.descending {
vb.partial_cmp(&va).unwrap_or(std::cmp::Ordering::Equal)
} else {
va.partial_cmp(&vb).unwrap_or(std::cmp::Ordering::Equal)
};
if ord != std::cmp::Ordering::Equal {
return ord;
}
}
ga.cmp(gb)
});
let group_ranks: BTreeMap<String, usize> = groups_with_tuples
.into_iter()
.enumerate()
.map(|(i, (g, _))| (g, i))
.collect();
rows.sort_by(|a, b| {
let ra = group_ranks.get(&a.group_key).copied().unwrap_or(usize::MAX);
let rb = group_ranks.get(&b.group_key).copied().unwrap_or(usize::MAX);
ra.cmp(&rb).then_with(|| {
let ia = metric_idx.get(a.metric_name).copied().unwrap_or(usize::MAX);
let ib = metric_idx.get(b.metric_name).copied().unwrap_or(usize::MAX);
ia.cmp(&ib)
})
});
derived_rows.sort_by(|a, b| {
let ra = group_ranks.get(&a.group_key).copied().unwrap_or(usize::MAX);
let rb = group_ranks.get(&b.group_key).copied().unwrap_or(usize::MAX);
ra.cmp(&rb).then_with(|| {
let ia = derived_idx
.get(a.metric_name)
.copied()
.unwrap_or(usize::MAX);
let ib = derived_idx
.get(b.metric_name)
.copied()
.unwrap_or(usize::MAX);
ia.cmp(&ib)
})
});
}
pub(super) fn build_derived_row(
key: &str,
display_key: &str,
n_a: usize,
n_b: usize,
def: &DerivedMetricDef,
metrics_a: &BTreeMap<String, Aggregated>,
metrics_b: &BTreeMap<String, Aggregated>,
) -> DerivedRow {
let baseline = (def.compute)(metrics_a);
let candidate = (def.compute)(metrics_b);
let (delta, delta_pct) = match (baseline, candidate) {
(Some(a), Some(b)) => {
let va = a.as_f64();
let vb = b.as_f64();
let d = vb - va;
let pct = if def.is_ratio {
None
} else if va.abs() > f64::EPSILON {
Some(d / va)
} else {
None
};
(Some(d), pct)
}
_ => (None, None),
};
DerivedRow {
group_key: key.to_string(),
display_key: display_key.to_string(),
thread_count_a: n_a,
thread_count_b: n_b,
metric_name: def.name,
metric_ladder: def.ladder,
is_ratio: def.is_ratio,
baseline,
candidate,
delta,
delta_pct,
sort_by_cell: None,
sort_by_delta: None,
}
}
pub(super) fn emit_fudged_rows(
diff: &mut CtprofDiff,
matches: &BTreeMap<String, Vec<String>>,
groups_a: &BTreeMap<String, ThreadGroup>,
groups_b: &BTreeMap<String, ThreadGroup>,
) {
for (bkey, ckeys) in matches {
let Some(ga) = groups_a.get(bkey) else {
continue;
};
let mut merged_metrics: BTreeMap<String, Aggregated> = BTreeMap::new();
let mut merged_thread_count: usize = 0;
for ckey in ckeys {
let Some(gb) = groups_b.get(ckey) else {
continue;
};
merged_thread_count += gb.thread_count;
for (name, val) in &gb.metrics {
let entry = merged_metrics.entry(name.clone());
match entry {
std::collections::btree_map::Entry::Vacant(e) => {
e.insert(val.clone());
}
std::collections::btree_map::Entry::Occupied(mut e) => {
let existing = e.get_mut();
merge_aggregated_into(existing, val);
}
}
}
}
let bcg = bkey.split_once('\x00').map_or(bkey.as_str(), |(cg, _)| cg);
let leaf = bcg.rsplit_once('/').map_or(bcg, |(_, l)| l);
let display_key = if leaf.is_empty() {
"[fudged]".to_string()
} else {
format!("[fudged: {leaf}]")
};
for metric in CTPROF_METRICS {
let Some(a) = ga.metrics.get(metric.name).cloned() else {
continue;
};
let Some(b) = merged_metrics.get(metric.name).cloned() else {
continue;
};
diff.rows.push(build_row(
bkey,
&display_key,
ga.thread_count,
merged_thread_count,
metric,
a,
b,
None,
));
}
for def in CTPROF_DERIVED_METRICS {
diff.derived_rows.push(build_derived_row(
bkey,
&display_key,
ga.thread_count,
merged_thread_count,
def,
&ga.metrics,
&merged_metrics,
));
}
}
}
pub fn compare(
baseline: &CtprofSnapshot,
candidate: &CtprofSnapshot,
opts: &CompareOptions,
) -> CtprofDiff {
let flatten = compile_flatten_patterns(&opts.cgroup_flatten);
let group_by = opts.group_by.0;
let pattern_counts: Option<BTreeMap<String, usize>> = match (group_by, opts.no_thread_normalize)
{
(GroupBy::Comm, false) => Some(pattern_counts_union(baseline, candidate, |t| {
t.comm.as_str()
})),
(GroupBy::Pcomm, false) => Some(pattern_counts_union(baseline, candidate, |t| {
t.pcomm.as_str()
})),
_ => None,
};
let cgroup_key_map: Option<BTreeMap<String, String>> =
if matches!(group_by, GroupBy::Cgroup | GroupBy::All) && !opts.no_cg_normalize {
Some(build_cgroup_key_map(baseline, candidate, &flatten))
} else {
None
};
let groups_a = build_groups(
baseline,
group_by,
&flatten,
pattern_counts.as_ref(),
cgroup_key_map.as_ref(),
opts.no_thread_normalize,
);
let groups_b = build_groups(
candidate,
group_by,
&flatten,
pattern_counts.as_ref(),
cgroup_key_map.as_ref(),
opts.no_thread_normalize,
);
let mut diff = CtprofDiff::default();
let now_b = candidate
.threads
.iter()
.map(|t| t.start_time_clock_ticks)
.max()
.unwrap_or(0);
for (key, group_a) in &groups_a {
let Some(group_b) = groups_b.get(key) else {
diff.only_baseline.push(key.clone());
continue;
};
let pattern_axis_active =
matches!(group_by, GroupBy::Comm | GroupBy::Pcomm) && !opts.no_thread_normalize;
let display_key = if pattern_axis_active {
let mut union: Vec<String> = group_a.members.clone();
union.extend(group_b.members.iter().cloned());
union.sort();
union.dedup();
pattern_display_label(key, &union)
} else {
key.clone()
};
for metric in CTPROF_METRICS {
let Some(a) = group_a.metrics.get(metric.name).cloned() else {
continue;
};
let Some(b) = group_b.metrics.get(metric.name).cloned() else {
continue;
};
diff.rows.push(build_row(
key,
&display_key,
group_a.thread_count,
group_b.thread_count,
metric,
a,
b,
None, ));
}
for def in CTPROF_DERIVED_METRICS {
diff.derived_rows.push(build_derived_row(
key,
&display_key,
group_a.thread_count,
group_b.thread_count,
def,
&group_a.metrics,
&group_b.metrics,
));
}
}
for key in groups_b.keys() {
if !groups_a.contains_key(key) {
diff.only_candidate.push(key.clone());
}
}
let mut fudged_key_pairs: Vec<(String, String)> = Vec::new();
if group_by == GroupBy::All && !diff.only_baseline.is_empty() && !diff.only_candidate.is_empty()
{
fn cg_prefix(key: &str) -> &str {
key.split_once('\x00').map_or(key, |(cg, _)| cg)
}
type TypeSet = std::collections::BTreeSet<(String, String)>;
let mut cg_types_a: BTreeMap<String, TypeSet> = BTreeMap::new();
let mut cg_types_b: BTreeMap<String, TypeSet> = BTreeMap::new();
let matched_prefixes: std::collections::BTreeSet<String> = groups_a
.keys()
.filter(|k| groups_b.contains_key(*k))
.map(|k| cg_prefix(k).to_string())
.collect();
let mut cg_prefixes_a: std::collections::BTreeSet<String> =
std::collections::BTreeSet::new();
let mut cg_prefixes_b: std::collections::BTreeSet<String> =
std::collections::BTreeSet::new();
for key in &diff.only_baseline {
let pfx = cg_prefix(key).to_string();
if !matched_prefixes.contains(&pfx) {
cg_prefixes_a.insert(pfx);
}
}
for key in &diff.only_candidate {
let pfx = cg_prefix(key).to_string();
if !matched_prefixes.contains(&pfx) {
cg_prefixes_b.insert(pfx);
}
}
for t in &baseline.threads {
let cg = flatten_cgroup_path(&t.cgroup, &flatten);
let cg_key = match cgroup_key_map.as_ref().and_then(|m| m.get(&cg)) {
Some(k) => k.clone(),
None => cg,
};
if cg_prefixes_a.contains(&cg_key) {
cg_types_a
.entry(cg_key)
.or_default()
.insert((pattern_key(&t.pcomm), pattern_key(&t.comm)));
}
}
for t in &candidate.threads {
let cg = flatten_cgroup_path(&t.cgroup, &flatten);
let cg_key = match cgroup_key_map.as_ref().and_then(|m| m.get(&cg)) {
Some(k) => k.clone(),
None => cg,
};
if cg_prefixes_b.contains(&cg_key) {
cg_types_b
.entry(cg_key)
.or_default()
.insert((pattern_key(&t.pcomm), pattern_key(&t.comm)));
}
}
let mut fudged_cg: Vec<(String, String)> = Vec::new();
for ccg in &cg_prefixes_b {
let Some(set_b) = cg_types_b.get(ccg) else {
continue;
};
if set_b.len() < 10 {
continue;
}
let mut best: Option<(&str, f64, usize)> = None;
for bcg in &cg_prefixes_a {
let Some(set_a) = cg_types_a.get(bcg) else {
continue;
};
let intersection = set_a.intersection(set_b).count();
if intersection < 10 {
continue;
}
let union = set_a.union(set_b).count();
let jaccard = intersection as f64 / union as f64;
if jaccard >= 0.90 && best.is_none_or(|(_, bj, _)| jaccard > bj) {
best = Some((bcg.as_str(), jaccard, intersection));
}
}
if let Some((bcg, _jaccard, _overlap)) = best {
fudged_cg.push((bcg.to_string(), ccg.clone()));
}
}
let mut remove_baseline: std::collections::BTreeSet<String> =
std::collections::BTreeSet::new();
let mut remove_candidate: std::collections::BTreeSet<String> =
std::collections::BTreeSet::new();
let mut fudge_matches: BTreeMap<String, Vec<String>> = BTreeMap::new(); for (bcg, ccg) in &fudged_cg {
let b_keys: Vec<&String> = diff
.only_baseline
.iter()
.filter(|k| cg_prefix(k) == bcg.as_str())
.collect();
let c_keys: Vec<&String> = diff
.only_candidate
.iter()
.filter(|k| cg_prefix(k) == ccg.as_str())
.collect();
let c_suffix_map: BTreeMap<&str, &String> = c_keys
.iter()
.map(|k| {
let suffix = k.split_once('\x00').map_or("", |(_, s)| s);
(suffix, *k)
})
.collect();
for bkey in &b_keys {
let b_suffix = bkey.split_once('\x00').map_or("", |(_, s)| s);
if let Some(ckey) = c_suffix_map.get(b_suffix) {
remove_baseline.insert((*bkey).clone());
remove_candidate.insert((*ckey).clone());
fudged_key_pairs.push(((*bkey).clone(), (*ckey).clone()));
fudge_matches
.entry((*bkey).clone())
.or_default()
.push((*ckey).clone());
}
}
}
emit_fudged_rows(&mut diff, &fudge_matches, &groups_a, &groups_b);
let mut cascade_counts: BTreeMap<String, usize> = BTreeMap::new();
let mut cascade_roots: BTreeMap<(String, String), (String, String)> = BTreeMap::new();
let mut cascade_matches: BTreeMap<String, Vec<String>> = BTreeMap::new();
for (bcg, ccg) in &fudged_cg {
let b_segs: Vec<&str> = bcg.split('/').collect();
let c_segs: Vec<&str> = ccg.split('/').collect();
let common_suffix_len = b_segs
.iter()
.rev()
.zip(c_segs.iter().rev())
.take_while(|(a, b)| a == b)
.count();
let b_root: String = b_segs[..b_segs.len().saturating_sub(common_suffix_len)].join("/");
let c_root: String = c_segs[..c_segs.len().saturating_sub(common_suffix_len)].join("/");
let b_root = if b_root.is_empty() {
bcg.clone()
} else {
b_root
};
let c_root = if c_root.is_empty() {
ccg.clone()
} else {
c_root
};
cascade_roots.insert((bcg.clone(), ccg.clone()), (b_root.clone(), c_root.clone()));
let is_root_or_child = |cg: &str, root: &str| {
let Some(tail) = cg.strip_prefix(root) else {
return false;
};
tail.is_empty() || tail.starts_with('/')
};
let remaining_b: Vec<String> = diff
.only_baseline
.iter()
.filter(|k| {
!remove_baseline.contains(*k) && is_root_or_child(cg_prefix(k), b_root.as_str())
})
.cloned()
.collect();
let remaining_c: Vec<String> = diff
.only_candidate
.iter()
.filter(|k| {
!remove_candidate.contains(*k)
&& is_root_or_child(cg_prefix(k), c_root.as_str())
})
.cloned()
.collect();
let c_by_suffix: BTreeMap<String, &String> = remaining_c
.iter()
.filter_map(|k| {
let child_cg = cg_prefix(k);
let tail = &child_cg[c_root.len()..];
if !tail.is_empty() && !tail.starts_with('/') {
return None;
}
let rewritten = format!("{b_root}{tail}");
let suffix = k.split_once('\x00').map_or("", |(_, s)| s);
Some((format!("{rewritten}\x00{suffix}"), k))
})
.collect();
for bkey in &remaining_b {
if let Some(ckey) = c_by_suffix.get(bkey) {
remove_baseline.insert(bkey.clone());
remove_candidate.insert((*ckey).clone());
fudged_key_pairs.push((bkey.clone(), (*ckey).clone()));
*cascade_counts.entry(bcg.clone()).or_insert(0) += 1;
cascade_matches
.entry(bkey.clone())
.or_default()
.push((*ckey).clone());
}
}
}
emit_fudged_rows(&mut diff, &cascade_matches, &groups_a, &groups_b);
diff.only_baseline.retain(|k| !remove_baseline.contains(k));
diff.only_candidate
.retain(|k| !remove_candidate.contains(k));
let mut union_b_for_bcg: BTreeMap<String, TypeSet> = BTreeMap::new();
let mut union_a_for_ccg: BTreeMap<String, TypeSet> = BTreeMap::new();
for (bcg, ccg) in &fudged_cg {
if let Some(sb) = cg_types_b.get(ccg) {
union_b_for_bcg
.entry(bcg.clone())
.or_default()
.extend(sb.iter().cloned());
}
if let Some(sa) = cg_types_a.get(bcg) {
union_a_for_ccg
.entry(ccg.clone())
.or_default()
.extend(sa.iter().cloned());
}
}
diff.fudged_pairs = fudged_cg
.iter()
.map(|(bcg, ccg)| {
let set_a = cg_types_a.get(bcg).cloned().unwrap_or_default();
let set_b = cg_types_b.get(ccg).cloned().unwrap_or_default();
let union_b = union_b_for_bcg.get(bcg).cloned().unwrap_or_default();
let union_a = union_a_for_ccg.get(ccg).cloned().unwrap_or_default();
let residual_a: Vec<String> = set_a
.difference(&union_b)
.map(|(p, c)| format!("{p}:{c}"))
.collect();
let residual_b: Vec<String> = set_b
.difference(&union_a)
.map(|(p, c)| format!("{p}:{c}"))
.collect();
let intersection = set_a.intersection(&set_b).count();
let union = set_a.union(&set_b).count();
FudgedPair {
baseline_cgroup: bcg.clone(),
candidate_cgroup: ccg.clone(),
overlap: intersection,
jaccard: if union > 0 {
intersection as f64 / union as f64
} else {
0.0
},
baseline_residual: residual_a,
candidate_residual: residual_b,
cascaded_children: cascade_counts.get(bcg).copied().unwrap_or(0),
baseline_root: cascade_roots
.get(&(bcg.clone(), ccg.clone()))
.map(|(b, _)| b.clone())
.unwrap_or_else(|| bcg.clone()),
candidate_root: cascade_roots
.get(&(bcg.clone(), ccg.clone()))
.map(|(_, c)| c.clone())
.unwrap_or_else(|| ccg.clone()),
}
})
.collect();
}
diff.only_baseline.sort();
diff.only_candidate.sort();
{
let mut group_lifetime: BTreeMap<String, u64> = BTreeMap::new();
for (key, group_b) in &groups_b {
if groups_a.contains_key(key) {
group_lifetime.insert(key.clone(), now_b.saturating_sub(group_b.avg_start_ticks));
}
}
let mut fudge_lt_sum: BTreeMap<String, (u64, u64)> = BTreeMap::new();
for (bkey, ckey) in &fudged_key_pairs {
if let Some(gb) = groups_b.get(ckey) {
let lt = now_b.saturating_sub(gb.avg_start_ticks);
let entry = fudge_lt_sum.entry(bkey.clone()).or_insert((0, 0));
entry.0 += lt;
entry.1 += 1;
}
}
for (bkey, (sum, count)) in &fudge_lt_sum {
if *count > 0 {
group_lifetime.insert(bkey.clone(), sum / count);
}
}
let max_lifetime = group_lifetime.values().copied().max().unwrap_or(1).max(1);
for row in &mut diff.rows {
if let Some(<) = group_lifetime.get(&row.group_key) {
row.uptime_pct = Some(lt as f64 / max_lifetime as f64 * 100.0);
}
}
}
if opts.sort_by.is_empty() {
diff.rows.sort_by(|a, b| {
b.sort_key()
.partial_cmp(&a.sort_key())
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| a.group_key.cmp(&b.group_key))
});
diff.derived_rows.sort_by(|a, b| {
b.sort_key()
.partial_cmp(&a.sort_key())
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| a.group_key.cmp(&b.group_key))
});
} else {
sort_diff_rows_by_keys(&mut diff.rows, &mut diff.derived_rows, &opts.sort_by);
let sort_metric = opts.sort_by.first().map(|sk| sk.metric);
diff.sort_metric_name = sort_metric;
if let Some(metric_name) = sort_metric {
let mut group_cells: BTreeMap<String, (String, Option<f64>)> = BTreeMap::new();
for row in &diff.rows {
if row.metric_name == metric_name && !group_cells.contains_key(&row.group_key) {
let b = format_value_cell(&row.baseline, row.metric_ladder);
let c = format_value_cell(&row.candidate, row.metric_ladder);
let pct = match row.delta_pct {
Some(p) => format!(" ({:+.1}%)", p * 100.0),
None => String::new(),
};
group_cells.insert(
row.group_key.clone(),
(format!("{b}\u{2192}{c}{pct}"), row.delta),
);
}
}
for row in &mut diff.rows {
if let Some((cell, delta)) = group_cells.get(&row.group_key) {
row.sort_by_cell = Some(cell.clone());
row.sort_by_delta = *delta;
}
}
for row in &mut diff.derived_rows {
if let Some((cell, delta)) = group_cells.get(&row.group_key) {
row.sort_by_cell = Some(cell.clone());
row.sort_by_delta = *delta;
}
}
}
}
if group_by == GroupBy::Cgroup {
diff.cgroup_stats_a =
flatten_cgroup_stats(&baseline.cgroup_stats, &flatten, cgroup_key_map.as_ref());
diff.cgroup_stats_b =
flatten_cgroup_stats(&candidate.cgroup_stats, &flatten, cgroup_key_map.as_ref());
}
diff.host_psi_a = baseline.psi;
diff.host_psi_b = candidate.psi;
if group_by == GroupBy::All {
diff.smaps_rollup_a = collect_smaps_rollup_hierarchical(
baseline,
opts.no_thread_normalize,
&flatten,
cgroup_key_map.as_ref(),
);
diff.smaps_rollup_b = collect_smaps_rollup_hierarchical(
candidate,
opts.no_thread_normalize,
&flatten,
cgroup_key_map.as_ref(),
);
} else {
diff.smaps_rollup_a = collect_smaps_rollup(baseline, opts.no_thread_normalize);
diff.smaps_rollup_b = collect_smaps_rollup(candidate, opts.no_thread_normalize);
}
{
let fudged_cg_set: std::collections::BTreeSet<&str> = fudged_key_pairs
.iter()
.map(|(_, ckey)| ckey.split_once('\x00').map_or(ckey.as_str(), |(cg, _)| cg))
.collect();
let mut sorted_pairs: Vec<&FudgedPair> = diff.fudged_pairs.iter().collect();
sorted_pairs.sort_by(|a, b| b.candidate_root.len().cmp(&a.candidate_root.len()));
for fp in sorted_pairs {
let br = &fp.baseline_root;
let cr = &fp.candidate_root;
let cr_slash = format!("{cr}/");
let cr_nul = format!("{cr}\x00");
let mut summed_by_rel: BTreeMap<String, BTreeMap<String, u64>> = BTreeMap::new();
let keys: Vec<String> = diff
.smaps_rollup_b
.keys()
.filter(|k| {
let in_root = k.starts_with(&cr_slash) || k.starts_with(&cr_nul);
if !in_root {
return false;
}
let cg_path = k.split_once('\x00').map_or(k.as_str(), |(cg, _)| cg);
fudged_cg_set.contains(cg_path)
})
.cloned()
.collect();
for k in keys {
if let Some(val) = diff.smaps_rollup_b.remove(&k) {
let (cg_path, pcomm) = k.split_once('\x00').unwrap_or((&k, ""));
let child = if cg_path == cr.as_str() {
""
} else if let Some(rest) = cg_path.strip_prefix(&cr_slash) {
rest
} else {
continue;
};
let rel_key = format!("{child}\x00{pcomm}");
let entry = summed_by_rel.entry(rel_key).or_default();
for (field, v) in &val {
let slot = entry.entry(field.clone()).or_insert(0);
*slot = slot.saturating_add(*v);
}
}
}
for (rel_key, summed) in summed_by_rel {
let (child, pcomm) = rel_key.split_once('\x00').unwrap_or((&rel_key, ""));
let base_key = if child.is_empty() {
format!("{br}\x00{pcomm}")
} else {
format!("{br}/{child}\x00{pcomm}")
};
diff.smaps_rollup_b.insert(base_key, summed);
}
}
}
diff.sched_ext_a = baseline.sched_ext.clone();
diff.sched_ext_b = candidate.sched_ext.clone();
diff
}
pub fn flatten_cgroup_stats(
stats: &BTreeMap<String, CgroupStats>,
patterns: &[glob::Pattern],
cgroup_key_map: Option<&BTreeMap<String, String>>,
) -> BTreeMap<String, CgroupStats> {
let mut out: BTreeMap<String, CgroupStats> = BTreeMap::new();
for (path, cs) in stats {
let post_flatten = flatten_cgroup_path(path, patterns);
let key = match cgroup_key_map.and_then(|m| m.get(&post_flatten)) {
Some(k) => k.clone(),
None => post_flatten,
};
match out.get_mut(&key) {
None => {
out.insert(key, cs.clone());
}
Some(agg) => {
merge_cgroup_cpu(&mut agg.cpu, &cs.cpu);
merge_cgroup_memory(&mut agg.memory, &cs.memory);
merge_cgroup_pids(&mut agg.pids, &cs.pids);
agg.psi = merge_psi(agg.psi, cs.psi);
}
}
}
out
}