alloc_tracker/
session.rs

1use std::collections::HashMap;
2use std::fmt;
3use std::sync::{Arc, Mutex};
4
5use crate::{ERR_POISONED_LOCK, Operation, OperationMetrics, Report};
6
7/// Manages allocation tracking session state and contains operations.
8///
9/// # Examples
10///
11/// ```rust
12/// use alloc_tracker::{Allocator, Session};
13///
14/// #[global_allocator]
15/// static ALLOCATOR: Allocator<std::alloc::System> = Allocator::system();
16///
17/// let session = Session::new();
18/// let string_op = session.operation("do_stuff_with_strings");
19///
20/// {
21///     let _span = string_op.measure_process().iterations(3);
22///     for _ in 0..3 {
23///         let _data = String::from("example string allocation");
24///     }
25/// }
26///
27/// // Output statistics of all operations to console.
28/// // Using print_to_stdout() here is important in benchmarks because it will
29/// // print nothing if no spans were recorded, not even an empty line, which can
30/// // be functionally critical for benchmark harness behavior.
31/// session.print_to_stdout();
32/// ```
33#[derive(Debug)]
34pub struct Session {
35    operations: Arc<Mutex<HashMap<String, Arc<Mutex<OperationMetrics>>>>>,
36}
37
38impl Session {
39    /// Creates a new allocation tracking session.
40    ///
41    /// # Examples
42    ///
43    /// ```rust
44    /// use alloc_tracker::{Allocator, Session};
45    ///
46    /// #[global_allocator]
47    /// static ALLOCATOR: Allocator<std::alloc::System> = Allocator::system();
48    ///
49    /// let session = Session::new();
50    /// // Allocation tracking is now enabled
51    /// // Session will disable tracking when dropped
52    /// ```
53    #[expect(
54        clippy::new_without_default,
55        reason = "to avoid ambiguity with the notion of a 'default session' that is not actually a default session"
56    )]
57    #[must_use]
58    pub fn new() -> Self {
59        Self {
60            operations: Arc::new(Mutex::new(HashMap::new())),
61        }
62    }
63
64    /// Creates or retrieves an operation with the given name.
65    ///
66    /// If an operation with the given name already exists, its existing statistics are preserved
67    /// and any consecutive or concurrent use of multiple such `Operation` instances will merge
68    /// the data sets.
69    ///
70    /// # Examples
71    ///
72    /// ```rust
73    /// use alloc_tracker::{Allocator, Session};
74    ///
75    /// #[global_allocator]
76    /// static ALLOCATOR: Allocator<std::alloc::System> = Allocator::system();
77    ///
78    /// let session = Session::new();
79    /// let string_op = session.operation("string_operations");
80    ///
81    /// {
82    ///     let _span = string_op.measure_process().iterations(3);
83    ///     for _ in 0..3 {
84    ///         let _s = String::from("test"); // This allocation will be tracked
85    ///     }
86    /// }
87    /// ```
88    pub fn operation(&self, name: impl Into<String>) -> Operation {
89        let name = name.into();
90
91        // Ensure the operation exists in the shared data
92        let operation_data = {
93            let mut operations = self.operations.lock().expect(ERR_POISONED_LOCK);
94            Arc::clone(
95                operations
96                    .entry(name.clone())
97                    .or_insert_with(|| Arc::new(Mutex::new(OperationMetrics::default()))),
98            )
99        };
100
101        Operation::new(name, operation_data)
102    }
103
104    /// Creates a thread-safe report from this session.
105    ///
106    /// The report contains a snapshot of all memory allocation statistics captured by this session.
107    ///
108    /// # Examples
109    ///
110    /// ```
111    /// use alloc_tracker::{Allocator, Session};
112    ///
113    /// #[global_allocator]
114    /// static ALLOCATOR: Allocator<std::alloc::System> = Allocator::system();
115    ///
116    /// let session = Session::new();
117    /// let operation = session.operation("test_work");
118    /// {
119    ///     let _span = operation.measure_process();
120    ///     let _data = vec![1, 2, 3]; // This allocates memory
121    /// }
122    ///
123    /// let report = session.to_report();
124    /// report.print_to_stdout();
125    /// ```
126    #[must_use]
127    pub fn to_report(&self) -> Report {
128        let operations = self.operations.lock().expect(ERR_POISONED_LOCK);
129        let operation_data: HashMap<String, OperationMetrics> = operations
130            .iter()
131            .map(|(name, data_ref)| {
132                (
133                    name.clone(),
134                    data_ref.lock().expect(ERR_POISONED_LOCK).clone(),
135                )
136            })
137            .collect();
138
139        Report::from_operation_data(&operation_data)
140    }
141
142    /// Prints the allocation statistics of all operations to stdout.
143    ///
144    /// This is a convenience method equivalent to `self.to_report().print_to_stdout()`.
145    /// Prints nothing if no spans were captured. This may indicate that the session
146    /// was part of a "list available benchmarks" probe run instead of some real activity,
147    /// in which case printing anything might violate the output protocol the tool is speaking.
148    #[cfg_attr(test, mutants::skip)] // Too difficult to test stdout output reliably - manually tested.
149    pub fn print_to_stdout(&self) {
150        self.to_report().print_to_stdout();
151    }
152
153    /// Whether there is any recorded activity in this session.
154    #[must_use]
155    pub fn is_empty(&self) -> bool {
156        let operations = self.operations.lock().expect(ERR_POISONED_LOCK);
157        operations.is_empty()
158            || operations
159                .values()
160                .all(|op| op.lock().expect(ERR_POISONED_LOCK).total_iterations == 0)
161    }
162}
163
164impl fmt::Display for Session {
165    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
166        // Delegate to Report's Display implementation for consistency
167        write!(f, "{}", self.to_report())
168    }
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174
175    // The type is thread-safe.
176    static_assertions::assert_impl_all!(Session: Send, Sync);
177}