use std::collections::HashMap;
use std::fmt;
#[derive(Clone, Debug, Default)]
pub struct Report {
operations: HashMap<String, ReportOperation>,
}
#[derive(Clone, Debug)]
#[expect(
clippy::struct_field_names,
reason = "field names are descriptive and clear"
)]
pub struct ReportOperation {
total_bytes_allocated: u64,
total_allocations_count: u64,
total_iterations: u64,
}
impl Report {
#[cfg(test)]
#[must_use]
pub(crate) fn new() -> Self {
Self {
operations: HashMap::new(),
}
}
#[must_use]
pub(crate) fn from_operation_data(
operation_data: &HashMap<String, crate::operation_metrics::OperationMetrics>,
) -> Self {
let report_operations = operation_data
.iter()
.map(|(name, op_data)| {
(
name.clone(),
ReportOperation {
total_bytes_allocated: op_data.total_bytes_allocated,
total_allocations_count: op_data.total_allocations_count,
total_iterations: op_data.total_iterations,
},
)
})
.collect();
Self {
operations: report_operations,
}
}
#[must_use]
pub fn merge(a: &Self, b: &Self) -> Self {
let mut merged_operations = a.operations.clone();
for (name, b_op) in &b.operations {
merged_operations
.entry(name.clone())
.and_modify(|a_op| {
a_op.total_bytes_allocated = a_op
.total_bytes_allocated
.checked_add(b_op.total_bytes_allocated)
.expect("merging bytes allocated overflows u64 - this indicates an unrealistic scenario");
a_op.total_allocations_count = a_op
.total_allocations_count
.checked_add(b_op.total_allocations_count)
.expect("merging allocations count overflows u64 - this indicates an unrealistic scenario");
a_op.total_iterations = a_op
.total_iterations
.checked_add(b_op.total_iterations)
.expect("merging iteration counts overflows u64 - this indicates an unrealistic scenario");
})
.or_insert_with(|| b_op.clone());
}
Self {
operations: merged_operations,
}
}
#[cfg_attr(test, mutants::skip)] pub fn print_to_stdout(&self) {
if self.is_empty() {
return;
}
println!("{self}");
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.operations.is_empty() || self.operations.values().all(|op| op.total_iterations == 0)
}
pub fn operations(&self) -> impl Iterator<Item = (&str, &ReportOperation)> {
self.operations.iter().map(|(name, op)| (name.as_str(), op))
}
}
impl ReportOperation {
#[must_use]
pub fn total_bytes_allocated(&self) -> u64 {
self.total_bytes_allocated
}
#[must_use]
pub fn total_allocations_count(&self) -> u64 {
self.total_allocations_count
}
#[must_use]
pub fn total_iterations(&self) -> u64 {
self.total_iterations
}
#[expect(
clippy::integer_division,
reason = "we accept loss of precision for mean calculation"
)]
#[expect(
clippy::arithmetic_side_effects,
reason = "division by zero is guarded by if-else"
)]
#[must_use]
pub fn mean_bytes(&self) -> u64 {
if self.total_iterations == 0 {
0
} else {
self.total_bytes_allocated / self.total_iterations
}
}
#[expect(
clippy::integer_division,
reason = "we accept loss of precision for mean calculation"
)]
#[expect(
clippy::arithmetic_side_effects,
reason = "division by zero is guarded by if-else"
)]
#[must_use]
pub fn mean_allocations(&self) -> u64 {
if self.total_iterations == 0 {
0
} else {
self.total_allocations_count / self.total_iterations
}
}
#[must_use]
pub fn mean(&self) -> u64 {
self.mean_bytes()
}
}
impl fmt::Display for ReportOperation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} bytes (mean)", self.mean_bytes())
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
impl fmt::Display for Report {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.operations.values().all(|op| op.total_iterations == 0) {
writeln!(f, "No allocation statistics captured.")?;
} else {
writeln!(f, "Allocation statistics:")?;
writeln!(f)?;
let mut sorted_ops: Vec<_> = self.operations.iter().collect();
sorted_ops.sort_by_key(|(name, _)| *name);
let max_name_width = sorted_ops
.iter()
.map(|(name, _)| name.len())
.max()
.unwrap_or(0)
.max("Operation".len());
let max_bytes_width = sorted_ops
.iter()
.map(|(_, operation)| operation.mean_bytes().to_string().len())
.max()
.unwrap_or(0)
.max("Mean bytes".len());
let max_count_width = sorted_ops
.iter()
.map(|(_, operation)| operation.mean_allocations().to_string().len())
.max()
.unwrap_or(0)
.max("Mean count".len());
writeln!(
f,
"| {:<name_width$} | {:>bytes_width$} | {:>count_width$} |",
"Operation",
"Mean bytes",
"Mean count",
name_width = max_name_width,
bytes_width = max_bytes_width,
count_width = max_count_width
)?;
let separator_name_width = max_name_width
.checked_add(2)
.expect("operation name width fits in memory, adding 2 cannot overflow");
let separator_bytes_width = max_bytes_width
.checked_add(2)
.expect("bytes width fits in memory, adding 2 cannot overflow");
let separator_count_width = max_count_width
.checked_add(2)
.expect("count width fits in memory, adding 2 cannot overflow");
writeln!(
f,
"|{:-<name_width$}|{:-<bytes_width$}|{:-<count_width$}|",
"",
"",
"",
name_width = separator_name_width,
bytes_width = separator_bytes_width,
count_width = separator_count_width
)?;
for (name, operation) in sorted_ops {
writeln!(
f,
"| {:<name_width$} | {:>bytes_width$} | {:>count_width$} |",
name,
operation.mean_bytes(),
operation.mean_allocations(),
name_width = max_name_width,
bytes_width = max_bytes_width,
count_width = max_count_width
)?;
}
}
Ok(())
}
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use std::panic::RefUnwindSafe;
use std::panic::UnwindSafe;
use super::*;
use crate::Session;
use crate::allocator::register_fake_allocation;
#[test]
fn new_report_is_empty() {
let report = Report::new();
assert!(report.is_empty());
}
#[test]
fn report_from_empty_session_is_empty() {
let session = Session::new();
let report = session.to_report();
assert!(report.is_empty());
}
#[test]
fn report_from_session_with_operations_is_not_empty() {
let session = Session::new();
{
let operation = session.operation("test");
let _span = operation.measure_thread();
register_fake_allocation(100, 1);
}
let report = session.to_report();
assert!(!report.is_empty());
}
#[test]
fn merge_empty_reports() {
let report1 = Report::new();
let report2 = Report::new();
let merged = Report::merge(&report1, &report2);
assert!(merged.is_empty());
}
#[test]
fn merge_empty_with_non_empty() {
let session = Session::new();
{
let operation = session.operation("test");
let _span = operation.measure_thread();
register_fake_allocation(100, 1);
}
let report1 = Report::new();
let report2 = session.to_report();
let merged1 = Report::merge(&report1, &report2);
let merged2 = Report::merge(&report2, &report1);
assert!(!merged1.is_empty());
assert!(!merged2.is_empty());
}
#[test]
fn merge_different_operations() {
let session1 = Session::new();
let session2 = Session::new();
{
let op1 = session1.operation("test1");
let _span1 = op1.measure_thread();
register_fake_allocation(100, 1);
}
{
let op2 = session2.operation("test2");
let _span2 = op2.measure_thread();
register_fake_allocation(200, 2);
}
let report1 = session1.to_report();
let report2 = session2.to_report();
let merged = Report::merge(&report1, &report2);
assert_eq!(merged.operations.len(), 2);
assert!(merged.operations.contains_key("test1"));
assert!(merged.operations.contains_key("test2"));
}
#[test]
fn merge_same_operations() {
let session1 = Session::new();
let session2 = Session::new();
{
let op1 = session1.operation("test");
let _span1 = op1.measure_thread();
register_fake_allocation(100, 1);
}
{
let op2 = session2.operation("test");
let _span2 = op2.measure_thread();
register_fake_allocation(200, 2);
}
let report1 = session1.to_report();
let report2 = session2.to_report();
let merged = Report::merge(&report1, &report2);
assert_eq!(merged.operations.len(), 1);
let merged_op = merged.operations.get("test").unwrap();
assert_eq!(merged_op.total_iterations, 2); assert_eq!(merged_op.total_bytes_allocated, 300); assert_eq!(merged_op.total_allocations_count, 3); }
#[test]
fn report_clone() {
let session = Session::new();
{
let operation = session.operation("test");
let _span = operation.measure_thread();
register_fake_allocation(100, 1);
}
let report1 = session.to_report();
let report2 = report1.clone();
assert_eq!(report1.operations.len(), report2.operations.len());
}
#[test]
fn report_operation_total_allocations_count_zero() {
let operation = ReportOperation {
total_bytes_allocated: 0,
total_allocations_count: 0,
total_iterations: 1,
};
assert_eq!(operation.total_allocations_count(), 0);
}
#[test]
fn report_operation_total_allocations_count_multiple() {
let operation = ReportOperation {
total_bytes_allocated: 500,
total_allocations_count: 25,
total_iterations: 5,
};
assert_eq!(operation.total_allocations_count(), 25);
}
#[test]
fn report_operation_total_allocations_count_consistency_with_session() {
let session = Session::new();
{
let operation = session.operation("test_consistency");
let _span = operation.measure_thread();
register_fake_allocation(300, 3);
}
let report = session.to_report();
let operations: Vec<_> = report.operations().collect();
assert_eq!(operations.len(), 1);
let (_name, report_op) = operations.first().unwrap();
assert_eq!(report_op.total_allocations_count(), 3);
assert_eq!(report_op.total_bytes_allocated(), 300);
assert_eq!(report_op.total_iterations(), 1);
}
static_assertions::assert_impl_all!(Report: Send, Sync);
static_assertions::assert_impl_all!(ReportOperation: Send, Sync);
static_assertions::assert_impl_all!(Report: UnwindSafe, RefUnwindSafe);
static_assertions::assert_impl_all!(
ReportOperation: UnwindSafe, RefUnwindSafe
);
#[test]
fn report_operation_display_shows_mean_bytes() {
let operation = ReportOperation {
total_bytes_allocated: 1000,
total_allocations_count: 10,
total_iterations: 4,
};
let display_output = operation.to_string();
assert!(display_output.contains("bytes (mean)"));
assert!(display_output.contains("250")); }
#[test]
fn empty_report_display_shows_no_statistics_message() {
let report = Report::new();
let display_output = report.to_string();
assert!(display_output.contains("No allocation statistics captured."));
}
}