use std::fmt;
use std::sync::{Arc, Mutex};
use crate::report::format_count;
use crate::{ERR_POISONED_LOCK, OperationMetrics, ProcessSpan, ThreadSpan};
#[derive(Debug)]
pub struct Operation {
metrics: Arc<Mutex<OperationMetrics>>,
}
impl Operation {
#[must_use]
pub(crate) fn new(_name: String, operation_data: Arc<Mutex<OperationMetrics>>) -> Self {
Self {
metrics: operation_data,
}
}
#[must_use]
pub(crate) fn metrics(&self) -> Arc<Mutex<OperationMetrics>> {
Arc::clone(&self.metrics)
}
pub fn measure_thread(&self) -> ThreadSpan {
ThreadSpan::new(self)
}
pub fn measure_process(&self) -> ProcessSpan {
ProcessSpan::new(self)
}
#[must_use]
#[cfg(test)]
pub(crate) fn mean(&self) -> u64 {
let data = self.metrics.lock().expect(ERR_POISONED_LOCK);
data.mean_bytes()
}
#[must_use]
#[cfg(test)]
pub(crate) fn total_iterations(&self) -> u64 {
let data = self.metrics.lock().unwrap();
data.total_iterations()
}
#[must_use]
#[cfg(test)]
pub(crate) fn total_bytes_allocated(&self) -> u64 {
let data = self.metrics.lock().expect(ERR_POISONED_LOCK);
data.total_bytes_allocated()
}
}
impl fmt::Display for Operation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let metrics = self.metrics.lock().expect(ERR_POISONED_LOCK);
match (metrics.bytes_slope(), metrics.allocations_slope()) {
(Some(bytes), Some(allocations)) => write!(
f,
"{} bytes/iter, {} allocations/iter",
format_count(bytes),
format_count(allocations),
),
_ => write!(f, "no measurements"),
}
}
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use std::panic::{RefUnwindSafe, UnwindSafe};
use super::*;
use crate::Session;
use crate::allocator::register_fake_allocation;
fn create_test_operation() -> Operation {
let session = Session::new().no_stdout().no_file();
session.operation("test")
}
#[test]
fn operation_new() {
let operation = create_test_operation();
assert_eq!(operation.mean(), 0);
assert_eq!(operation.total_iterations(), 0);
assert_eq!(operation.total_bytes_allocated(), 0);
}
#[test]
fn operation_add_single() {
let operation = create_test_operation();
{
let mut metrics = operation.metrics.lock().unwrap();
metrics.add_iterations(100, 1, 1);
}
assert_eq!(operation.mean(), 100);
assert_eq!(operation.total_iterations(), 1);
assert_eq!(operation.total_bytes_allocated(), 100);
}
#[test]
fn operation_add_multiple() {
let operation = create_test_operation();
{
let mut metrics = operation.metrics.lock().unwrap();
metrics.add_iterations(100, 1, 1); metrics.add_iterations(200, 2, 1); metrics.add_iterations(300, 3, 1); }
assert_eq!(operation.mean(), 200); assert_eq!(operation.total_iterations(), 3);
assert_eq!(operation.total_bytes_allocated(), 600);
}
#[test]
fn operation_add_zero() {
let operation = create_test_operation();
{
let mut metrics = operation.metrics.lock().unwrap();
metrics.add_iterations(0, 0, 1);
metrics.add_iterations(0, 0, 1);
}
assert_eq!(operation.mean(), 0);
assert_eq!(operation.total_iterations(), 2);
assert_eq!(operation.total_bytes_allocated(), 0);
}
#[test]
fn operation_span_drop() {
let operation = create_test_operation();
{
let _span = operation.measure_thread().iterations(1);
register_fake_allocation(75, 1);
}
assert_eq!(operation.mean(), 75);
assert_eq!(operation.total_iterations(), 1);
assert_eq!(operation.total_bytes_allocated(), 75);
}
#[test]
fn operation_multiple_spans() {
let operation = create_test_operation();
{
let _span = operation.measure_thread().iterations(1);
register_fake_allocation(100, 1);
}
{
let _span = operation.measure_thread().iterations(1);
register_fake_allocation(200, 1);
}
assert_eq!(operation.mean(), 150); assert_eq!(operation.total_iterations(), 2);
assert_eq!(operation.total_bytes_allocated(), 300);
}
#[test]
fn operation_thread_span_drop() {
let operation = create_test_operation();
{
let _span = operation.measure_thread().iterations(1);
register_fake_allocation(50, 1);
}
assert_eq!(operation.mean(), 50);
assert_eq!(operation.total_iterations(), 1);
assert_eq!(operation.total_bytes_allocated(), 50);
}
#[test]
fn operation_mixed_spans() {
let operation = create_test_operation();
{
let _span = operation.measure_thread().iterations(1);
register_fake_allocation(100, 1);
}
{
let _span = operation.measure_thread().iterations(1);
register_fake_allocation(200, 1);
}
assert_eq!(operation.mean(), 150); assert_eq!(operation.total_iterations(), 2);
assert_eq!(operation.total_bytes_allocated(), 300);
}
#[test]
fn operation_thread_span_no_allocation() {
let operation = create_test_operation();
{
let _span = operation.measure_thread().iterations(1);
}
assert_eq!(operation.mean(), 0);
assert_eq!(operation.total_iterations(), 1);
assert_eq!(operation.total_bytes_allocated(), 0);
}
#[test]
fn operation_batch_iterations() {
let operation = create_test_operation();
{
let _span = operation.measure_thread().iterations(10);
register_fake_allocation(1000, 10);
}
assert_eq!(operation.total_iterations(), 10);
assert_eq!(operation.total_bytes_allocated(), 1000);
assert_eq!(operation.mean(), 100); }
#[test]
fn operation_drop_merges_data() {
let session = Session::new().no_stdout().no_file();
{
let operation = session.operation("test");
{
let mut metrics = operation.metrics.lock().unwrap();
metrics.add_iterations(100, 2, 5);
}
}
let report = session.to_report();
assert!(!report.is_empty());
let report = session.to_report();
let (_, op) = report.operations().next().expect("one operation");
let stats = op.statistics().expect("operation has spans");
let session_display = format!("{session}");
println!("Actual session display: '{session_display}'");
assert!(
session_display.contains(&format_count(stats.bytes.slope)),
"table should show the byte slope: got {session_display}"
);
assert!(
session_display.contains(&format_count(stats.allocations.slope)),
"table should show the allocation slope: got {session_display}"
);
}
#[test]
fn multiple_operations_concurrent() {
let session = Session::new().no_stdout().no_file();
let op1 = session.operation("test");
let op2 = session.operation("test");
{
let mut metrics = op1.metrics.lock().unwrap();
metrics.add_iterations(100, 1, 2); metrics.add_iterations(200, 2, 3); }
assert_eq!(op1.mean(), 160);
assert_eq!(op2.mean(), 160);
drop(op1);
drop(op2);
let session_display = format!("{session}");
assert!(session_display.contains("169.23"), "got {session_display}");
}
static_assertions::assert_impl_all!(Operation: Send, Sync);
static_assertions::assert_impl_all!(
Operation: UnwindSafe, RefUnwindSafe
);
#[test]
fn operation_display_shows_robust_per_iteration_estimate() {
let operation = create_test_operation();
{
let mut metrics = operation.metrics.lock().unwrap();
metrics.add_iterations(250, 5, 2); }
let display_output = operation.to_string();
assert!(
display_output.contains("bytes/iter"),
"got {display_output}"
);
assert!(display_output.contains("250"), "got {display_output}");
}
#[test]
fn operation_display_reports_no_measurements_when_empty() {
let operation = create_test_operation();
assert_eq!(operation.to_string(), "no measurements");
}
}