use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::{fmt, thread};
use crate::{ERR_POISONED_LOCK, Operation, OperationMetrics, Report};
#[derive(Debug)]
pub struct Session {
operations: Arc<Mutex<HashMap<String, Arc<Mutex<OperationMetrics>>>>>,
emit_stdout: bool,
emit_file: bool,
}
impl Session {
#[expect(
clippy::new_without_default,
reason = "to avoid ambiguity with the notion of a 'default session' that is not actually a default session"
)]
#[must_use]
pub fn new() -> Self {
Self {
operations: Arc::new(Mutex::new(HashMap::new())),
emit_stdout: true,
emit_file: true,
}
}
#[must_use]
pub fn no_stdout(mut self) -> Self {
self.emit_stdout = false;
self
}
#[must_use]
pub fn no_file(mut self) -> Self {
self.emit_file = false;
self
}
pub fn operation(&self, name: impl Into<String>) -> Operation {
let name = name.into();
let operation_data = {
let mut operations = self.operations.lock().expect(ERR_POISONED_LOCK);
Arc::clone(
operations
.entry(name.clone())
.or_insert_with(|| Arc::new(Mutex::new(OperationMetrics::default()))),
)
};
Operation::new(name, operation_data)
}
#[must_use]
pub fn to_report(&self) -> Report {
let operations = self.operations.lock().expect(ERR_POISONED_LOCK);
let operation_data: HashMap<String, OperationMetrics> = operations
.iter()
.map(|(name, data_ref)| {
(
name.clone(),
data_ref.lock().expect(ERR_POISONED_LOCK).clone(),
)
})
.collect();
Report::from_operation_data(&operation_data)
}
#[must_use]
pub fn is_empty(&self) -> bool {
let operations = self.operations.lock().expect(ERR_POISONED_LOCK);
operations.is_empty()
|| operations
.values()
.all(|op| op.lock().expect(ERR_POISONED_LOCK).is_empty())
}
fn emit_results(
&self,
panicking: bool,
print: impl FnOnce(&Report),
write: impl FnOnce(&Report),
) {
if panicking {
return;
}
if !(self.emit_stdout || self.emit_file) {
return;
}
if self.is_empty() {
return;
}
let report = self.to_report();
if self.emit_stdout {
print(&report);
}
if self.emit_file {
write(&report);
}
}
}
impl Drop for Session {
#[cfg_attr(test, mutants::skip)]
fn drop(&mut self) {
self.emit_results(
thread::panicking(),
Report::print_to_stdout,
Report::write_to_target,
);
}
}
impl fmt::Display for Session {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_report())
}
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use std::panic::{RefUnwindSafe, UnwindSafe};
use super::*;
use crate::counters::register_fake_allocation;
const OPERATION: &str = "work";
const ITERATIONS: u64 = 7;
const BYTES: u64 = 91;
const ALLOCATIONS: u64 = 13;
static_assertions::assert_impl_all!(Session: Send, Sync);
static_assertions::assert_impl_all!(Session: UnwindSafe, RefUnwindSafe);
#[test]
fn empty_session_lifecycle() {
let session = Session::new().no_stdout().no_file();
assert!(session.is_empty());
_ = session.operation("unmeasured");
assert!(session.is_empty());
record(&session, 0);
assert!(session.is_empty());
record(&session, ITERATIONS);
assert!(!session.is_empty());
_ = session.operation("also_unmeasured");
assert!(!session.is_empty());
}
#[test]
fn completed_iterations_without_allocations_are_not_empty() {
let session = Session::new().no_stdout().no_file();
let operation = session.operation(OPERATION);
drop(operation.measure_thread().iterations(ITERATIONS));
assert!(!session.is_empty());
}
#[test]
fn output_destinations_are_independent() {
for (session, expected) in [
(Session::new(), (true, true)),
(Session::new().no_stdout(), (false, true)),
(Session::new().no_file(), (true, false)),
(Session::new().no_stdout().no_file(), (false, false)),
] {
record(&session, ITERATIONS);
let mut stdout = None;
let mut file = None;
session.emit_results(
false,
|report| stdout = Some(report.clone()),
|report| file = Some(report.clone()),
);
drop(session.no_stdout().no_file());
assert_eq!((stdout.is_some(), file.is_some()), expected);
for report in stdout.iter().chain(file.iter()) {
assert_recorded_work(report);
}
}
}
#[test]
fn outputs_share_a_snapshot_without_holding_session_locks() {
let session = Session::new();
record(&session, ITERATIONS);
let mut stdout = None;
let mut file = None;
session.emit_results(
false,
|report| {
stdout = Some(report.clone());
drop(session.operations.try_lock().unwrap());
record(&session, ITERATIONS);
},
|report| file = Some(report.clone()),
);
drop(session.no_stdout().no_file());
assert_recorded_work(&stdout.unwrap());
assert_recorded_work(&file.unwrap());
}
#[test]
fn unused_session_emits_nothing() {
assert_silent(Session::new(), false);
}
#[test]
fn unmeasured_operations_emit_nothing() {
let session = Session::new();
_ = session.operation(OPERATION);
assert_silent(session, false);
}
#[test]
fn zero_iterations_emit_nothing() {
let session = Session::new();
record(&session, 0);
assert_silent(session, false);
}
#[test]
fn unwinding_emits_nothing() {
let session = Session::new();
record(&session, ITERATIONS);
assert_silent(session, true);
}
fn record(session: &Session, iterations: u64) {
let operation = session.operation(OPERATION);
let _span = operation.measure_thread().iterations(iterations);
register_fake_allocation(BYTES, ALLOCATIONS);
}
fn assert_silent(session: Session, panicking: bool) {
let mut stdout = false;
let mut file = false;
session.emit_results(panicking, |_| stdout = true, |_| file = true);
drop(session.no_stdout().no_file());
assert!(!stdout);
assert!(!file);
}
fn assert_recorded_work(report: &Report) {
let operations = report.operations().collect::<Vec<_>>();
assert_eq!(operations.len(), 1);
let (name, operation) = operations.first().unwrap();
assert_eq!(*name, OPERATION);
assert_eq!(operation.total_iterations(), ITERATIONS);
assert_eq!(operation.total_bytes_allocated(), BYTES);
assert_eq!(operation.total_allocations_count(), ALLOCATIONS);
}
}