1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
//! Memory allocation tracking utilities for benchmarks and performance analysis.
//!
//! This package provides utilities to track memory allocations during code execution,
//! enabling analysis of allocation patterns in benchmarks and performance tests.
//! The tracker reports both the number of bytes allocated and the count of allocations.
//!
//! The core functionality includes:
//! - [`Allocator`] - A Rust memory allocator wrapper that enables allocation tracking
//! - [`Session`] - Configures allocation tracking and provides access to tracking data
//! - [`Report`] - Thread-safe memory allocation statistics that can be merged and processed independently
//! - [`ProcessSpan`] - Tracks process-wide memory allocation changes over a time period
//! - [`ThreadSpan`] - Tracks thread-local memory allocation changes over a time period
//! - [`Operation`] - Measures per-iteration memory allocation of a repeated operation
//!
//! Additionally, when the `panic_on_next_alloc` feature is enabled:
//!
//! This package is not meant for use in production, serving only as a development tool.
//!
//! # Features
//! unexpected allocations. This feature adds some overhead to allocations, so it is optional.
//!
//! # Benchmarking
//!
//! The typical pattern is to drive measurement from Criterion's [`iter_custom()`]
//! function, feeding its chosen iteration count into
//! [`iterations()`](ProcessSpan::iterations) so each recorded span covers a whole
//! sample rather than a single iteration:
//!
//! ```no_run
//! use std::hint::black_box;
//! use std::time::Instant;
//!
//! use alloc_tracker::{Allocator, Session};
//! use criterion::Criterion;
//!
//! #[global_allocator]
//! static ALLOCATOR: Allocator<std::alloc::System> = Allocator::system();
//!
//! fn bench(c: &mut Criterion) {
//! let session = Session::new();
//!
//! let operation = session.operation("my_operation");
//! c.bench_function("my_operation", |b| {
//! b.iter_custom(|iters| {
//! let start = Instant::now();
//! let _span = operation.measure_process().iterations(iters);
//!
//! for _ in 0..iters {
//! black_box(vec![1, 2, 3, 4, 5]);
//! }
//!
//! start.elapsed()
//! });
//! });
//!
//! // When `session` is dropped, the recorded statistics are printed to
//! // stdout and written to the Cargo target directory as JSON.
//! }
//! ```
//!
//! You **must** call [`iterations()`](ProcessSpan::iterations) on the span before
//! it is dropped. Failure to do so will result in a panic.
//!
//! # Human-readable summary
//!
//! When a [`Session`] is dropped it prints a table of per-iteration figures to
//! stdout, one row per operation:
//!
//! ```text
//! Allocation statistics:
//!
//! | Operation | Bytes/iter | Allocations/iter |
//! |-----------------|------------|------------------|
//! | allocate_buffer | 1024 | 3 |
//! | build_map | 64 | 1 |
//! ```
//!
//! # Machine-readable output
//!
//! Dropping a [`Session`] also writes JSON files (one per operation) into the
//! Cargo target directory at `target/alloc_tracker/<operation>.json`, with
//! operation names sanitized to be filesystem-safe.
//!
//! Both outputs are produced automatically, so a typical benchmark only needs to
//! create a session and record work.
//!
//! # Measuring a variable amount of work
//!
//! You do not need to specify the iteration count up front, as long as it is
//! provided before the span is dropped.
//!
//! This allows you to measure work whose extent is not known at the start.
//!
//! ```
//! use std::hint::black_box;
//!
//! use alloc_tracker::{Allocator, Session};
//!
//! #[global_allocator]
//! static ALLOCATOR: Allocator<std::alloc::System> = Allocator::system();
//!
//! fn main() {
//! let session = Session::new();
//! # let session = session.no_stdout().no_file();
//! let operation = session.operation("drain_queue");
//!
//! let span = operation.measure_process();
//!
//! let mut processed = 0_u64;
//!
//! while let Some(item) = get_next_item() {
//! black_box(item.process());
//! processed += 1;
//! }
//!
//! drop(span.iterations(processed));
//! }
//! # fn get_next_item() -> Option<Item> {
//! # use std::sync::atomic::{AtomicU32, Ordering};
//! # static REMAINING: AtomicU32 = AtomicU32::new(3);
//! # (REMAINING.fetch_sub(1, Ordering::Relaxed) > 0).then_some(Item)
//! # }
//! # struct Item;
//! # impl Item { fn process(&self) -> Vec<u8> { vec![0_u8; 16] } }
//! ```
//!
//! # Overhead
//!
//! Capturing a single measurement by calling `measure_xyz()` incurs a small
//! overhead (on the order of tens of nanoseconds on an arbitrary sample machine),
//! and the tracking logic slightly perturbs allocator activity.
//!
//! It is crucial that you measure multiple iterations in the same sample to
//! amortize this overhead. This is the purpose of the [`iter_custom()`] pattern
//! described above.
//!
//! Operating without batching, by measuring individual iterations, is only viable
//! for macrobenchmarks for which a single iteration is a large unit of work (e.g.
//! an HTTP request).
//!
//! # Session management
//!
//! Multiple [`Session`] instances can be used concurrently as they track memory allocation
//! independently. Each session maintains its own set of operations and statistics.
//!
//! While [`Session`] itself is single-threaded, reports from sessions can be converted to
//! thread-safe [`Report`] instances and sent to other threads for processing:
//!
//! ```
//! use std::thread;
//!
//! use alloc_tracker::{Allocator, Session};
//!
//! #[global_allocator]
//! static ALLOCATOR: Allocator<std::alloc::System> = Allocator::system();
//!
//! # fn main() {
//! let session = Session::new();
//! # let session = session.no_stdout().no_file();
//! {
//! let operation = session.operation("work");
//! let _span = operation.measure_process().iterations(1);
//! let _data = vec![1, 2, 3]; // Some allocation work
//! }
//!
//! let report = session.to_report();
//!
//! // Reports are `Send`, so they can be moved to another thread for processing.
//! let total_bytes: u64 = thread::spawn(move || {
//! report
//! .operations()
//! .map(|(_, op)| op.total_bytes_allocated())
//! .sum()
//! })
//! .join()
//! .unwrap();
//!
//! println!("Captured {total_bytes} bytes across all operations");
//! # }
//! ```
//!
//! # Miri compatibility
//!
//! Miri replaces the global allocator with its own logic, so you cannot execute code that uses
//! this package under Miri.
//!
//! [`iter_custom()`]: https://docs.rs/criterion/latest/criterion/struct.Bencher.html#method.iter_custom
pub use *;
pub use *;
pub use *;
pub use *;
pub use *;
pub use *;
pub use *;
pub use *;