use std::collections::HashMap;
use std::fmt;
use std::time::Duration;
#[derive(Clone, Debug, Default)]
pub struct Report {
operations: HashMap<String, ReportOperation>,
}
#[derive(Clone, Debug)]
pub struct ReportOperation {
total_processor_time: Duration,
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, data)| {
(
name.clone(),
ReportOperation {
total_processor_time: data.total_processor_time,
total_iterations: 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_processor_time = a_op
.total_processor_time
.checked_add(b_op.total_processor_time)
.expect("merging processor times overflows Duration - 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_processor_time(&self) -> Duration {
self.total_processor_time
}
#[must_use]
pub fn total_iterations(&self) -> u64 {
self.total_iterations
}
#[must_use]
pub fn mean(&self) -> Duration {
if self.total_iterations == 0 {
Duration::ZERO
} else {
Duration::from_nanos(
self.total_processor_time
.as_nanos()
.checked_div(u128::from(self.total_iterations))
.expect("guarded by if condition")
.try_into()
.expect("all realistic values fit in u64"),
)
}
}
}
impl fmt::Display for ReportOperation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?} (mean)", self.mean())
}
}
#[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 processor time statistics captured.")?;
} else {
writeln!(f, "Processor time 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_mean_width = sorted_ops
.iter()
.map(|(_, operation)| format!("{:?}", operation.mean()).len())
.max()
.unwrap_or(0)
.max("Mean".len());
writeln!(
f,
"| {:<name_width$} | {:>mean_width$} |",
"Operation",
"Mean",
name_width = max_name_width,
mean_width = max_mean_width
)?;
let separator_name_width = max_name_width
.checked_add(2)
.expect("operation name width fits in memory, adding 2 cannot overflow");
let separator_mean_width = max_mean_width
.checked_add(2)
.expect("mean width fits in memory, adding 2 cannot overflow");
writeln!(
f,
"|{:-<name_width$}|{:-<mean_width$}|",
"",
"",
name_width = separator_name_width,
mean_width = separator_mean_width
)?;
for (name, operation) in sorted_ops {
writeln!(
f,
"| {:<name_width$} | {:>mean_width$} |",
name,
format!("{:?}", operation.mean()),
name_width = max_name_width,
mean_width = max_mean_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;
fn create_test_session() -> Session {
use crate::pal::{FakePlatform, PlatformFacade};
let fake_platform = FakePlatform::new();
let platform_facade = PlatformFacade::fake(fake_platform);
Session::with_platform(platform_facade)
}
#[test]
fn new_report_is_empty() {
let report = Report::new();
assert!(report.is_empty());
}
#[test]
fn report_from_empty_session_is_empty() {
let session = create_test_session();
let report = session.to_report();
assert!(report.is_empty());
}
#[test]
fn report_from_session_with_operations_is_not_empty() {
let session = create_test_session();
{
let operation = session.operation("test");
let _span = operation.measure_thread();
}
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 = create_test_session();
{
let operation = session.operation("test");
let _span = operation.measure_thread();
}
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 = create_test_session();
let session2 = create_test_session();
{
let op1 = session1.operation("test1");
let _span1 = op1.measure_thread();
}
{
let op2 = session2.operation("test2");
let _span2 = op2.measure_thread();
}
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 = create_test_session();
let session2 = create_test_session();
{
let op1 = session1.operation("test");
let _span1 = op1.measure_thread().iterations(5);
}
{
let op2 = session2.operation("test");
let _span2 = op2.measure_thread().iterations(3);
}
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, 8); }
#[test]
fn report_clone() {
let session = create_test_session();
{
let operation = session.operation("test");
let _span = operation.measure_thread();
}
let report1 = session.to_report();
let report2 = report1.clone();
assert_eq!(report1.operations.len(), report2.operations.len());
}
#[test]
fn report_mean_with_fake_platform() {
use crate::pal::{FakePlatform, PlatformFacade};
let fake_platform = FakePlatform::new();
let platform_facade = PlatformFacade::fake(fake_platform.clone());
let session = Session::with_platform(platform_facade);
fake_platform.set_thread_time(Duration::from_millis(10));
{
let operation = session.operation("test_operation");
let _span = operation.measure_thread().iterations(4);
fake_platform.set_thread_time(Duration::from_millis(50));
}
{
let operation = session.operation("test_operation");
let _span = operation.measure_thread().iterations(2);
fake_platform.set_thread_time(Duration::from_millis(90));
}
let report = session.to_report();
let operations: Vec<_> = report.operations().collect();
assert_eq!(operations.len(), 1);
let (_name, op) = operations.first().unwrap();
let expected_mean = Duration::from_nanos(13_333_333); assert_eq!(op.mean(), expected_mean);
assert_eq!(op.total_processor_time(), Duration::from_millis(80));
assert_eq!(op.total_iterations(), 6);
}
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() {
use crate::pal::{FakePlatform, PlatformFacade};
let fake_platform = FakePlatform::new();
let platform_facade = PlatformFacade::fake(fake_platform.clone());
let session = Session::with_platform(platform_facade);
fake_platform.set_thread_time(Duration::from_millis(0));
{
let operation = session.operation("test_op");
let _span = operation.measure_thread().iterations(2);
fake_platform.set_thread_time(Duration::from_millis(100));
}
let report = session.to_report();
let (_name, op) = report.operations().next().unwrap();
let display = op.to_string();
assert!(display.contains("mean"), "Display should mention 'mean'");
assert!(
display.contains("50"),
"Display should show the mean duration containing '50' (for 50ms): got {display}"
);
}
}