alloc_tracker/
operation.rs

1//! Mean allocation tracking.
2
3use std::fmt;
4use std::sync::{Arc, Mutex};
5
6use crate::{ERR_POISONED_LOCK, OperationMetrics, ProcessSpan, ThreadSpan};
7
8/// A measurement handle for tracking mean memory allocation per operation across multiple iterations.
9///
10/// This utility is particularly useful for benchmarking scenarios where you want
11/// to understand the mean memory footprint and allocation behavior of repeated operations.
12/// It tracks both the number of bytes allocated and the count of allocations.
13/// Operations share data with their parent session via reference counting, and data is
14/// merged when the operation is dropped.
15///
16/// Multiple operations with the same name can be created concurrently.
17///
18/// # Examples
19///
20/// ```
21/// use alloc_tracker::{Allocator, Operation, Session};
22///
23/// #[global_allocator]
24/// static ALLOCATOR: Allocator<std::alloc::System> = Allocator::system();
25///
26/// let session = Session::new();
27/// let mean_calc = session.operation("string_allocations");
28///
29/// // Simulate multiple operations
30/// for i in 0..5 {
31///     let _span = mean_calc.measure_process();
32///     let _data = vec![0; i + 1]; // Allocate different amounts
33/// }
34///
35/// let mean_bytes = mean_calc.mean();
36/// println!("Mean allocation: {} bytes per operation", mean_bytes);
37/// ```
38#[derive(Debug)]
39pub struct Operation {
40    metrics: Arc<Mutex<OperationMetrics>>,
41}
42
43impl Operation {
44    #[must_use]
45    pub(crate) fn new(_name: String, operation_data: Arc<Mutex<OperationMetrics>>) -> Self {
46        Self {
47            metrics: operation_data,
48        }
49    }
50
51    /// Returns a clone of the operation metrics for use by spans.
52    #[must_use]
53    pub(crate) fn metrics(&self) -> Arc<Mutex<OperationMetrics>> {
54        Arc::clone(&self.metrics)
55    }
56
57    /// Creates a span that tracks thread allocations from creation until it is dropped.
58    ///
59    /// This method tracks allocations made by the current thread only.
60    /// Use this when you want to measure allocations for single-threaded operations
61    /// or when you want to track per-thread allocation usage.
62    ///
63    /// The span defaults to 1 iteration but can be changed using the `iterations()` method.
64    ///
65    /// # Examples
66    ///
67    /// ```
68    /// use alloc_tracker::{Allocator, Session};
69    ///
70    /// #[global_allocator]
71    /// static ALLOCATOR: Allocator<std::alloc::System> = Allocator::system();
72    ///
73    /// let session = Session::new();
74    /// let operation = session.operation("thread_work");
75    /// {
76    ///     let _span = operation.measure_thread();
77    ///     // Perform some allocation in this thread
78    ///     let _data = vec![1, 2, 3, 4, 5];
79    /// } // Thread allocations are tracked for 1 iteration
80    /// ```
81    ///
82    /// For batch operations:
83    /// ```
84    /// use alloc_tracker::{Allocator, Session};
85    ///
86    /// #[global_allocator]
87    /// static ALLOCATOR: Allocator<std::alloc::System> = Allocator::system();
88    ///
89    /// let session = Session::new();
90    /// let operation = session.operation("batch_work");
91    /// {
92    ///     let _span = operation.measure_thread().iterations(1000);
93    ///     for _ in 0..1000 {
94    ///         // Perform the same operation 1000 times
95    ///         let _data = vec![42];
96    ///     }
97    /// } // Total allocation is measured once and divided by 1000
98    /// ```
99    pub fn measure_thread(&self) -> ThreadSpan {
100        ThreadSpan::new(self, 1)
101    }
102
103    /// Creates a span that tracks process allocations from creation until it is dropped.
104    ///
105    /// This method tracks allocations made by the entire process (all threads).
106    /// Use this when you want to measure total allocations including multi-threaded
107    /// operations or when you want to track overall process allocation usage.
108    ///
109    /// The span defaults to 1 iteration but can be changed using the `iterations()` method.
110    ///
111    /// # Examples
112    ///
113    /// ```
114    /// use alloc_tracker::{Allocator, Session};
115    ///
116    /// #[global_allocator]
117    /// static ALLOCATOR: Allocator<std::alloc::System> = Allocator::system();
118    ///
119    /// let session = Session::new();
120    /// let operation = session.operation("process_work");
121    /// {
122    ///     let _span = operation.measure_process();
123    ///     // Perform some allocation that might span threads
124    ///     let _data = vec![1, 2, 3, 4, 5];
125    /// } // Total process allocations are tracked for 1 iteration
126    /// ```
127    ///
128    /// For batch operations:
129    /// ```
130    /// use alloc_tracker::{Allocator, Session};
131    ///
132    /// #[global_allocator]
133    /// static ALLOCATOR: Allocator<std::alloc::System> = Allocator::system();
134    ///
135    /// let session = Session::new();
136    /// let operation = session.operation("batch_work");
137    /// {
138    ///     let _span = operation.measure_process().iterations(1000);
139    ///     for _ in 0..1000 {
140    ///         // Perform the same operation 1000 times
141    ///         let _data = vec![42];
142    ///     }
143    /// } // Total allocation is measured once and divided by 1000
144    /// ```
145    pub fn measure_process(&self) -> ProcessSpan {
146        ProcessSpan::new(self, 1)
147    }
148
149    /// Calculates the mean bytes allocated per iteration.
150    ///
151    /// Returns 0 if no iterations have been recorded.
152    #[expect(clippy::integer_division, reason = "we accept loss of precision")]
153    #[expect(
154        clippy::arithmetic_side_effects,
155        reason = "division by zero excluded via if-else"
156    )]
157    #[must_use]
158    pub fn mean(&self) -> u64 {
159        let data = self.metrics.lock().expect(ERR_POISONED_LOCK);
160        if data.total_iterations == 0 {
161            0
162        } else {
163            data.total_bytes_allocated / data.total_iterations
164        }
165    }
166
167    /// Returns the total number of iterations recorded.
168    #[must_use]
169    #[allow(dead_code, reason = "Used in tests")]
170    #[cfg(test)]
171    fn total_iterations(&self) -> u64 {
172        let data = self.metrics.lock().expect(ERR_POISONED_LOCK);
173        data.total_iterations
174    }
175
176    /// Returns the total bytes allocated across all iterations.
177    #[must_use]
178    pub fn total_bytes_allocated(&self) -> u64 {
179        let data = self.metrics.lock().expect("ERR_POISONED_LOCK");
180        data.total_bytes_allocated
181    }
182}
183
184impl fmt::Display for Operation {
185    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
186        write!(f, "{} bytes (mean)", self.mean())
187    }
188}
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193    use crate::Session;
194    use crate::allocator::register_fake_allocation;
195
196    fn create_test_operation() -> Operation {
197        let session = Session::new();
198        session.operation("test")
199    }
200
201    #[test]
202    fn operation_new() {
203        let operation = create_test_operation();
204        assert_eq!(operation.mean(), 0);
205        assert_eq!(operation.total_iterations(), 0);
206        assert_eq!(operation.total_bytes_allocated(), 0);
207    }
208
209    #[test]
210    fn operation_add_single() {
211        let operation = create_test_operation();
212
213        // Directly test the metrics
214        {
215            let mut metrics = operation.metrics.lock().expect(ERR_POISONED_LOCK);
216            metrics.add_iterations(100, 1, 1);
217        }
218
219        assert_eq!(operation.mean(), 100);
220        assert_eq!(operation.total_iterations(), 1);
221        assert_eq!(operation.total_bytes_allocated(), 100);
222    }
223
224    #[test]
225    fn operation_add_multiple() {
226        let operation = create_test_operation();
227
228        // Directly test the metrics
229        {
230            let mut metrics = operation.metrics.lock().expect(ERR_POISONED_LOCK);
231            metrics.add_iterations(100, 1, 1); // 100 bytes, 1 allocation, 1 iteration
232            metrics.add_iterations(200, 2, 1); // 200 bytes, 2 allocations, 1 iteration  
233            metrics.add_iterations(300, 3, 1); // 300 bytes, 3 allocations, 1 iteration
234        }
235
236        assert_eq!(operation.mean(), 200); // (100 + 200 + 300) / (1 + 1 + 1) = 600 / 3 = 200
237        assert_eq!(operation.total_iterations(), 3);
238        assert_eq!(operation.total_bytes_allocated(), 600);
239    }
240
241    #[test]
242    fn operation_add_zero() {
243        let operation = create_test_operation();
244
245        // Directly test the metrics
246        {
247            let mut metrics = operation.metrics.lock().expect(ERR_POISONED_LOCK);
248            metrics.add_iterations(0, 0, 1);
249            metrics.add_iterations(0, 0, 1);
250        }
251
252        assert_eq!(operation.mean(), 0);
253        assert_eq!(operation.total_iterations(), 2);
254        assert_eq!(operation.total_bytes_allocated(), 0);
255    }
256
257    #[test]
258    fn operation_span_drop() {
259        let operation = create_test_operation();
260
261        {
262            let _span = operation.measure_thread();
263            // Simulate allocation
264            register_fake_allocation(75, 1);
265        }
266
267        assert_eq!(operation.mean(), 75);
268        assert_eq!(operation.total_iterations(), 1);
269        assert_eq!(operation.total_bytes_allocated(), 75);
270    }
271
272    #[test]
273    fn operation_multiple_spans() {
274        let operation = create_test_operation();
275
276        {
277            let _span = operation.measure_thread();
278            register_fake_allocation(100, 1);
279        }
280
281        {
282            let _span = operation.measure_thread();
283            register_fake_allocation(200, 1);
284        }
285
286        assert_eq!(operation.mean(), 150); // (100 + 200) / 2
287        assert_eq!(operation.total_iterations(), 2);
288        assert_eq!(operation.total_bytes_allocated(), 300);
289    }
290
291    #[test]
292    fn operation_thread_span_drop() {
293        let operation = create_test_operation();
294
295        {
296            let _span = operation.measure_thread();
297            register_fake_allocation(50, 1);
298        }
299
300        assert_eq!(operation.mean(), 50);
301        assert_eq!(operation.total_iterations(), 1);
302        assert_eq!(operation.total_bytes_allocated(), 50);
303    }
304
305    #[test]
306    fn operation_mixed_spans() {
307        let operation = create_test_operation();
308
309        {
310            let _span = operation.measure_thread();
311            register_fake_allocation(100, 1);
312        }
313
314        {
315            let _span = operation.measure_thread();
316            register_fake_allocation(200, 1);
317        }
318
319        assert_eq!(operation.mean(), 150); // (100 + 200) / 2
320        assert_eq!(operation.total_iterations(), 2);
321        assert_eq!(operation.total_bytes_allocated(), 300);
322    }
323
324    #[test]
325    fn operation_thread_span_no_allocation() {
326        let operation = create_test_operation();
327
328        {
329            let _span = operation.measure_thread();
330            // No allocation
331        }
332
333        assert_eq!(operation.mean(), 0);
334        assert_eq!(operation.total_iterations(), 1);
335        assert_eq!(operation.total_bytes_allocated(), 0);
336    }
337
338    #[test]
339    fn operation_batch_iterations() {
340        let operation = create_test_operation();
341
342        {
343            let _span = operation.measure_thread().iterations(10);
344            // Simulate a 1000 byte allocation that should be divided by 10 iterations
345            register_fake_allocation(1000, 10);
346        }
347
348        assert_eq!(operation.total_iterations(), 10);
349        assert_eq!(operation.total_bytes_allocated(), 1000);
350        assert_eq!(operation.mean(), 100); // 1000 / 10
351    }
352
353    #[test]
354    fn operation_drop_merges_data() {
355        let session = Session::new();
356
357        // Create and use operation
358        {
359            let operation = session.operation("test");
360
361            // Directly test the metrics
362            {
363                let mut metrics = operation.metrics.lock().expect(ERR_POISONED_LOCK);
364                metrics.add_iterations(100, 2, 5);
365            }
366            // operation is dropped here, merging data into session
367        }
368
369        // Check that session contains the data
370        let report = session.to_report();
371        assert!(!report.is_empty());
372
373        // Verify the session shows the data was merged
374        let session_display = format!("{session}");
375        println!("Actual session display: '{session_display}'");
376        assert!(session_display.contains("| test      |        100 |          2 |")); // 500 bytes / 5 iterations = 100, 10 allocations / 5 iterations = 2
377    }
378
379    #[test]
380    fn multiple_operations_concurrent() {
381        let session = Session::new();
382
383        let op1 = session.operation("test");
384        let op2 = session.operation("test");
385
386        // Directly manipulate the metrics
387        {
388            let mut metrics = op1.metrics.lock().expect(ERR_POISONED_LOCK);
389            metrics.add_iterations(100, 1, 2); // 200 bytes, 2 allocations, 2 iterations
390            metrics.add_iterations(200, 2, 3); // 600 bytes, 6 allocations, 3 iterations
391        }
392
393        // Both operations share the same data immediately since they have the same name
394        // Total: 200 + 600 = 800 bytes, 2 + 3 = 5 iterations, mean = 800 / 5 = 160 bytes
395        assert_eq!(op1.mean(), 160);
396        assert_eq!(op2.mean(), 160);
397
398        // Drop operations
399        drop(op1);
400        drop(op2);
401
402        // Session should show merged results: 800 bytes, 5 iterations = 160 bytes mean, 8 allocations / 5 iterations = 1.6 ≈ 1 (integer division)
403        let session_display = format!("{session}");
404        assert!(session_display.contains("| test      |        160 |          1 |"));
405    }
406
407    static_assertions::assert_impl_all!(Operation: Send, Sync);
408}