use std::cell::Cell;
use std::marker::PhantomData;
use std::panic::RefUnwindSafe;
use std::sync::{Arc, Mutex};
use crate::counters::{AllocationTotals, allocation_totals};
use crate::{ERR_POISONED_LOCK, Operation, OperationMetrics, SpanMeasurement};
#[derive(Debug)]
#[must_use = "a span must be held across the measured work and given a count with `.iterations(n)`; it records when dropped and panics if the count is missing"]
pub struct ProcessSpan {
metrics: Arc<Mutex<OperationMetrics>>,
start_bytes: u64,
start_count: u64,
iterations: Option<u64>,
_not_sync: PhantomData<Cell<()>>,
}
impl RefUnwindSafe for ProcessSpan {}
impl ProcessSpan {
pub(crate) fn new(operation: &Operation) -> Self {
let AllocationTotals {
bytes: start_bytes,
count: start_count,
} = allocation_totals();
Self {
metrics: operation.metrics(),
start_bytes,
start_count,
iterations: None,
_not_sync: PhantomData,
}
}
pub fn iterations(mut self, iterations: u64) -> Self {
self.iterations = Some(iterations);
self
}
}
impl Drop for ProcessSpan {
fn drop(&mut self) {
if std::thread::panicking() {
return;
}
let iterations = self.iterations.expect(
"the span was dropped without an iteration count; call `.iterations(1)` \
if the measured region is a single iteration",
);
let (bytes_delta, count_delta) =
process_deltas(self.start_bytes, self.start_count, allocation_totals());
let mut data = self.metrics.lock().expect(ERR_POISONED_LOCK);
data.add_span(SpanMeasurement {
iterations,
bytes: bytes_delta,
count: count_delta,
peak_outstanding_bytes: None,
});
}
}
fn process_deltas(start_bytes: u64, start_count: u64, current: AllocationTotals) -> (u64, u64) {
let AllocationTotals {
bytes: current_bytes,
count: current_count,
} = current;
let bytes_delta = current_bytes
.checked_sub(start_bytes)
.expect("total bytes allocated could not possibly decrease");
let count_delta = current_count
.checked_sub(start_count)
.expect("total allocations count could not possibly decrease");
(bytes_delta, count_delta)
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use std::panic::{RefUnwindSafe, UnwindSafe};
use super::*;
use crate::Session;
static_assertions::assert_impl_all!(ProcessSpan: Send, UnwindSafe, RefUnwindSafe);
static_assertions::assert_not_impl_any!(ProcessSpan: Sync);
#[test]
fn process_deltas_subtract_each_start_counter() {
let current = AllocationTotals {
bytes: 137,
count: 19,
};
assert_eq!(process_deltas(100, 12, current), (37, 7));
assert_eq!(process_deltas(120, 16, current), (17, 3));
}
#[test]
fn process_deltas_preserve_zero_activity() {
let current = AllocationTotals {
bytes: 137,
count: 19,
};
assert_eq!(
process_deltas(current.bytes, current.count, current),
(0, 0)
);
}
#[test]
fn process_deltas_preserve_large_integer_totals() {
let current = AllocationTotals {
bytes: u64::MAX,
count: u64::MAX,
};
assert_eq!(
process_deltas(u64::MAX - 37, u64::MAX - 7, current),
(37, 7)
);
}
#[test]
#[should_panic]
fn process_deltas_reject_decreasing_bytes() {
let current = AllocationTotals { bytes: 0, count: 1 };
_ = process_deltas(1, 0, current);
}
#[test]
#[should_panic]
fn process_deltas_reject_decreasing_count() {
let current = AllocationTotals { bytes: 1, count: 0 };
_ = process_deltas(0, 1, current);
}
#[test]
fn iterations_zero_is_accepted() {
let session = Session::new().no_stdout().no_file();
let operation = session.operation("test");
drop(operation.measure_process().iterations(0));
assert_eq!(operation.total_iterations(), 0);
}
#[test]
fn records_span_via_post_hoc_iterations() {
let session = Session::new().no_stdout().no_file();
let operation = session.operation("test");
drop(operation.measure_process().iterations(5));
assert_eq!(operation.total_iterations(), 5);
}
#[test]
fn records_span_via_iterations_guard() {
let session = Session::new().no_stdout().no_file();
let operation = session.operation("test");
{
let _span = operation.measure_process().iterations(3);
}
assert_eq!(operation.total_iterations(), 3);
}
#[test]
#[should_panic]
fn dropping_span_without_iterations_panics() {
let session = Session::new().no_stdout().no_file();
let operation = session.operation("test");
drop(operation.measure_process());
}
#[test]
fn process_span_reports_no_peak() {
let session = Session::new().no_stdout().no_file();
let operation = session.operation("test");
drop(operation.measure_process().iterations(1));
assert_eq!(operation.peak_outstanding_bytes(), None);
}
#[test]
fn a_process_span_suppresses_the_peak_of_a_mixed_operation() {
let session = Session::new().no_stdout().no_file();
let operation = session.operation("test");
drop(operation.measure_thread().iterations(1));
assert_eq!(operation.peak_outstanding_bytes(), Some(0.0));
drop(operation.measure_process().iterations(1));
assert_eq!(operation.peak_outstanding_bytes(), None);
}
#[test]
fn panic_while_held_records_nothing() {
let session = Session::new().no_stdout().no_file();
let operation = session.operation("test");
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _span = operation.measure_process().iterations(1);
panic!("boom");
}));
assert!(result.is_err());
assert_eq!(operation.total_iterations(), 0);
}
}