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}