1use std::collections::HashMap;
4use std::fmt;
5
6#[derive(Clone, Debug, Default)]
68pub struct Report {
69 operations: HashMap<String, ReportOperation>,
70}
71
72#[derive(Clone, Debug)]
74#[expect(
75 clippy::struct_field_names,
76 reason = "field names are descriptive and clear"
77)]
78pub struct ReportOperation {
79 total_bytes_allocated: u64,
80 total_allocations_count: u64,
81 total_iterations: u64,
82}
83
84impl Report {
85 #[cfg(test)]
87 #[must_use]
88 pub(crate) fn new() -> Self {
89 Self {
90 operations: HashMap::new(),
91 }
92 }
93
94 #[must_use]
96 pub(crate) fn from_operation_data(
97 operation_data: &HashMap<String, crate::operation_metrics::OperationMetrics>,
98 ) -> Self {
99 let report_operations = operation_data
100 .iter()
101 .map(|(name, op_data)| {
102 (
103 name.clone(),
104 ReportOperation {
105 total_bytes_allocated: op_data.total_bytes_allocated,
106 total_allocations_count: op_data.total_allocations_count,
107 total_iterations: op_data.total_iterations,
108 },
109 )
110 })
111 .collect();
112
113 Self {
114 operations: report_operations,
115 }
116 }
117
118 #[must_use]
157 pub fn merge(a: &Self, b: &Self) -> Self {
158 let mut merged_operations = a.operations.clone();
159
160 for (name, b_op) in &b.operations {
161 merged_operations
162 .entry(name.clone())
163 .and_modify(|a_op| {
164 a_op.total_bytes_allocated = a_op
165 .total_bytes_allocated
166 .checked_add(b_op.total_bytes_allocated)
167 .expect("merging bytes allocated overflows u64 - this indicates an unrealistic scenario");
168
169 a_op.total_allocations_count = a_op
170 .total_allocations_count
171 .checked_add(b_op.total_allocations_count)
172 .expect("merging allocations count overflows u64 - this indicates an unrealistic scenario");
173
174 a_op.total_iterations = a_op
175 .total_iterations
176 .checked_add(b_op.total_iterations)
177 .expect("merging iteration counts overflows u64 - this indicates an unrealistic scenario");
178 })
179 .or_insert_with(|| b_op.clone());
180 }
181
182 Self {
183 operations: merged_operations,
184 }
185 }
186
187 #[cfg_attr(test, mutants::skip)] pub fn print_to_stdout(&self) {
194 if self.is_empty() {
195 return;
196 }
197 println!("{self}");
198 }
199
200 #[must_use]
202 pub fn is_empty(&self) -> bool {
203 self.operations.is_empty() || self.operations.values().all(|op| op.total_iterations == 0)
204 }
205
206 pub fn operations(&self) -> impl Iterator<Item = (&str, &ReportOperation)> {
240 self.operations.iter().map(|(name, op)| (name.as_str(), op))
241 }
242}
243
244impl ReportOperation {
245 #[must_use]
247 pub fn total_bytes_allocated(&self) -> u64 {
248 self.total_bytes_allocated
249 }
250
251 #[must_use]
253 pub fn total_allocations_count(&self) -> u64 {
254 self.total_allocations_count
255 }
256
257 #[must_use]
259 pub fn total_iterations(&self) -> u64 {
260 self.total_iterations
261 }
262
263 #[expect(
265 clippy::integer_division,
266 reason = "we accept loss of precision for mean calculation"
267 )]
268 #[expect(
269 clippy::arithmetic_side_effects,
270 reason = "division by zero is guarded by if-else"
271 )]
272 #[must_use]
273 pub fn mean_bytes(&self) -> u64 {
274 if self.total_iterations == 0 {
275 0
276 } else {
277 self.total_bytes_allocated / self.total_iterations
278 }
279 }
280
281 #[expect(
283 clippy::integer_division,
284 reason = "we accept loss of precision for mean calculation"
285 )]
286 #[expect(
287 clippy::arithmetic_side_effects,
288 reason = "division by zero is guarded by if-else"
289 )]
290 #[must_use]
291 pub fn mean_allocations(&self) -> u64 {
292 if self.total_iterations == 0 {
293 0
294 } else {
295 self.total_allocations_count / self.total_iterations
296 }
297 }
298
299 #[must_use]
303 pub fn mean(&self) -> u64 {
304 self.mean_bytes()
305 }
306}
307
308impl fmt::Display for ReportOperation {
309 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
310 write!(f, "{} bytes (mean)", self.mean_bytes())
311 }
312}
313
314#[cfg_attr(coverage_nightly, coverage(off))]
316impl fmt::Display for Report {
317 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
318 if self.operations.values().all(|op| op.total_iterations == 0) {
319 writeln!(f, "No allocation statistics captured.")?;
320 } else {
321 writeln!(f, "Allocation statistics:")?;
322 writeln!(f)?;
323
324 let mut sorted_ops: Vec<_> = self.operations.iter().collect();
326 sorted_ops.sort_by_key(|(name, _)| *name);
327
328 let max_name_width = sorted_ops
330 .iter()
331 .map(|(name, _)| name.len())
332 .max()
333 .unwrap_or(0)
334 .max("Operation".len());
335
336 let max_bytes_width = sorted_ops
337 .iter()
338 .map(|(_, operation)| operation.mean_bytes().to_string().len())
339 .max()
340 .unwrap_or(0)
341 .max("Mean bytes".len());
342
343 let max_count_width = sorted_ops
344 .iter()
345 .map(|(_, operation)| operation.mean_allocations().to_string().len())
346 .max()
347 .unwrap_or(0)
348 .max("Mean count".len());
349
350 writeln!(
352 f,
353 "| {:<name_width$} | {:>bytes_width$} | {:>count_width$} |",
354 "Operation",
355 "Mean bytes",
356 "Mean count",
357 name_width = max_name_width,
358 bytes_width = max_bytes_width,
359 count_width = max_count_width
360 )?;
361
362 let separator_name_width = max_name_width
364 .checked_add(2)
365 .expect("operation name width fits in memory, adding 2 cannot overflow");
366 let separator_bytes_width = max_bytes_width
367 .checked_add(2)
368 .expect("bytes width fits in memory, adding 2 cannot overflow");
369 let separator_count_width = max_count_width
370 .checked_add(2)
371 .expect("count width fits in memory, adding 2 cannot overflow");
372 writeln!(
373 f,
374 "|{:-<name_width$}|{:-<bytes_width$}|{:-<count_width$}|",
375 "",
376 "",
377 "",
378 name_width = separator_name_width,
379 bytes_width = separator_bytes_width,
380 count_width = separator_count_width
381 )?;
382
383 for (name, operation) in sorted_ops {
385 writeln!(
386 f,
387 "| {:<name_width$} | {:>bytes_width$} | {:>count_width$} |",
388 name,
389 operation.mean_bytes(),
390 operation.mean_allocations(),
391 name_width = max_name_width,
392 bytes_width = max_bytes_width,
393 count_width = max_count_width
394 )?;
395 }
396 }
397 Ok(())
398 }
399}
400
401#[cfg(test)]
402#[cfg_attr(coverage_nightly, coverage(off))]
403mod tests {
404 use std::panic::{RefUnwindSafe, UnwindSafe};
405
406 use super::*;
407 use crate::Session;
408 use crate::allocator::register_fake_allocation;
409
410 #[test]
411 fn new_report_is_empty() {
412 let report = Report::new();
413 assert!(report.is_empty());
414 }
415
416 #[test]
417 fn report_from_empty_session_is_empty() {
418 let session = Session::new();
419 let report = session.to_report();
420 assert!(report.is_empty());
421 }
422
423 #[test]
424 fn report_from_session_with_operations_is_not_empty() {
425 let session = Session::new();
426 {
427 let operation = session.operation("test");
428 let _span = operation.measure_thread();
429 register_fake_allocation(100, 1);
430 } let report = session.to_report();
433 assert!(!report.is_empty());
434 }
435
436 #[test]
437 fn merge_empty_reports() {
438 let report1 = Report::new();
439 let report2 = Report::new();
440 let merged = Report::merge(&report1, &report2);
441 assert!(merged.is_empty());
442 }
443
444 #[test]
445 fn merge_empty_with_non_empty() {
446 let session = Session::new();
447 {
448 let operation = session.operation("test");
449 let _span = operation.measure_thread();
450 register_fake_allocation(100, 1);
451 } let report1 = Report::new();
454 let report2 = session.to_report();
455
456 let merged1 = Report::merge(&report1, &report2);
457 let merged2 = Report::merge(&report2, &report1);
458
459 assert!(!merged1.is_empty());
460 assert!(!merged2.is_empty());
461 }
462
463 #[test]
464 fn merge_different_operations() {
465 let session1 = Session::new();
466 let session2 = Session::new();
467
468 {
469 let op1 = session1.operation("test1");
470 let _span1 = op1.measure_thread();
471 register_fake_allocation(100, 1);
472 } {
475 let op2 = session2.operation("test2");
476 let _span2 = op2.measure_thread();
477 register_fake_allocation(200, 2);
478 } let report1 = session1.to_report();
481 let report2 = session2.to_report();
482 let merged = Report::merge(&report1, &report2);
483
484 assert_eq!(merged.operations.len(), 2);
485 assert!(merged.operations.contains_key("test1"));
486 assert!(merged.operations.contains_key("test2"));
487 }
488
489 #[test]
490 fn merge_same_operations() {
491 let session1 = Session::new();
492 let session2 = Session::new();
493
494 {
495 let op1 = session1.operation("test");
496 let _span1 = op1.measure_thread();
497 register_fake_allocation(100, 1);
498 } {
501 let op2 = session2.operation("test");
502 let _span2 = op2.measure_thread();
503 register_fake_allocation(200, 2);
504 } let report1 = session1.to_report();
507 let report2 = session2.to_report();
508 let merged = Report::merge(&report1, &report2);
509
510 assert_eq!(merged.operations.len(), 1);
511 let merged_op = merged.operations.get("test").unwrap();
512 assert_eq!(merged_op.total_iterations, 2); assert_eq!(merged_op.total_bytes_allocated, 300); assert_eq!(merged_op.total_allocations_count, 3); }
516
517 #[test]
518 fn report_clone() {
519 let session = Session::new();
520 {
521 let operation = session.operation("test");
522 let _span = operation.measure_thread();
523 register_fake_allocation(100, 1);
524 } let report1 = session.to_report();
527 let report2 = report1.clone();
528
529 assert_eq!(report1.operations.len(), report2.operations.len());
530 }
531
532 #[test]
533 fn report_operation_total_allocations_count_zero() {
534 let operation = ReportOperation {
535 total_bytes_allocated: 0,
536 total_allocations_count: 0,
537 total_iterations: 1,
538 };
539
540 assert_eq!(operation.total_allocations_count(), 0);
541 }
542
543 #[test]
544 fn report_operation_total_allocations_count_multiple() {
545 let operation = ReportOperation {
546 total_bytes_allocated: 500,
547 total_allocations_count: 25,
548 total_iterations: 5,
549 };
550
551 assert_eq!(operation.total_allocations_count(), 25);
552 }
553
554 #[test]
555 fn report_operation_total_allocations_count_consistency_with_session() {
556 let session = Session::new();
557 {
558 let operation = session.operation("test_consistency");
559 let _span = operation.measure_thread();
560 register_fake_allocation(300, 3);
562 } let report = session.to_report();
565 let operations: Vec<_> = report.operations().collect();
566 assert_eq!(operations.len(), 1);
567
568 let (_name, report_op) = operations.first().unwrap();
569 assert_eq!(report_op.total_allocations_count(), 3);
570 assert_eq!(report_op.total_bytes_allocated(), 300);
571 assert_eq!(report_op.total_iterations(), 1);
572 }
573
574 static_assertions::assert_impl_all!(Report: Send, Sync);
576 static_assertions::assert_impl_all!(ReportOperation: Send, Sync);
577
578 static_assertions::assert_impl_all!(Report: UnwindSafe, RefUnwindSafe);
580 static_assertions::assert_impl_all!(
581 ReportOperation: UnwindSafe, RefUnwindSafe
582 );
583
584 #[test]
585 fn report_operation_display_shows_mean_bytes() {
586 let operation = ReportOperation {
587 total_bytes_allocated: 1000,
588 total_allocations_count: 10,
589 total_iterations: 4,
590 };
591
592 let display_output = operation.to_string();
593 assert!(display_output.contains("bytes (mean)"));
594 assert!(display_output.contains("250")); }
596
597 #[test]
598 fn empty_report_display_shows_no_statistics_message() {
599 let report = Report::new();
600 let display_output = report.to_string();
601 assert!(display_output.contains("No allocation statistics captured."));
602 }
603}