use hotpath::json::{
format_bytes_signed, parse_bytes_signed, JsonFunctionEntry, JsonFunctionsList, JsonReport,
JsonThreadEntry, JsonThreadsList,
};
use hotpath::{
format_bytes, format_count, parse_bytes, parse_count, parse_duration, shorten_function_name,
};
use prettytable::{color, Attr, Cell, Row, Table};
use std::fmt;
use std::time::Duration;
#[derive(Debug, Clone)]
pub enum MetricDiff {
CallsCount(u64, u64), DurationNs(u64, u64), AllocBytes(u64, u64), AllocCount(u64, u64), AllocSigned(i64, i64), Percentage(u64, u64), }
impl fmt::Display for MetricDiff {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.format_with_emoji(None))
}
}
impl MetricDiff {
pub fn format_with_emoji(&self, emoji_threshold: Option<u32>) -> String {
match self {
MetricDiff::CallsCount(before, after) => {
let diff_percent = calculate_percentage_diff(*before, *after);
let emoji = get_emoji_for_diff(diff_percent, emoji_threshold);
format!("{} → {} ({:+.1}%){}", before, after, diff_percent, emoji)
}
MetricDiff::DurationNs(before, after) => {
let diff_percent = calculate_percentage_diff(*before, *after);
let before_duration = Duration::from_nanos(*before);
let after_duration = Duration::from_nanos(*after);
let emoji = get_emoji_for_diff(diff_percent, emoji_threshold);
format!(
"{:.2?} → {:.2?} ({:+.1}%){}",
before_duration, after_duration, diff_percent, emoji
)
}
MetricDiff::AllocBytes(before, after) => {
let diff_percent = calculate_percentage_diff(*before, *after);
let emoji = get_emoji_for_diff(diff_percent, emoji_threshold);
format!(
"{} → {} ({:+.1}%){}",
format_bytes(*before),
format_bytes(*after),
diff_percent,
emoji
)
}
MetricDiff::AllocCount(before, after) => {
let diff_percent = calculate_percentage_diff(*before, *after);
let emoji = get_emoji_for_diff(diff_percent, emoji_threshold);
format!(
"{} → {} ({:+.1}%){}",
format_count(*before),
format_count(*after),
diff_percent,
emoji
)
}
MetricDiff::AllocSigned(before, after) => {
let diff_percent = calculate_percentage_diff_signed(*before, *after);
let emoji = get_emoji_for_diff(diff_percent, emoji_threshold);
format!(
"{} → {} ({:+.1}%){}",
format_bytes_signed(*before),
format_bytes_signed(*after),
diff_percent,
emoji
)
}
MetricDiff::Percentage(before, after) => {
let diff_percent = calculate_percentage_diff(*before, *after);
let before_percent = *before as f64 / 100.0;
let after_percent = *after as f64 / 100.0;
let emoji = get_emoji_for_diff(diff_percent, emoji_threshold);
format!(
"{:.2}% → {:.2}% ({:+.1}%){}",
before_percent, after_percent, diff_percent, emoji
)
}
}
}
}
fn get_emoji_for_diff(diff_percent: f64, threshold: Option<u32>) -> &'static str {
if let Some(threshold_val) = threshold {
let threshold = threshold_val as f64;
if diff_percent > threshold {
" ⚠️ "
} else if diff_percent < -threshold {
" 🚀 "
} else {
" "
}
} else {
""
}
}
#[derive(Debug, Clone)]
pub struct FunctionsComparison {
pub profiling_mode: hotpath::ProfilingMode,
pub description: String,
pub percentiles: Vec<f64>,
pub function_diffs: Vec<FunctionMetricsDiff>,
}
#[derive(Debug, Clone)]
pub struct FunctionMetricsDiff {
pub function_name: String,
pub metrics: Vec<MetricDiff>,
pub is_removed: bool,
pub is_new: bool,
}
#[derive(Debug, Clone)]
pub struct ThreadMetricsDiff {
pub thread_name: String,
pub cpu_percent_avg: Option<MetricDiff>,
pub cpu_percent_max: Option<MetricDiff>,
pub alloc_bytes: Option<MetricDiff>,
pub dealloc_bytes: Option<MetricDiff>,
pub mem_diff: Option<MetricDiff>,
pub is_removed: bool,
pub is_new: bool,
}
#[derive(Debug, Clone)]
pub struct ThreadsComparison {
pub total_alloc_diff: Option<MetricDiff>,
pub total_dealloc_diff: Option<MetricDiff>,
pub total_mem_diff_diff: Option<MetricDiff>,
pub thread_diffs: Vec<ThreadMetricsDiff>,
}
#[derive(Debug, Clone)]
pub struct JsonReportDiff {
pub before_label: Option<String>,
pub after_label: Option<String>,
pub total_elapsed_diff: MetricDiff,
pub cpu_baseline_diff: Option<MetricDiff>,
pub functions_timing: Option<FunctionsComparison>,
pub functions_alloc: Option<FunctionsComparison>,
pub threads: Option<ThreadsComparison>,
}
fn ensure_matching_profiling_mode(
section_name: &str,
before_metrics: &JsonFunctionsList,
after_metrics: &JsonFunctionsList,
) -> Result<(), String> {
if before_metrics.profiling_mode != after_metrics.profiling_mode {
return Err(format!(
"Profiling mode mismatch in {} section: before={}, after={}",
section_name, before_metrics.profiling_mode, after_metrics.profiling_mode
));
}
Ok(())
}
pub fn compare_reports(before: &JsonReport, after: &JsonReport) -> Result<JsonReportDiff, String> {
if let (Some(b), Some(a)) = (&before.functions_timing, &after.functions_timing) {
ensure_matching_profiling_mode("functions_timing", b, a)?;
}
if let (Some(b), Some(a)) = (&before.functions_alloc, &after.functions_alloc) {
ensure_matching_profiling_mode("functions_alloc", b, a)?;
}
let functions_timing = match (&before.functions_timing, &after.functions_timing) {
(Some(b), Some(a)) => Some(compare_metrics(b, a)),
_ => None,
};
let functions_alloc = match (&before.functions_alloc, &after.functions_alloc) {
(Some(b), Some(a)) => Some(compare_metrics(b, a)),
_ => None,
};
let (before_section, after_section) = before
.functions_timing
.as_ref()
.zip(after.functions_timing.as_ref())
.or_else(|| {
before
.functions_alloc
.as_ref()
.zip(after.functions_alloc.as_ref())
})
.unzip();
let before_ns = before_section.map(|s| s.total_elapsed_ns).unwrap_or(0);
let after_ns = after_section.map(|s| s.total_elapsed_ns).unwrap_or(0);
let cpu_baseline_diff = match (&before.cpu_baseline, &after.cpu_baseline) {
(Some(b), Some(a)) => {
let b_ns = parse_duration(&b.avg).unwrap_or(0);
let a_ns = parse_duration(&a.avg).unwrap_or(0);
Some(MetricDiff::DurationNs(b_ns, a_ns))
}
_ => None,
};
let threads = match (&before.threads, &after.threads) {
(Some(b), Some(a)) => Some(compare_threads(b, a)),
_ => None,
};
Ok(JsonReportDiff {
before_label: before.label.clone(),
after_label: after.label.clone(),
total_elapsed_diff: MetricDiff::DurationNs(before_ns, after_ns),
cpu_baseline_diff,
functions_timing,
functions_alloc,
threads,
})
}
fn calculate_percentage_diff(before: u64, after: u64) -> f64 {
if before == 0 {
if after == 0 {
0.0
} else {
100.0
}
} else {
((after as f64 - before as f64) / before as f64) * 100.0
}
}
fn calculate_percentage_diff_signed(before: i64, after: i64) -> f64 {
if before == 0 {
if after == 0 {
0.0
} else {
100.0
}
} else {
((after as f64 - before as f64) / (before as f64).abs()) * 100.0
}
}
fn find_function<'a>(data: &'a [JsonFunctionEntry], name: &str) -> Option<&'a JsonFunctionEntry> {
data.iter().find(|f| f.name == name)
}
fn parse_metric(s: &str, mode: &hotpath::ProfilingMode) -> Option<u64> {
use hotpath::ProfilingMode;
match mode {
ProfilingMode::AllocCount => parse_count(s),
ProfilingMode::AllocBytes => parse_bytes(s),
ProfilingMode::Timing => parse_duration(s),
}
}
fn parse_percent(s: &str) -> Option<u64> {
let s = s.trim().trim_end_matches('%').trim();
let pct: f64 = s.parse().ok()?;
Some((pct * 100.0).round() as u64)
}
#[derive(Debug, Clone, Copy)]
enum MetricKind {
Calls,
Duration,
Alloc,
AllocCount,
Percentage,
}
fn build_metrics_from_function(
func: &JsonFunctionEntry,
percentiles: &[f64],
mode: &hotpath::ProfilingMode,
) -> Vec<(MetricKind, u64)> {
use hotpath::ProfilingMode;
let mut metrics = Vec::new();
let kind = match mode {
ProfilingMode::AllocCount => MetricKind::AllocCount,
ProfilingMode::AllocBytes => MetricKind::Alloc,
ProfilingMode::Timing => MetricKind::Duration,
};
metrics.push((MetricKind::Calls, func.calls));
if let Some(val) = parse_metric(&func.avg, mode) {
metrics.push((kind, val));
}
for p in percentiles {
let key = hotpath::format_percentile_key(*p);
if let Some(formatted) = func.percentiles.get(&key) {
if let Some(val) = parse_metric(formatted, mode) {
metrics.push((kind, val));
}
}
}
if let Some(val) = parse_metric(&func.total, mode) {
metrics.push((kind, val));
}
if let Some(bp) = parse_percent(&func.percent_total) {
metrics.push((MetricKind::Percentage, bp));
}
metrics
}
pub fn compare_metrics(
before_metrics: &JsonFunctionsList,
after_metrics: &JsonFunctionsList,
) -> FunctionsComparison {
let mode = &before_metrics.profiling_mode;
let mut function_diffs = Vec::new();
let mut new_functions = Vec::new();
let make_diff = |kind: &MetricKind, before_val: u64, after_val: u64| -> MetricDiff {
match kind {
MetricKind::Calls => MetricDiff::CallsCount(before_val, after_val),
MetricKind::Duration => MetricDiff::DurationNs(before_val, after_val),
MetricKind::Alloc => MetricDiff::AllocBytes(before_val, after_val),
MetricKind::AllocCount => MetricDiff::AllocCount(before_val, after_val),
MetricKind::Percentage => MetricDiff::Percentage(before_val, after_val),
}
};
for after_func in &after_metrics.data {
if let Some(before_func) = find_function(&before_metrics.data, &after_func.name) {
let before_vals =
build_metrics_from_function(before_func, &before_metrics.percentiles, mode);
let after_vals =
build_metrics_from_function(after_func, &after_metrics.percentiles, mode);
let metrics = before_vals
.iter()
.zip(after_vals.iter())
.map(|((kind, before_val), (_, after_val))| {
make_diff(kind, *before_val, *after_val)
})
.collect();
function_diffs.push(FunctionMetricsDiff {
function_name: after_func.name.clone(),
metrics,
is_removed: false,
is_new: false,
});
} else {
let after_vals =
build_metrics_from_function(after_func, &after_metrics.percentiles, mode);
let metrics = after_vals
.iter()
.map(|(kind, after_val)| make_diff(kind, 0, *after_val))
.collect();
new_functions.push(FunctionMetricsDiff {
function_name: after_func.name.clone(),
metrics,
is_removed: false,
is_new: true,
});
}
}
for before_func in &before_metrics.data {
if find_function(&after_metrics.data, &before_func.name).is_none() {
let before_vals =
build_metrics_from_function(before_func, &before_metrics.percentiles, mode);
let metrics = before_vals
.iter()
.map(|(kind, before_val)| make_diff(kind, *before_val, 0))
.collect();
function_diffs.push(FunctionMetricsDiff {
function_name: before_func.name.clone(),
metrics,
is_removed: true,
is_new: false,
});
}
}
function_diffs.extend(new_functions);
function_diffs.sort_by(|a, b| {
let a_percent = a
.metrics
.iter()
.find_map(|m| {
if let MetricDiff::Percentage(_, after) = m {
Some(*after)
} else {
None
}
})
.unwrap_or(0);
let b_percent = b
.metrics
.iter()
.find_map(|m| {
if let MetricDiff::Percentage(_, after) = m {
Some(*after)
} else {
None
}
})
.unwrap_or(0);
b_percent.cmp(&a_percent)
});
FunctionsComparison {
profiling_mode: before_metrics.profiling_mode,
description: before_metrics.description.clone(),
percentiles: before_metrics.percentiles.clone(),
function_diffs,
}
}
fn parse_cpu_percent(s: &str) -> Option<u64> {
let s = s.trim().trim_end_matches('%').trim();
let pct: f64 = s.parse().ok()?;
Some((pct * 100.0).round() as u64)
}
fn find_thread<'a>(data: &'a [JsonThreadEntry], name: &str) -> Option<&'a JsonThreadEntry> {
data.iter().find(|t| t.name == name)
}
fn make_percent_diff(before: &Option<String>, after: &Option<String>) -> Option<MetricDiff> {
let b = parse_cpu_percent(before.as_deref()?)?;
let a = parse_cpu_percent(after.as_deref()?)?;
Some(MetricDiff::Percentage(b, a))
}
fn make_alloc_diff(before: &Option<String>, after: &Option<String>) -> Option<MetricDiff> {
let b = parse_bytes(before.as_deref()?)?;
let a = parse_bytes(after.as_deref()?)?;
Some(MetricDiff::AllocBytes(b, a))
}
fn make_alloc_signed_diff(before: &Option<String>, after: &Option<String>) -> Option<MetricDiff> {
let b = parse_bytes_signed(before.as_deref()?)?;
let a = parse_bytes_signed(after.as_deref()?)?;
Some(MetricDiff::AllocSigned(b, a))
}
fn build_thread_diff(
before: &JsonThreadEntry,
after: &JsonThreadEntry,
is_new: bool,
is_removed: bool,
) -> ThreadMetricsDiff {
ThreadMetricsDiff {
thread_name: if is_new {
after.name.clone()
} else {
before.name.clone()
},
cpu_percent_avg: make_percent_diff(&before.cpu_percent_avg, &after.cpu_percent_avg),
cpu_percent_max: make_percent_diff(&before.cpu_percent_max, &after.cpu_percent_max),
alloc_bytes: make_alloc_diff(&before.alloc_bytes, &after.alloc_bytes),
dealloc_bytes: make_alloc_diff(&before.dealloc_bytes, &after.dealloc_bytes),
mem_diff: make_alloc_signed_diff(&before.mem_diff, &after.mem_diff),
is_removed,
is_new,
}
}
pub fn compare_threads(
before_threads: &JsonThreadsList,
after_threads: &JsonThreadsList,
) -> ThreadsComparison {
let mut thread_diffs = Vec::new();
let mut new_threads = Vec::new();
let zero = JsonThreadEntry {
os_tid: 0,
name: String::new(),
status: String::new(),
status_code: String::new(),
cpu_percent: None,
cpu_percent_max: Some("0.0%".to_string()),
cpu_percent_avg: Some("0.0%".to_string()),
alloc_bytes: Some("0 B".to_string()),
dealloc_bytes: Some("0 B".to_string()),
mem_diff: Some("0 B".to_string()),
};
let duplicate_names = {
let mut dups = std::collections::HashSet::new();
for list in [&before_threads.data, &after_threads.data] {
let mut counts = std::collections::HashMap::<&str, usize>::new();
for t in list {
*counts.entry(&t.name).or_default() += 1;
}
for (name, count) in counts {
if count > 1 {
dups.insert(name.to_string());
}
}
}
dups
};
let before_data: Vec<_> = before_threads
.data
.iter()
.filter(|t| !duplicate_names.contains(&t.name))
.collect();
let after_data: Vec<_> = after_threads
.data
.iter()
.filter(|t| !duplicate_names.contains(&t.name))
.collect();
for after_thread in &after_data {
if let Some(before_thread) = find_thread(&before_threads.data, &after_thread.name) {
thread_diffs.push(build_thread_diff(before_thread, after_thread, false, false));
} else {
new_threads.push(build_thread_diff(&zero, after_thread, true, false));
}
}
for before_thread in &before_data {
if find_thread(&after_threads.data, &before_thread.name).is_none() {
thread_diffs.push(build_thread_diff(before_thread, &zero, false, true));
}
}
thread_diffs.extend(new_threads);
let total_alloc_diff = make_alloc_diff(
&before_threads.total_alloc_bytes,
&after_threads.total_alloc_bytes,
);
let total_dealloc_diff = make_alloc_diff(
&before_threads.total_dealloc_bytes,
&after_threads.total_dealloc_bytes,
);
let total_mem_diff_diff = make_alloc_signed_diff(
&before_threads.alloc_dealloc_diff,
&after_threads.alloc_dealloc_diff,
);
ThreadsComparison {
total_alloc_diff,
total_dealloc_diff,
total_mem_diff_diff,
thread_diffs,
}
}
fn styled_header(text: &str, use_colors: bool) -> Cell {
if use_colors {
Cell::new(text)
.with_style(Attr::Bold)
.with_style(Attr::ForegroundColor(color::CYAN))
} else {
Cell::new(text).with_style(Attr::Bold)
}
}
pub fn build_functions_table(
comparison: &FunctionsComparison,
emoji_threshold: Option<u32>,
use_colors: bool,
) -> Table {
let mut table = Table::new();
let mut header_cells = vec![
styled_header("Function", use_colors),
styled_header("Calls", use_colors),
styled_header("Avg", use_colors),
];
for &p in &comparison.percentiles {
header_cells.push(styled_header(
&hotpath::format_percentile_header(p),
use_colors,
));
}
header_cells.push(styled_header("Total", use_colors));
header_cells.push(styled_header("% Total", use_colors));
table.add_row(Row::new(header_cells));
for func_diff in &comparison.function_diffs {
let short_name = shorten_function_name(&func_diff.function_name);
let function_display = if func_diff.is_removed {
format!("🗑️ {}", short_name)
} else if func_diff.is_new {
format!("🆕 {}", short_name)
} else {
short_name
};
let mut row_cells = vec![Cell::new(&function_display)];
for metric_diff in &func_diff.metrics {
row_cells.push(Cell::new(&metric_diff.format_with_emoji(emoji_threshold)));
}
table.add_row(Row::new(row_cells));
}
table
}
pub fn build_threads_table(
threads: &ThreadsComparison,
emoji_threshold: Option<u32>,
use_colors: bool,
) -> Table {
let fmt = |m: &Option<MetricDiff>| {
m.as_ref()
.map(|d| d.format_with_emoji(emoji_threshold))
.unwrap_or_default()
};
let has_alloc = threads
.thread_diffs
.iter()
.any(|d| d.alloc_bytes.is_some() || d.dealloc_bytes.is_some() || d.mem_diff.is_some());
let mut table = Table::new();
let mut header = vec![
styled_header("Thread", use_colors),
styled_header("CPU % Avg", use_colors),
styled_header("CPU % Max", use_colors),
];
if has_alloc {
header.extend([
styled_header("Alloc", use_colors),
styled_header("Dealloc", use_colors),
styled_header("Mem Diff", use_colors),
]);
}
table.add_row(Row::new(header));
for diff in &threads.thread_diffs {
let name = if diff.is_removed {
format!("🗑️ {}", diff.thread_name)
} else if diff.is_new {
format!("🆕 {}", diff.thread_name)
} else {
diff.thread_name.clone()
};
let mut row = vec![
Cell::new(&name),
Cell::new(&fmt(&diff.cpu_percent_avg)),
Cell::new(&fmt(&diff.cpu_percent_max)),
];
if has_alloc {
row.extend([
Cell::new(&fmt(&diff.alloc_bytes)),
Cell::new(&fmt(&diff.dealloc_bytes)),
Cell::new(&fmt(&diff.mem_diff)),
]);
}
table.add_row(Row::new(row));
}
table
}
pub fn format_threads_globals(
threads: &ThreadsComparison,
emoji_threshold: Option<u32>,
) -> Vec<String> {
let fmt = |label: &str, m: &Option<MetricDiff>| {
m.as_ref()
.map(|d| format!("{}: {}", label, d.format_with_emoji(emoji_threshold)))
};
[
fmt("Total Alloc", &threads.total_alloc_diff),
fmt("Total Dealloc", &threads.total_dealloc_diff),
fmt("Mem Diff", &threads.total_mem_diff_diff),
]
.into_iter()
.flatten()
.collect()
}
#[cfg(test)]
mod test {
use crate::cmd::shared::{compare_metrics, compare_reports, compare_threads};
use hotpath::json::{
JsonFunctionEntry, JsonFunctionsList, JsonReport, JsonThreadEntry, JsonThreadsList,
};
use std::collections::HashMap;
fn make_function_data(
name: &str,
calls: u64,
avg: u64,
p95: u64,
total: u64,
percent: u64,
) -> JsonFunctionEntry {
let mut percentiles = HashMap::new();
percentiles.insert("p95".to_string(), hotpath::format_duration(p95));
JsonFunctionEntry {
id: 0,
name: name.to_string(),
calls,
avg: hotpath::format_duration(avg),
percentiles,
total: hotpath::format_duration(total),
percent_total: format!("{:.2}%", percent as f64 / 100.0),
}
}
fn make_metrics(data: Vec<JsonFunctionEntry>, total_elapsed_ns: u64) -> JsonFunctionsList {
let count = data.len();
JsonFunctionsList {
profiling_mode: hotpath::ProfilingMode::Timing,
time_elapsed: hotpath::format_duration(total_elapsed_ns),
total_elapsed_ns,
total_allocated: None,
description: "Time metrics".to_string(),
caller_name: "test::main".to_string(),
percentiles: vec![95],
displayed_count: count,
total_count: count,
data,
}
}
fn make_alloc_function_data(
name: &str,
calls: u64,
avg_bytes: u64,
p95_bytes: u64,
total_bytes: u64,
percent: u64,
) -> JsonFunctionEntry {
let mut percentiles = HashMap::new();
percentiles.insert("p95".to_string(), hotpath::format_bytes(p95_bytes));
JsonFunctionEntry {
id: 0,
name: name.to_string(),
calls,
avg: hotpath::format_bytes(avg_bytes),
percentiles,
total: hotpath::format_bytes(total_bytes),
percent_total: format!("{:.2}%", percent as f64 / 100.0),
}
}
fn make_alloc_metrics(
data: Vec<JsonFunctionEntry>,
total_elapsed_ns: u64,
) -> JsonFunctionsList {
let count = data.len();
JsonFunctionsList {
profiling_mode: hotpath::ProfilingMode::AllocBytes,
time_elapsed: hotpath::format_duration(total_elapsed_ns),
total_elapsed_ns,
total_allocated: Some("10.00 MB".to_string()),
description: "Alloc metrics".to_string(),
caller_name: "test::main".to_string(),
percentiles: vec![95],
displayed_count: count,
total_count: count,
data,
}
}
fn make_report(
timing: Option<JsonFunctionsList>,
alloc: Option<JsonFunctionsList>,
) -> JsonReport {
JsonReport {
functions_timing: timing,
functions_alloc: alloc,
..Default::default()
}
}
#[test]
fn test_compare_metrics_new_removed_unchanged() {
let after_data = vec![
make_function_data("test::function_a", 100, 1000000, 1100000, 100000000, 7000),
make_function_data("test::function_c", 40, 400000, 450000, 16000000, 1500),
];
let after_metrics = make_metrics(after_data, 140000000);
let before_data = vec![
make_function_data("test::function_a", 90, 900000, 1000000, 81000000, 8000),
make_function_data("test::function_b", 30, 300000, 350000, 9000000, 1200),
];
let before_metrics = make_metrics(before_data, 120000000);
let comparison = compare_metrics(&before_metrics, &after_metrics);
assert_eq!(comparison.function_diffs.len(), 3);
assert!(comparison
.function_diffs
.iter()
.any(|f| f.function_name == "test::function_b" && f.is_removed));
assert!(comparison
.function_diffs
.iter()
.any(|f| f.function_name == "test::function_c" && f.is_new));
assert!(comparison
.function_diffs
.iter()
.any(|f| f.function_name == "test::function_a" && !f.is_new && !f.is_removed));
}
#[test]
fn test_compare_reports_timing_and_alloc() {
let before_timing = make_metrics(
vec![make_function_data(
"fn_a", 100, 1000000, 1100000, 100000000, 10000,
)],
100000000,
);
let after_timing = make_metrics(
vec![make_function_data(
"fn_a", 120, 1200000, 1300000, 144000000, 10000,
)],
144000000,
);
let before_alloc = make_alloc_metrics(
vec![make_alloc_function_data(
"fn_a", 100, 1024, 2048, 102400, 10000,
)],
100000000,
);
let after_alloc = make_alloc_metrics(
vec![make_alloc_function_data(
"fn_a", 120, 2048, 4096, 245760, 10000,
)],
144000000,
);
let before = make_report(Some(before_timing), Some(before_alloc));
let after = make_report(Some(after_timing), Some(after_alloc));
let diff = compare_reports(&before, &after).expect("reports should compare");
assert!(diff.functions_timing.is_some());
assert!(diff.functions_alloc.is_some());
let timing = diff.functions_timing.unwrap();
assert_eq!(timing.function_diffs.len(), 1);
assert_eq!(timing.function_diffs[0].function_name, "fn_a");
let alloc = diff.functions_alloc.unwrap();
assert_eq!(alloc.function_diffs.len(), 1);
assert_eq!(alloc.function_diffs[0].function_name, "fn_a");
}
fn make_thread_entry(os_tid: u64, name: &str, cpu_percent_max: f64) -> JsonThreadEntry {
JsonThreadEntry {
os_tid,
name: name.to_string(),
status: "Running".to_string(),
status_code: "1".to_string(),
cpu_percent: None,
cpu_percent_max: Some(format!("{:.1}%", cpu_percent_max)),
cpu_percent_avg: None,
alloc_bytes: None,
dealloc_bytes: None,
mem_diff: None,
}
}
fn make_threads_list(data: Vec<JsonThreadEntry>) -> JsonThreadsList {
let thread_count = data.len();
JsonThreadsList {
current_elapsed_ns: 1_000_000_000,
sample_interval_ms: 1000,
thread_count,
rss_bytes: None,
total_alloc_bytes: None,
total_dealloc_bytes: None,
alloc_dealloc_diff: None,
data,
}
}
#[test]
fn test_compare_threads_new_removed_unchanged() {
let before = make_threads_list(vec![
make_thread_entry(100, "main", 30.0),
make_thread_entry(101, "worker-1", 17.5),
]);
let after = make_threads_list(vec![
make_thread_entry(200, "main", 42.5),
make_thread_entry(201, "worker-2", 11.5),
]);
let comparison = compare_threads(&before, &after);
assert_eq!(comparison.thread_diffs.len(), 3);
assert!(comparison
.thread_diffs
.iter()
.any(|t| t.thread_name == "main" && !t.is_new && !t.is_removed));
assert!(comparison
.thread_diffs
.iter()
.any(|t| t.thread_name == "worker-1" && t.is_removed));
assert!(comparison
.thread_diffs
.iter()
.any(|t| t.thread_name == "worker-2" && t.is_new));
assert!(comparison.total_alloc_diff.is_none());
assert!(comparison.total_dealloc_diff.is_none());
assert!(comparison.total_mem_diff_diff.is_none());
}
#[test]
fn test_compare_threads_global_alloc_diffs() {
let mut before = make_threads_list(vec![make_thread_entry(100, "main", 30.0)]);
before.total_alloc_bytes = Some("1.00 MB".to_string());
before.total_dealloc_bytes = Some("512.00 KB".to_string());
before.alloc_dealloc_diff = Some("512.00 KB".to_string());
let mut after = make_threads_list(vec![make_thread_entry(200, "main", 42.5)]);
after.total_alloc_bytes = Some("2.00 MB".to_string());
after.total_dealloc_bytes = Some("1.00 MB".to_string());
after.alloc_dealloc_diff = Some("1.00 MB".to_string());
let comparison = compare_threads(&before, &after);
assert!(comparison.total_alloc_diff.is_some());
assert!(comparison.total_dealloc_diff.is_some());
assert!(comparison.total_mem_diff_diff.is_some());
}
#[test]
fn test_compare_threads_duplicate_names_skipped() {
let before = make_threads_list(vec![
make_thread_entry(100, "main", 30.0),
make_thread_entry(101, "worker", 10.0),
make_thread_entry(102, "worker", 20.0),
]);
let after = make_threads_list(vec![
make_thread_entry(200, "main", 42.5),
make_thread_entry(201, "worker", 15.0),
make_thread_entry(202, "worker", 25.0),
]);
let comparison = compare_threads(&before, &after);
assert_eq!(comparison.thread_diffs.len(), 1);
assert!(comparison
.thread_diffs
.iter()
.any(|t| t.thread_name == "main" && !t.is_new && !t.is_removed));
assert!(!comparison
.thread_diffs
.iter()
.any(|t| t.thread_name == "worker"));
}
}