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}