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}