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
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
//! 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 the number of bytes allocated, the count of allocations and the
//! peak amount of memory held at any single moment.
//!
//! 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()`](ThreadSpan::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_thread().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()`](ThreadSpan::iterations) on the span before
//! it is dropped. Failure to do so will result in a panic.
//!
//! # Measurement scope
//!
//! [`Operation::measure_thread`] observes only the calling thread's allocator
//! activity and is the right choice whenever the measured work stays on that
//! thread, which covers most benchmarks.
//!
//! Work spread across threads is still best measured this way when those threads can
//! be instrumented and each processes iterations of its own: every worker opens a span
//! naming the same operation, counting the iterations it completed itself. An operation
//! combines its spans as repeated samples of one per-iteration cost rather than adding
//! them up, so threads that collaborate on every iteration need
//! [`Operation::measure_process`] instead.
//!
//! [`Operation::measure_process`] observes every thread in the process. Reach for it
//! when the measured work is performed by threads that cannot be instrumented,
//! accepting that it costs more to capture, that its totals are approximate, that it
//! attributes concurrent allocation by unrelated threads to the operation, and that it
//! withholds peak memory from the whole operation.
//!
//! # Metrics
//!
//! Every metric is a per-iteration figure, estimated across an operation's spans
//! with each span weighted by the square of its iteration count. Benchmark harnesses
//! run short warmup batches before settling into long steady-state ones, and the
//! weighting makes those short batches contribute almost nothing.
//!
//! Bytes per iteration and allocations per iteration describe the cost of performing
//! the operation once.
//!
//! Peak bytes is the memory one iteration holds at its high-water moment, measured
//! relative to the memory already outstanding when a span began. It answers how much
//! memory has to exist at once, which the cumulative byte count cannot: an operation
//! that takes and releases a buffer a thousand times and one that holds a thousand
//! buffers allocate the same total.
//!
//! The estimate assumes every iteration within a measured batch reaches the same peak.
//! An operation that instead accumulates memory across the iterations of a batch
//! violates that assumption. Nothing detects the violation, so a figure is still
//! reported, but it grows with the iteration counts the harness chose and is not
//! comparable between runs.
//!
//! Peak bytes requires that every span of the operation could measure it, so an
//! operation containing even one [`ProcessSpan`] reports no peak rather than one that
//! describes only part of the work. Spans that measured a peak but covered no
//! iterations leave the rate undefined, which also leaves nothing to report.
//!
//! Span watermarks are averaged, not summed, so an operation measured concurrently on
//! several threads reports what a typical one of them held rather than the total held
//! at once. It counts memory requested through the allocator as seen at the boundaries
//! of allocator calls, so memory an allocator transiently holds inside a call does not
//! appear in it.
//!
//! Because the measurement is relative to the level at span entry, an operation that
//! releases memory it did not allocate creates headroom that masks its own later
//! allocations. Peak bytes does not describe operations whose main effect is freeing
//! pre-existing memory. Deallocation is attributed to the thread that performs it, so
//! a [`ThreadSpan`] covering work that allocates on one thread and frees on another
//! does not describe how much memory is live in the process.
//!
//! # Human-readable summary
//!
//! When a [`Session`] is dropped it prints a table to stdout, one row per operation:
//!
//! ```text
//! Allocation statistics:
//!
//! | Operation | Bytes/iter | Allocations/iter | Peak bytes |
//! |-----------------|------------|------------------|------------|
//! | allocate_buffer | 1024 | 3 | 1024 |
//! | build_map | 64 | 1 | n/a |
//! ```
//!
//! # 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_thread();
//!
//! 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_thread().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 *;
pub use *;