use std::marker::PhantomData;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use crate::pal::{Platform, PlatformFacade};
use crate::{ERR_POISONED_LOCK, Operation, OperationMetrics};
#[derive(Debug)]
#[must_use = "Measurements are taken between creation and drop"]
pub struct ThreadSpan {
metrics: Arc<Mutex<OperationMetrics>>,
platform: PlatformFacade,
start_time: Duration,
iterations: u64,
_single_threaded: PhantomData<*const ()>,
}
impl ThreadSpan {
pub(crate) fn new(operation: &Operation, iterations: u64) -> Self {
assert!(iterations != 0, "Iterations cannot be zero");
let platform = operation.platform().clone();
let start_time = platform.thread_time();
Self {
metrics: operation.metrics(),
platform,
start_time,
iterations,
_single_threaded: PhantomData,
}
}
pub fn iterations(mut self, iterations: u64) -> Self {
assert!(iterations != 0, "Iterations cannot be zero");
self.iterations = iterations;
self
}
#[must_use]
#[cfg_attr(test, mutants::skip)] fn to_duration(&self) -> Duration {
let current_time = self.platform.thread_time();
let total_duration = current_time.saturating_sub(self.start_time);
if self.iterations > 1 {
Duration::from_nanos(
total_duration
.as_nanos()
.checked_div(u128::from(self.iterations))
.expect("guarded by if condition")
.try_into()
.expect("all realistic values fit in u64"),
)
} else {
total_duration
}
}
}
impl Drop for ThreadSpan {
fn drop(&mut self) {
let duration = self.to_duration();
let mut data = self.metrics.lock().expect(ERR_POISONED_LOCK);
data.add_iterations(duration, self.iterations);
}
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use std::time::Duration;
use crate::Session;
use crate::pal::{FakePlatform, PlatformFacade};
fn create_test_session() -> Session {
let fake_platform = FakePlatform::new();
let platform_facade = PlatformFacade::fake(fake_platform);
Session::with_platform(platform_facade)
}
fn create_test_session_with_time(thread_time: Duration) -> Session {
let fake_platform = FakePlatform::new();
fake_platform.set_thread_time(thread_time);
let platform_facade = PlatformFacade::fake(fake_platform);
Session::with_platform(platform_facade)
}
#[test]
fn creates_span_with_iterations() {
let session = create_test_session();
let operation = session.operation("test");
let span = operation.measure_thread().iterations(5);
assert_eq!(span.iterations, 5);
}
#[test]
#[should_panic]
fn panics_on_zero_iterations() {
let session = create_test_session();
let operation = session.operation("test");
let _span = operation.measure_thread().iterations(0);
}
#[test]
fn extracts_time_from_pal() {
let session = create_test_session_with_time(Duration::ZERO);
let operation = session.operation("test");
{
let _span = operation.measure_thread();
}
assert_eq!(operation.total_iterations(), 1);
assert_eq!(operation.total_processor_time(), Duration::ZERO);
}
#[test]
fn calculates_time_delta() {
let session = create_test_session();
let operation = session.operation("test");
{
let span = operation.measure_thread();
drop(span);
}
assert_eq!(operation.total_iterations(), 1);
}
#[test]
fn records_one_span_per_iteration() {
let session = create_test_session();
let operation = session.operation("test");
{
let _span = operation.measure_thread().iterations(5);
}
assert_eq!(operation.total_iterations(), 5);
}
#[test]
fn calculates_per_iteration_duration() {
let session = create_test_session();
let operation = session.operation("test");
{
let _span = operation.measure_thread().iterations(10);
}
assert_eq!(operation.total_iterations(), 10);
}
#[test]
fn uses_thread_time_from_pal() {
let fake_platform = FakePlatform::new();
fake_platform.set_thread_time(Duration::from_millis(50));
fake_platform.set_process_time(Duration::from_millis(200));
let platform_facade = PlatformFacade::fake(fake_platform);
let session = Session::with_platform(platform_facade);
let operation = session.operation("test");
{
let _span = operation.measure_thread();
}
assert_eq!(operation.total_iterations(), 1);
}
#[test]
fn correctly_divides_by_iterations_count_single() {
let session = create_test_session();
let operation = session.operation("test");
{
let _span = operation.measure_thread();
}
assert_eq!(operation.total_iterations(), 1);
assert_eq!(operation.total_processor_time(), Duration::ZERO);
}
#[test]
fn correctly_divides_by_iterations_count_multiple() {
let session = create_test_session();
let operation = session.operation("test");
{
let _span = operation.measure_thread().iterations(10);
}
assert_eq!(operation.total_iterations(), 10);
assert_eq!(operation.total_processor_time(), Duration::ZERO);
}
#[test]
fn iterations_divisor_applied_correctly_single() {
let test_cases = [
Duration::from_nanos(1000),
Duration::from_millis(5),
Duration::from_secs(1),
];
for total_duration in test_cases {
let iterations = 1_u64;
let result = if iterations > 1 {
Duration::from_nanos(
total_duration
.as_nanos()
.checked_div(u128::from(iterations))
.unwrap_or(0)
.try_into()
.unwrap_or(0),
)
} else {
total_duration
};
assert_eq!(result, total_duration);
}
}
#[test]
fn iterations_divisor_applied_correctly_multiple() {
let test_cases = [
(Duration::from_nanos(1000), 5_u64, Duration::from_nanos(200)),
(Duration::from_millis(100), 4_u64, Duration::from_millis(25)),
(Duration::from_secs(1), 10_u64, Duration::from_millis(100)),
];
for (total_duration, iterations, expected) in test_cases {
let result = if iterations > 1 {
Duration::from_nanos(
total_duration
.as_nanos()
.checked_div(u128::from(iterations))
.unwrap_or(0)
.try_into()
.unwrap_or(0),
)
} else {
total_duration
};
assert_eq!(
result, expected,
"Failed for total={total_duration:?}, iterations={iterations}"
);
}
}
#[test]
fn iterations_divisor_logic() {
let fake_platform = FakePlatform::new();
fake_platform.set_thread_time(Duration::ZERO);
let platform_facade = PlatformFacade::fake(fake_platform);
let session = Session::with_platform(platform_facade);
let operation = session.operation("test");
let span = operation.measure_thread().iterations(5);
let test_total_duration = Duration::from_nanos(1000);
let iterations = 5_u64;
let expected_per_iteration = Duration::from_nanos(
test_total_duration
.as_nanos()
.checked_div(u128::from(iterations))
.unwrap_or(0)
.try_into()
.unwrap_or(0),
);
assert_eq!(expected_per_iteration, Duration::from_nanos(200));
drop(span);
}
use std::panic::RefUnwindSafe;
use std::panic::UnwindSafe;
static_assertions::assert_not_impl_all!(super::ThreadSpan: Send);
static_assertions::assert_not_impl_all!(super::ThreadSpan: Sync);
static_assertions::assert_impl_all!(
super::ThreadSpan: UnwindSafe, RefUnwindSafe
);
}