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)]
191#[cfg_attr(coverage_nightly, coverage(off))]
192mod tests {
193    use super::*;
194    use crate::Session;
195    use crate::allocator::register_fake_allocation;
196
197    fn create_test_operation() -> Operation {
198        let session = Session::new();
199        session.operation("test")
200    }
201
202    #[test]
203    fn operation_new() {
204        let operation = create_test_operation();
205        assert_eq!(operation.mean(), 0);
206        assert_eq!(operation.total_iterations(), 0);
207        assert_eq!(operation.total_bytes_allocated(), 0);
208    }
209
210    #[test]
211    fn operation_add_single() {
212        let operation = create_test_operation();
213
214        // Directly test the metrics
215        {
216            let mut metrics = operation.metrics.lock().expect(ERR_POISONED_LOCK);
217            metrics.add_iterations(100, 1, 1);
218        }
219
220        assert_eq!(operation.mean(), 100);
221        assert_eq!(operation.total_iterations(), 1);
222        assert_eq!(operation.total_bytes_allocated(), 100);
223    }
224
225    #[test]
226    fn operation_add_multiple() {
227        let operation = create_test_operation();
228
229        // Directly test the metrics
230        {
231            let mut metrics = operation.metrics.lock().expect(ERR_POISONED_LOCK);
232            metrics.add_iterations(100, 1, 1); // 100 bytes, 1 allocation, 1 iteration
233            metrics.add_iterations(200, 2, 1); // 200 bytes, 2 allocations, 1 iteration  
234            metrics.add_iterations(300, 3, 1); // 300 bytes, 3 allocations, 1 iteration
235        }
236
237        assert_eq!(operation.mean(), 200); // (100 + 200 + 300) / (1 + 1 + 1) = 600 / 3 = 200
238        assert_eq!(operation.total_iterations(), 3);
239        assert_eq!(operation.total_bytes_allocated(), 600);
240    }
241
242    #[test]
243    fn operation_add_zero() {
244        let operation = create_test_operation();
245
246        // Directly test the metrics
247        {
248            let mut metrics = operation.metrics.lock().expect(ERR_POISONED_LOCK);
249            metrics.add_iterations(0, 0, 1);
250            metrics.add_iterations(0, 0, 1);
251        }
252
253        assert_eq!(operation.mean(), 0);
254        assert_eq!(operation.total_iterations(), 2);
255        assert_eq!(operation.total_bytes_allocated(), 0);
256    }
257
258    #[test]
259    fn operation_span_drop() {
260        let operation = create_test_operation();
261
262        {
263            let _span = operation.measure_thread();
264            // Simulate allocation
265            register_fake_allocation(75, 1);
266        }
267
268        assert_eq!(operation.mean(), 75);
269        assert_eq!(operation.total_iterations(), 1);
270        assert_eq!(operation.total_bytes_allocated(), 75);
271    }
272
273    #[test]
274    fn operation_multiple_spans() {
275        let operation = create_test_operation();
276
277        {
278            let _span = operation.measure_thread();
279            register_fake_allocation(100, 1);
280        }
281
282        {
283            let _span = operation.measure_thread();
284            register_fake_allocation(200, 1);
285        }
286
287        assert_eq!(operation.mean(), 150); // (100 + 200) / 2
288        assert_eq!(operation.total_iterations(), 2);
289        assert_eq!(operation.total_bytes_allocated(), 300);
290    }
291
292    #[test]
293    fn operation_thread_span_drop() {
294        let operation = create_test_operation();
295
296        {
297            let _span = operation.measure_thread();
298            register_fake_allocation(50, 1);
299        }
300
301        assert_eq!(operation.mean(), 50);
302        assert_eq!(operation.total_iterations(), 1);
303        assert_eq!(operation.total_bytes_allocated(), 50);
304    }
305
306    #[test]
307    fn operation_mixed_spans() {
308        let operation = create_test_operation();
309
310        {
311            let _span = operation.measure_thread();
312            register_fake_allocation(100, 1);
313        }
314
315        {
316            let _span = operation.measure_thread();
317            register_fake_allocation(200, 1);
318        }
319
320        assert_eq!(operation.mean(), 150); // (100 + 200) / 2
321        assert_eq!(operation.total_iterations(), 2);
322        assert_eq!(operation.total_bytes_allocated(), 300);
323    }
324
325    #[test]
326    fn operation_thread_span_no_allocation() {
327        let operation = create_test_operation();
328
329        {
330            let _span = operation.measure_thread();
331            // No allocation
332        }
333
334        assert_eq!(operation.mean(), 0);
335        assert_eq!(operation.total_iterations(), 1);
336        assert_eq!(operation.total_bytes_allocated(), 0);
337    }
338
339    #[test]
340    fn operation_batch_iterations() {
341        let operation = create_test_operation();
342
343        {
344            let _span = operation.measure_thread().iterations(10);
345            // Simulate a 1000 byte allocation that should be divided by 10 iterations
346            register_fake_allocation(1000, 10);
347        }
348
349        assert_eq!(operation.total_iterations(), 10);
350        assert_eq!(operation.total_bytes_allocated(), 1000);
351        assert_eq!(operation.mean(), 100); // 1000 / 10
352    }
353
354    #[test]
355    fn operation_drop_merges_data() {
356        let session = Session::new();
357
358        // Create and use operation
359        {
360            let operation = session.operation("test");
361
362            // Directly test the metrics
363            {
364                let mut metrics = operation.metrics.lock().expect(ERR_POISONED_LOCK);
365                metrics.add_iterations(100, 2, 5);
366            }
367            // operation is dropped here, merging data into session
368        }
369
370        // Check that session contains the data
371        let report = session.to_report();
372        assert!(!report.is_empty());
373
374        // Verify the session shows the data was merged
375        let session_display = format!("{session}");
376        println!("Actual session display: '{session_display}'");
377        assert!(session_display.contains("| test      |        100 |          2 |")); // 500 bytes / 5 iterations = 100, 10 allocations / 5 iterations = 2
378    }
379
380    #[test]
381    fn multiple_operations_concurrent() {
382        let session = Session::new();
383
384        let op1 = session.operation("test");
385        let op2 = session.operation("test");
386
387        // Directly manipulate the metrics
388        {
389            let mut metrics = op1.metrics.lock().expect(ERR_POISONED_LOCK);
390            metrics.add_iterations(100, 1, 2); // 200 bytes, 2 allocations, 2 iterations
391            metrics.add_iterations(200, 2, 3); // 600 bytes, 6 allocations, 3 iterations
392        }
393
394        // Both operations share the same data immediately since they have the same name
395        // Total: 200 + 600 = 800 bytes, 2 + 3 = 5 iterations, mean = 800 / 5 = 160 bytes
396        assert_eq!(op1.mean(), 160);
397        assert_eq!(op2.mean(), 160);
398
399        // Drop operations
400        drop(op1);
401        drop(op2);
402
403        // Session should show merged results: 800 bytes, 5 iterations = 160 bytes mean, 8 allocations / 5 iterations = 1.6 ≈ 1 (integer division)
404        let session_display = format!("{session}");
405        assert!(session_display.contains("| test      |        160 |          1 |"));
406    }
407
408    static_assertions::assert_impl_all!(Operation: Send, Sync);
409
410    #[test]
411    fn operation_display_shows_mean_bytes() {
412        let operation = create_test_operation();
413
414        // Add some data to have a non-zero mean.
415        // add_iterations(bytes_delta, count_delta, iterations) means bytes_delta * iterations total bytes.
416        {
417            let mut metrics = operation.metrics.lock().expect(ERR_POISONED_LOCK);
418            metrics.add_iterations(250, 5, 2); // 250 * 2 = 500 total bytes / 2 = 250 mean
419        }
420
421        let display_output = operation.to_string();
422        assert!(display_output.contains("bytes (mean)"));
423        assert!(display_output.contains("250")); // 500 / 2 = 250 mean bytes
424    }
425}