use std::marker::PhantomData;
use std::sync::{Arc, Mutex};
use crate::counters::{ThreadCounters, get_or_init_thread_counters};
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 ThreadSpan {
metrics: Arc<Mutex<OperationMetrics>>,
start_bytes: u64,
start_count: u64,
start_outstanding: i64,
enclosing_watermark: i64,
iterations: Option<u64>,
_single_threaded: PhantomData<*const ()>,
}
impl ThreadSpan {
pub(crate) fn new(operation: &Operation) -> Self {
let counters = get_or_init_thread_counters();
let start_outstanding = counters.outstanding();
let enclosing_watermark = counters.watermark();
counters.set_watermark(start_outstanding);
Self {
metrics: operation.metrics(),
start_bytes: counters.bytes(),
start_count: counters.count(),
start_outstanding,
enclosing_watermark,
iterations: None,
_single_threaded: PhantomData,
}
}
pub fn iterations(mut self, iterations: u64) -> Self {
self.iterations = Some(iterations);
self
}
}
impl Drop for ThreadSpan {
fn drop(&mut self) {
let counters = get_or_init_thread_counters();
let peak_bytes =
restore_watermark(counters, self.start_outstanding, self.enclosing_watermark);
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) =
thread_deltas(counters, self.start_bytes, self.start_count);
let mut data = self.metrics.lock().expect(ERR_POISONED_LOCK);
data.add_span(SpanMeasurement {
iterations,
bytes: bytes_delta,
count: count_delta,
peak_outstanding_bytes: Some(peak_bytes),
});
}
}
fn restore_watermark(
counters: ThreadCounters,
start_outstanding: i64,
enclosing_watermark: i64,
) -> u64 {
let span_watermark = counters.watermark();
counters.set_watermark(enclosing_watermark.max(span_watermark));
span_watermark
.saturating_sub(start_outstanding)
.max(0)
.cast_unsigned()
}
fn thread_deltas(counters: ThreadCounters, start_bytes: u64, start_count: u64) -> (u64, u64) {
let bytes_delta = counters
.bytes()
.checked_sub(start_bytes)
.expect("thread bytes allocated could not possibly decrease");
let count_delta = counters
.count()
.checked_sub(start_count)
.expect("thread allocations count could not possibly decrease");
(bytes_delta, count_delta)
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use std::panic::{self, AssertUnwindSafe, RefUnwindSafe, UnwindSafe};
use std::sync::Barrier;
use std::thread;
use testing::{assert_panics, with_watchdog};
use super::*;
use crate::Session;
use crate::counters::{register_fake_allocation, register_fake_deallocation};
const BLOCK: u64 = 100;
#[expect(
clippy::cast_precision_loss,
reason = "the byte counts these tests use are small integers that f64 represents exactly"
)]
fn as_reported(bytes: u64) -> f64 {
bytes as f64
}
fn mean_peak(levels: &[u64]) -> f64 {
let count = u64::try_from(levels.len()).unwrap();
levels.iter().copied().map(as_reported).sum::<f64>() / as_reported(count)
}
static_assertions::assert_not_impl_all!(ThreadSpan: Send);
static_assertions::assert_not_impl_all!(ThreadSpan: Sync);
static_assertions::assert_impl_all!(ThreadSpan: UnwindSafe, RefUnwindSafe);
#[test]
fn peak_is_the_high_water_mark_not_the_total() {
const ROUNDS: u64 = 3;
let session = Session::new().no_stdout().no_file();
let operation = session.operation("test");
{
let _span = operation.measure_thread().iterations(1);
for _ in 0..ROUNDS {
register_fake_allocation(BLOCK, 1);
register_fake_deallocation(BLOCK);
}
}
assert_eq!(operation.total_bytes_allocated(), BLOCK * ROUNDS);
assert_eq!(operation.peak_outstanding_bytes(), Some(as_reported(BLOCK)));
}
#[test]
fn peak_ignores_memory_outstanding_before_the_span() {
const PRE_EXISTING: u64 = 1000;
const SPAN_HELD: u64 = 50;
let session = Session::new().no_stdout().no_file();
let operation = session.operation("test");
register_fake_allocation(PRE_EXISTING, 1);
{
let _span = operation.measure_thread().iterations(1);
register_fake_allocation(SPAN_HELD, 1);
}
register_fake_deallocation(PRE_EXISTING + SPAN_HELD);
assert_eq!(
operation.peak_outstanding_bytes(),
Some(as_reported(SPAN_HELD))
);
}
#[test]
fn peak_underreports_when_the_span_frees_first() {
const PRE_EXISTING: u64 = 1000;
const FREED_FIRST: u64 = 800;
const SPAN_HELD: u64 = 500;
let session = Session::new().no_stdout().no_file();
let operation = session.operation("test");
register_fake_allocation(PRE_EXISTING, 1);
{
let _span = operation.measure_thread().iterations(1);
register_fake_deallocation(FREED_FIRST);
register_fake_allocation(SPAN_HELD, 1);
}
register_fake_deallocation(PRE_EXISTING - FREED_FIRST + SPAN_HELD);
assert_eq!(operation.peak_outstanding_bytes(), Some(0.0));
}
#[test]
fn nested_span_peak_is_visible_to_the_enclosing_span() {
const OUTER_HELD: u64 = 10;
const INNER_HELD: u64 = 200;
let session = Session::new().no_stdout().no_file();
let outer = session.operation("outer");
let inner = session.operation("inner");
{
let _outer_span = outer.measure_thread().iterations(1);
register_fake_allocation(OUTER_HELD, 1);
{
let _inner_span = inner.measure_thread().iterations(1);
register_fake_allocation(INNER_HELD, 1);
register_fake_deallocation(INNER_HELD);
}
register_fake_deallocation(OUTER_HELD);
}
assert_eq!(
inner.peak_outstanding_bytes(),
Some(as_reported(INNER_HELD))
);
assert_eq!(
outer.peak_outstanding_bytes(),
Some(as_reported(OUTER_HELD + INNER_HELD))
);
}
#[test]
fn enclosing_peak_survives_a_smaller_nested_span() {
const OUTER_HELD: u64 = 900;
const INNER_HELD: u64 = 5;
let session = Session::new().no_stdout().no_file();
let outer = session.operation("outer");
let inner = session.operation("inner");
{
let _outer_span = outer.measure_thread().iterations(1);
register_fake_allocation(OUTER_HELD, 1);
register_fake_deallocation(OUTER_HELD);
{
let _inner_span = inner.measure_thread().iterations(1);
register_fake_allocation(INNER_HELD, 1);
register_fake_deallocation(INNER_HELD);
}
}
assert_eq!(
inner.peak_outstanding_bytes(),
Some(as_reported(INNER_HELD))
);
assert_eq!(
outer.peak_outstanding_bytes(),
Some(as_reported(OUTER_HELD))
);
}
#[test]
fn abandoned_nested_span_does_not_suppress_the_enclosing_peak() {
const INNER_HELD: u64 = 400;
let session = Session::new().no_stdout().no_file();
let outer = session.operation("outer");
let inner = session.operation("inner");
{
let _outer_span = outer.measure_thread().iterations(1);
assert_panics(|| {
let _inner_span = inner.measure_thread().iterations(1);
register_fake_allocation(INNER_HELD, 1);
panic!("boom");
});
register_fake_deallocation(INNER_HELD);
}
assert_eq!(inner.peak_outstanding_bytes(), None);
assert_eq!(
outer.peak_outstanding_bytes(),
Some(as_reported(INNER_HELD))
);
}
#[test]
fn nested_span_without_iterations_still_restores_the_enclosing_peak() {
const INNER_HELD: u64 = 400;
let session = Session::new().no_stdout().no_file();
let outer = session.operation("outer");
let inner = session.operation("inner");
{
let _outer_span = outer.measure_thread().iterations(1);
assert_panics(|| {
let _inner_span = inner.measure_thread();
register_fake_allocation(INNER_HELD, 1);
});
register_fake_deallocation(INNER_HELD);
}
assert_eq!(inner.peak_outstanding_bytes(), None);
assert_eq!(
outer.peak_outstanding_bytes(),
Some(as_reported(INNER_HELD))
);
}
#[test]
fn sequential_spans_each_contribute_their_peak() {
const LEVELS: [u64; 3] = [100, 700, 400];
let session = Session::new().no_stdout().no_file();
let operation = session.operation("test");
for level in LEVELS {
let _span = operation.measure_thread().iterations(1);
register_fake_allocation(level, 1);
register_fake_deallocation(level);
}
assert_eq!(operation.peak_outstanding_bytes(), Some(mean_peak(&LEVELS)));
}
#[test]
fn concurrent_thread_spans_average_their_peaks() {
const LEVELS: [u64; 2] = [300, 900];
with_watchdog(|| {
let session = Session::new().no_stdout().no_file();
let operation = session.operation("test");
let both_inside = Barrier::new(LEVELS.len());
thread::scope(|scope| {
for level in LEVELS {
let operation = &operation;
let both_inside = &both_inside;
scope.spawn(move || {
let opened = panic::catch_unwind(AssertUnwindSafe(|| {
let span = operation.measure_thread().iterations(1);
register_fake_allocation(level, 1);
span
}));
both_inside.wait();
let span = opened.unwrap_or_else(|payload| panic::resume_unwind(payload));
register_fake_deallocation(level);
drop(span);
});
}
});
assert_eq!(operation.peak_outstanding_bytes(), Some(mean_peak(&LEVELS)));
});
}
#[test]
fn iterations_zero_is_accepted() {
let session = Session::new().no_stdout().no_file();
let operation = session.operation("test");
drop(operation.measure_thread().iterations(0));
assert_eq!(operation.total_iterations(), 0);
}
#[test]
fn records_span_via_post_hoc_iterations() {
const ITERATIONS: u64 = 5;
let session = Session::new().no_stdout().no_file();
let operation = session.operation("test");
drop(operation.measure_thread().iterations(ITERATIONS));
assert_eq!(operation.total_iterations(), ITERATIONS);
}
#[test]
fn records_span_via_iterations_guard() {
const ITERATIONS: u64 = 3;
let session = Session::new().no_stdout().no_file();
let operation = session.operation("test");
{
let _span = operation.measure_thread().iterations(ITERATIONS);
}
assert_eq!(operation.total_iterations(), ITERATIONS);
}
#[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_thread());
}
#[test]
fn panic_while_held_records_nothing() {
let session = Session::new().no_stdout().no_file();
let operation = session.operation("test");
assert_panics(|| {
let _span = operation.measure_thread().iterations(1);
panic!("boom");
});
assert_eq!(operation.total_iterations(), 0);
}
}