Skip to main content

pprof_alloc/
lib.rs

1//! Allocation profiling and Linux memory telemetry for Rust services.
2//!
3//! `pprof-alloc` provides a [`GlobalAlloc`] wrapper that can sample allocation
4//! stack traces and export them as gzipped pprof heap profiles. It also exposes
5//! Linux memory collectors for allocator state, cgroup v2 memory accounting, and
6//! `/proc/self/smaps_rollup` process residency.
7//!
8//! The crate is intended to be embedded in binaries that expose their own debug
9//! or metrics endpoint. Use [`PprofAlloc`] as the process global allocator, then
10//! call [`generate_pprof`] or [`snapshot`] from your application surface.
11//!
12//! [`GlobalAlloc`]: std::alloc::GlobalAlloc
13
14pub mod allocator;
15mod pprof;
16pub mod stats;
17mod trace;
18
19use crate::pprof::{StackProfile, WeightedStack};
20use crate::trace::HashedBacktrace;
21use dashmap::DashMap;
22use serde::Serialize;
23use std::alloc::{GlobalAlloc, Layout, System};
24use std::cell::Cell;
25use std::sync::atomic::{AtomicU8, AtomicUsize, Ordering};
26use std::time::{SystemTime, UNIX_EPOCH};
27
28pub use crate::trace::CaptureMode;
29
30/// Default average number of allocated bytes between recorded pprof samples.
31///
32/// This matches Go's default heap profiling rate: one sampled allocation per
33/// 512 KiB of allocated bytes on average. A rate of `1` records every
34/// allocation, while `0` disables pprof stack recording.
35pub const DEFAULT_PPROF_SAMPLE_RATE: usize = 512 * 1024;
36
37/// Environment variable read by [`PprofAlloc::with_pprof_sample_rate_from_env`].
38///
39/// The value must be an unsigned integer byte rate. Missing or invalid values
40/// use the default passed to `with_pprof_sample_rate_from_env`.
41pub const PPROF_SAMPLE_RATE_ENV: &str = "PPROF_ALLOC_SAMPLE_RATE";
42
43const PPROF_SAMPLE_RATE_ENV_CSTR: &[u8] = b"PPROF_ALLOC_SAMPLE_RATE\0";
44const MAX_FAST_EXP_RAND_MEAN: usize = 0x7000000;
45const RANDOM_BIT_COUNT: u32 = 26;
46const ENV_SAMPLE_RATE_UNINITIALIZED: u8 = 0;
47const ENV_SAMPLE_RATE_SET: u8 = 1;
48const ENV_SAMPLE_RATE_UNSET: u8 = 2;
49
50/// Global allocator wrapper that can collect allocation counters and pprof heap profiles.
51///
52/// Use this as a `#[global_allocator]` around [`std::alloc::System`] or another
53/// allocator implementing [`GlobalAlloc`]. Profiling and coarse allocation
54/// counters are opt-in through the builder methods.
55pub struct PprofAlloc<A = System> {
56	inner: A,
57	/// Enable profiling support
58	pprof: bool,
59	/// Enable coarse grained stats
60	stats: bool,
61	/// Average bytes between pprof samples. 0 disables pprof, 1 records everything.
62	pprof_sample_rate: usize,
63	/// Read the pprof sample rate from PPROF_ALLOC_SAMPLE_RATE at runtime.
64	pprof_sample_rate_from_env: bool,
65}
66
67#[derive(Clone)]
68struct AllocationRecord {
69	size: usize,
70	trace: HashedBacktrace,
71}
72
73struct HeapSampleValues {
74	alloc_objects: i64,
75	alloc_space: i64,
76	inuse_objects: i64,
77	inuse_space: i64,
78}
79
80impl HeapSampleValues {
81	fn from_allocations(stats: &stats::Allocations, sample_rate: usize) -> Self {
82		let (alloc_objects, alloc_space) =
83			scale_heap_sample(stats.allocations, stats.allocated, sample_rate);
84		let (inuse_objects, inuse_space) = scale_heap_sample(
85			stats.in_use_allocations(),
86			stats.in_use_bytes(),
87			sample_rate,
88		);
89		Self {
90			alloc_objects,
91			alloc_space,
92			inuse_objects,
93			inuse_space,
94		}
95	}
96}
97
98fn saturating_i64(value: u64) -> i64 {
99	value.min(i64::MAX as u64) as i64
100}
101
102fn scale_heap_sample(count: u64, size: u64, sample_rate: usize) -> (i64, i64) {
103	if count == 0 || size == 0 {
104		return (0, 0);
105	}
106
107	if sample_rate <= 1 {
108		return (saturating_i64(count), saturating_i64(size));
109	}
110
111	let average_size = size as f64 / count as f64;
112	let probability = -(-average_size / sample_rate as f64).exp_m1();
113	if probability <= 0.0 {
114		return (saturating_i64(count), saturating_i64(size));
115	}
116	let scale = 1.0 / probability;
117	(
118		(count as f64 * scale).min(i64::MAX as f64) as i64,
119		(size as f64 * scale).min(i64::MAX as f64) as i64,
120	)
121}
122
123#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
124/// Summary of the currently recorded pprof allocation profile.
125///
126/// Values are scaled according to the active pprof sample rate, so they are
127/// estimates when sampling is enabled.
128pub struct PprofSummary {
129	/// Number of distinct stack traces recorded in the profile.
130	pub total_stacks: u64,
131	/// Number of distinct stack traces with estimated live bytes greater than zero.
132	pub live_stacks: u64,
133	/// Estimated cumulative allocated bytes across all recorded stacks.
134	pub alloc_space_bytes: u64,
135	/// Estimated currently live bytes across all recorded stacks.
136	pub inuse_space_bytes: u64,
137	/// Estimated cumulative allocation count across all recorded stacks.
138	pub alloc_objects: u64,
139	/// Estimated currently live allocation count across all recorded stacks.
140	pub inuse_objects: u64,
141}
142
143#[derive(Clone, Debug, Serialize)]
144/// Best-effort snapshot of allocation and memory state for the current process created by `snapshot()`.
145///
146/// Snapshot collection never fails as a whole. Optional fields are `None` when
147/// the corresponding operating-system or allocator probe is unavailable.
148pub struct MemorySnapshot {
149	/// Wall-clock capture time as milliseconds since the Unix epoch.
150	pub captured_at_unix_ms: u64,
151	/// Stack capture implementation compiled into this build.
152	pub capture_mode: CaptureMode,
153	/// Coarse process-wide counters from [`allocation_stats`].
154	pub allocation_stats: stats::Allocations,
155	/// Summary of recorded pprof stack-attributed allocation data.
156	pub pprof: PprofSummary,
157	/// Active allocator identity and allocator-specific memory stats, if available.
158	pub allocator: allocator::AllocatorSnapshot,
159	/// cgroup v2 memory stats for this process, when available.
160	pub cgroup: Option<stats::cgroups::MemoryStat>,
161	/// `/proc/self/smaps_rollup` memory stats for this process, when available.
162	pub smaps: Option<stats::smaps::ProcessStats>,
163}
164
165impl Default for PprofAlloc<System> {
166	fn default() -> Self {
167		Self::new()
168	}
169}
170
171impl PprofAlloc<System> {
172	/// Create a wrapper around [`std::alloc::System`] with profiling disabled.
173	///
174	/// Use [`Self::with_pprof`], [`Self::with_pprof_sample_rate`], or
175	/// [`Self::with_stats`] to enable collection.
176	pub const fn new() -> Self {
177		Self::from_allocator(System)
178	}
179}
180
181impl<A> PprofAlloc<A> {
182	/// Create a wrapper around a custom allocator with profiling disabled.
183	pub const fn from_allocator(inner: A) -> Self {
184		PprofAlloc {
185			inner,
186			pprof: false,
187			stats: false,
188			pprof_sample_rate: DEFAULT_PPROF_SAMPLE_RATE,
189			pprof_sample_rate_from_env: false,
190		}
191	}
192
193	/// Enable sampled pprof stack profiling using [`DEFAULT_PPROF_SAMPLE_RATE`].
194	pub const fn with_pprof(mut self) -> Self {
195		self.pprof = true;
196		self
197	}
198
199	/// Enable sampled pprof stack profiling with an explicit byte sample rate.
200	///
201	/// A rate of `1` records every allocation. A rate of `0` disables pprof
202	/// stack recording while still allowing other enabled collectors to run.
203	pub const fn with_pprof_sample_rate(mut self, bytes: usize) -> Self {
204		self.pprof = true;
205		self.pprof_sample_rate = bytes;
206		self.pprof_sample_rate_from_env = false;
207		self
208	}
209
210	/// Enable pprof stack profiling with the sample rate read from the environment.
211	///
212	/// [`PPROF_SAMPLE_RATE_ENV`] is read lazily on the first profiled allocation.
213	/// If the variable is missing or invalid, `default_rate` is used.
214	pub const fn with_pprof_sample_rate_from_env(mut self, default_rate: usize) -> Self {
215		self.pprof = true;
216		self.pprof_sample_rate = default_rate;
217		self.pprof_sample_rate_from_env = true;
218		self
219	}
220
221	/// Enable coarse process-wide allocation and free counters.
222	pub const fn with_stats(mut self) -> Self {
223		self.stats = true;
224		self
225	}
226
227	fn effective_pprof_sample_rate(&self) -> usize {
228		if self.pprof_sample_rate_from_env {
229			env_pprof_sample_rate(self.pprof_sample_rate)
230		} else {
231			self.pprof_sample_rate
232		}
233	}
234
235	fn record_allocation(&self, ptr: usize, size: usize) {
236		if self.stats {
237			GLOBAL_STATS
238				.allocated
239				.fetch_add(size as u64, Ordering::Relaxed);
240			GLOBAL_STATS.allocations.fetch_add(1, Ordering::Relaxed);
241		}
242
243		if !self.pprof {
244			return;
245		}
246
247		let sample_rate = self.effective_pprof_sample_rate();
248		CURRENT_PPROF_SAMPLE_RATE.store(sample_rate, Ordering::Relaxed);
249		if !should_sample_allocation(size, sample_rate) {
250			return;
251		}
252
253		let trace = HashedBacktrace::capture();
254		self.record_allocation_with_trace(ptr, size, trace);
255	}
256
257	fn record_allocation_with_trace(&self, ptr: usize, size: usize, trace: HashedBacktrace) {
258		POINTER_MAP.insert(
259			ptr,
260			AllocationRecord {
261				size,
262				trace: trace.clone(),
263			},
264		);
265		let mut stats = TRACE_MAP.entry(trace).or_default();
266		stats.allocated += size as u64;
267		stats.allocations += 1;
268	}
269
270	fn take_allocation_record(&self, ptr: usize) -> Option<AllocationRecord> {
271		if self.pprof && self.effective_pprof_sample_rate() != 0 {
272			POINTER_MAP.remove(&ptr).map(|(_, record)| record)
273		} else {
274			None
275		}
276	}
277
278	fn restore_allocation_record(&self, ptr: usize, record: AllocationRecord) {
279		POINTER_MAP.insert(ptr, record);
280	}
281
282	fn finish_deallocation(&self, record: Option<AllocationRecord>, size: usize) {
283		let freed_size = record.as_ref().map(|record| record.size).unwrap_or(size);
284
285		if self.stats {
286			GLOBAL_STATS
287				.freed
288				.fetch_add(freed_size as u64, Ordering::Relaxed);
289			GLOBAL_STATS.frees.fetch_add(1, Ordering::Relaxed);
290		}
291
292		let Some(record) = record else {
293			return;
294		};
295
296		let mut stats = TRACE_MAP.entry(record.trace).or_default();
297		stats.freed += freed_size as u64;
298		stats.frees += 1;
299	}
300
301	#[cfg(test)]
302	fn record_deallocation(&self, ptr: usize, size: usize) {
303		let record = self.take_allocation_record(ptr);
304		self.finish_deallocation(record, size);
305	}
306
307	#[cfg(test)]
308	fn record_reallocation(&self, old_ptr: usize, old_size: usize, new_ptr: usize, new_size: usize) {
309		let record = self.take_allocation_record(old_ptr);
310		self.finish_deallocation(record, old_size);
311		self.record_allocation(new_ptr, new_size);
312	}
313}
314
315fn enter_alloc<T>(func: impl FnOnce() -> T) -> T {
316	let current_value = IN_ALLOC.with(|x| x.get());
317	IN_ALLOC.with(|x| x.set(true));
318	let output = func();
319	IN_ALLOC.with(|x| x.set(current_value));
320	output
321}
322
323thread_local! {
324	/// Used to avoid recursive alloc/dealloc calls for interior allocation.
325	static IN_ALLOC: Cell<bool> = const { Cell::new(false) };
326	static NEXT_SAMPLE: Cell<i64> = const { Cell::new(i64::MIN) };
327	static NEXT_SAMPLE_RATE: Cell<usize> = const { Cell::new(usize::MAX) };
328	static RNG_STATE: Cell<u64> = const { Cell::new(0) };
329}
330
331static GLOBAL_STATS: stats::AtomicAllocations = stats::AtomicAllocations::new();
332static ENV_PPROF_SAMPLE_RATE_STATE: AtomicU8 = AtomicU8::new(ENV_SAMPLE_RATE_UNINITIALIZED);
333static ENV_PPROF_SAMPLE_RATE_VALUE: AtomicUsize = AtomicUsize::new(DEFAULT_PPROF_SAMPLE_RATE);
334static CURRENT_PPROF_SAMPLE_RATE: AtomicUsize = AtomicUsize::new(DEFAULT_PPROF_SAMPLE_RATE);
335
336lazy_static::lazy_static! {
337	static ref POINTER_MAP: DashMap<usize, AllocationRecord> = DashMap::new();
338	static ref TRACE_MAP: DashMap<HashedBacktrace, stats::Allocations> = DashMap::new();
339}
340
341/// Return a snapshot of coarse process-wide allocation counters.
342///
343/// These counters are updated only when the global allocator wrapper was
344/// configured with [`PprofAlloc::with_stats`].
345pub fn allocation_stats() -> stats::Allocations {
346	GLOBAL_STATS.snapshot()
347}
348
349/// Return the stack capture mode compiled into this build.
350///
351/// Linux x86_64/aarch64 builds use the fast frame-pointer unwinder when the
352/// default `frame-pointer` feature is enabled. Other builds use the `backtrace`
353/// crate fallback.
354pub const fn capture_mode() -> CaptureMode {
355	trace::capture_mode()
356}
357
358fn current_pprof_sample_rate() -> usize {
359	CURRENT_PPROF_SAMPLE_RATE.load(Ordering::Relaxed)
360}
361
362fn env_pprof_sample_rate(default_rate: usize) -> usize {
363	match ENV_PPROF_SAMPLE_RATE_STATE.load(Ordering::Acquire) {
364		ENV_SAMPLE_RATE_SET => return ENV_PPROF_SAMPLE_RATE_VALUE.load(Ordering::Relaxed),
365		ENV_SAMPLE_RATE_UNSET => return default_rate,
366		_ => {},
367	}
368
369	if let Some(sample_rate) = read_pprof_sample_rate_env() {
370		ENV_PPROF_SAMPLE_RATE_VALUE.store(sample_rate, Ordering::Relaxed);
371		ENV_PPROF_SAMPLE_RATE_STATE.store(ENV_SAMPLE_RATE_SET, Ordering::Release);
372		sample_rate
373	} else {
374		ENV_PPROF_SAMPLE_RATE_STATE.store(ENV_SAMPLE_RATE_UNSET, Ordering::Release);
375		default_rate
376	}
377}
378
379fn read_pprof_sample_rate_env() -> Option<usize> {
380	let ptr = unsafe { libc::getenv(PPROF_SAMPLE_RATE_ENV_CSTR.as_ptr().cast()) };
381	if ptr.is_null() {
382		return None;
383	}
384
385	let mut value = 0usize;
386	let mut cursor = ptr.cast::<u8>();
387	let mut saw_digit = false;
388	loop {
389		let byte = unsafe { *cursor };
390		if byte == 0 {
391			break;
392		}
393		if !byte.is_ascii_digit() {
394			return None;
395		}
396		saw_digit = true;
397		value = value
398			.saturating_mul(10)
399			.saturating_add((byte - b'0') as usize);
400		cursor = unsafe { cursor.add(1) };
401	}
402
403	saw_digit.then_some(value)
404}
405
406fn should_sample_allocation(size: usize, sample_rate: usize) -> bool {
407	if size == 0 || sample_rate == 0 {
408		return false;
409	}
410	if sample_rate == 1 {
411		return true;
412	}
413
414	NEXT_SAMPLE.with(|next_sample| {
415		NEXT_SAMPLE_RATE.with(|next_sample_rate| {
416			if next_sample_rate.get() != sample_rate {
417				next_sample.set(next_sample_distance(sample_rate));
418				next_sample_rate.set(sample_rate);
419			}
420
421			let next = next_sample
422				.get()
423				.saturating_sub(i64::try_from(size).unwrap_or(i64::MAX));
424			if next < 0 {
425				next_sample.set(next_sample_distance(sample_rate));
426				true
427			} else {
428				next_sample.set(next);
429				false
430			}
431		})
432	})
433}
434
435fn next_sample_distance(sample_rate: usize) -> i64 {
436	match sample_rate {
437		0 => i64::MAX,
438		1 => 0,
439		rate => i64::from(fast_exp_rand(rate)),
440	}
441}
442
443fn fast_exp_rand(mean: usize) -> i32 {
444	let mean = mean.min(MAX_FAST_EXP_RAND_MEAN);
445	if mean == 0 {
446		return 0;
447	}
448
449	let q = cheap_random_n(1 << RANDOM_BIT_COUNT) + 1;
450	let qlog = ((q as f64).log2() - RANDOM_BIT_COUNT as f64).min(0.0);
451	(qlog * (-std::f64::consts::LN_2 * mean as f64)) as i32 + 1
452}
453
454fn cheap_random_n(n: u32) -> u32 {
455	(cheap_random() % u64::from(n)) as u32
456}
457
458fn cheap_random() -> u64 {
459	RNG_STATE.with(|state| {
460		let mut x = state.get();
461		if x == 0 {
462			x = random_seed();
463		}
464		x ^= x >> 12;
465		x ^= x << 25;
466		x ^= x >> 27;
467		state.set(x);
468		x.wrapping_mul(0x2545_f491_4f6c_dd1d)
469	})
470}
471
472fn random_seed() -> u64 {
473	let stack_addr = &() as *const () as usize as u64;
474	let time = SystemTime::now()
475		.duration_since(UNIX_EPOCH)
476		.map(|duration| duration.as_nanos() as u64)
477		.unwrap_or(0);
478	let seed = stack_addr ^ time ^ 0x9e37_79b9_7f4a_7c15;
479	if seed == 0 {
480		0x9e37_79b9_7f4a_7c15
481	} else {
482		seed
483	}
484}
485
486/// Capture a best-effort process memory snapshot.
487///
488/// Individual probes that fail are represented as `None` in the returned
489/// [`MemorySnapshot`]. This function intentionally does not fail as a whole.
490pub fn snapshot() -> MemorySnapshot {
491	enter_alloc(|| MemorySnapshot {
492		captured_at_unix_ms: SystemTime::now()
493			.duration_since(UNIX_EPOCH)
494			.expect("system time must be after the UNIX epoch")
495			.as_millis()
496			.try_into()
497			.expect("timestamp must fit in u64"),
498		capture_mode: capture_mode(),
499		allocation_stats: allocation_stats(),
500		pprof: pprof_summary(),
501		allocator: allocator::snapshot(),
502		cgroup: stats::cgroups::get_stats().ok(),
503		smaps: stats::smaps::rollup().ok(),
504	})
505}
506
507fn pprof_summary() -> PprofSummary {
508	let mut summary = PprofSummary::default();
509	let sample_rate = current_pprof_sample_rate();
510	for entry in TRACE_MAP.iter() {
511		let stats = entry.value();
512		let values = HeapSampleValues::from_allocations(stats, sample_rate);
513		summary.total_stacks += 1;
514		summary.alloc_space_bytes += values.alloc_space.max(0) as u64;
515		summary.inuse_space_bytes += values.inuse_space.max(0) as u64;
516		summary.alloc_objects += values.alloc_objects.max(0) as u64;
517		summary.inuse_objects += values.inuse_objects.max(0) as u64;
518		if values.inuse_space > 0 {
519			summary.live_stacks += 1;
520		}
521	}
522	summary
523}
524
525unsafe impl<A: GlobalAlloc> GlobalAlloc for PprofAlloc<A> {
526	unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
527		unsafe {
528			if IN_ALLOC.with(|x| x.get()) {
529				return self.inner.alloc(layout);
530			}
531
532			enter_alloc(|| {
533				let ptr = self.inner.alloc(layout);
534				if !ptr.is_null() {
535					self.record_allocation(ptr as usize, layout.size());
536				}
537				ptr
538			})
539		}
540	}
541
542	unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
543		unsafe {
544			if IN_ALLOC.with(|x| x.get()) {
545				return self.inner.alloc_zeroed(layout);
546			}
547
548			enter_alloc(|| {
549				let ptr = self.inner.alloc_zeroed(layout);
550				if !ptr.is_null() {
551					self.record_allocation(ptr as usize, layout.size());
552				}
553				ptr
554			})
555		}
556	}
557
558	unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
559		unsafe {
560			if IN_ALLOC.with(|x| x.get()) {
561				self.inner.dealloc(ptr, layout);
562				return;
563			}
564
565			enter_alloc(|| {
566				let record = self.take_allocation_record(ptr as usize);
567				self.inner.dealloc(ptr, layout);
568				self.finish_deallocation(record, layout.size());
569			});
570		}
571	}
572
573	unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
574		unsafe {
575			if IN_ALLOC.with(|x| x.get()) {
576				return self.inner.realloc(ptr, layout, new_size);
577			}
578
579			enter_alloc(|| {
580				let record = self.take_allocation_record(ptr as usize);
581				let new_ptr = self.inner.realloc(ptr, layout, new_size);
582				if !new_ptr.is_null() {
583					self.finish_deallocation(record, layout.size());
584					self.record_allocation(new_ptr as usize, new_size);
585				} else if let Some(record) = record {
586					self.restore_allocation_record(ptr as usize, record);
587				}
588				new_ptr
589			})
590		}
591	}
592}
593
594pub fn generate_pprof() -> anyhow::Result<Vec<u8>> {
595	enter_alloc(|| {
596		let sample_rate = current_pprof_sample_rate();
597		let mut profile = StackProfile {
598			annotations: Default::default(),
599			stacks: Default::default(),
600			mappings: if let Some(m) = crate::pprof::MAPPINGS.as_deref() {
601				m.to_vec()
602			} else {
603				Default::default()
604			},
605		};
606
607		for entry in TRACE_MAP.iter() {
608			let sample_values = HeapSampleValues::from_allocations(entry.value(), sample_rate);
609			if sample_values.alloc_space == 0
610				&& sample_values.inuse_space == 0
611				&& sample_values.alloc_objects == 0
612				&& sample_values.inuse_objects == 0
613			{
614				continue;
615			}
616
617			profile.push_stack(
618				WeightedStack {
619					addrs: entry.key().addrs(),
620					values: smallvec::smallvec![
621						sample_values.alloc_objects,
622						sample_values.alloc_space,
623						sample_values.inuse_objects,
624						sample_values.inuse_space
625					],
626				},
627				None,
628			);
629		}
630
631		Ok(profile.to_pprof_with_period(
632			&[
633				("alloc_objects", "count"),
634				("alloc_space", "bytes"),
635				("inuse_objects", "count"),
636				("inuse_space", "bytes"),
637			],
638			("space", "bytes"),
639			sample_rate as i64,
640			None,
641		))
642	})
643}
644
645#[doc(hidden)]
646#[macro_export]
647macro_rules! __pprof_alloc_register_allocator_kind {
648	($kind:expr) => {
649		const _: () = {
650			#[cfg(target_os = "linux")]
651			#[used]
652			#[unsafe(link_section = ".init_array")]
653			static INIT_ARRAY: extern "C" fn() = {
654				extern "C" fn init() {
655					$crate::allocator::configure($kind);
656				}
657				init
658			};
659		};
660	};
661}
662
663#[macro_export]
664macro_rules! declare_allocator_kind {
665	($kind:expr $(;)?) => {
666		$crate::__pprof_alloc_register_allocator_kind!($kind);
667	};
668}
669
670#[cfg(test)]
671fn reset_tracking_state() {
672	POINTER_MAP.clear();
673	TRACE_MAP.clear();
674	GLOBAL_STATS.allocated.store(0, Ordering::Relaxed);
675	GLOBAL_STATS.freed.store(0, Ordering::Relaxed);
676	GLOBAL_STATS.allocations.store(0, Ordering::Relaxed);
677	GLOBAL_STATS.frees.store(0, Ordering::Relaxed);
678	CURRENT_PPROF_SAMPLE_RATE.store(1, Ordering::Relaxed);
679	ENV_PPROF_SAMPLE_RATE_STATE.store(ENV_SAMPLE_RATE_UNINITIALIZED, Ordering::Relaxed);
680	ENV_PPROF_SAMPLE_RATE_VALUE.store(DEFAULT_PPROF_SAMPLE_RATE, Ordering::Relaxed);
681	NEXT_SAMPLE.with(|next_sample| next_sample.set(i64::MIN));
682	NEXT_SAMPLE_RATE.with(|next_sample_rate| next_sample_rate.set(usize::MAX));
683}
684
685#[cfg(test)]
686mod tests {
687	use super::*;
688	use parking_lot::Mutex;
689
690	static TEST_GUARD: Mutex<()> = Mutex::new(());
691
692	#[test]
693	fn allocation_stats_compute_in_use_values() {
694		let stats = stats::Allocations {
695			allocated: 4096,
696			freed: 1024,
697			allocations: 4,
698			frees: 1,
699		};
700
701		assert_eq!(stats.in_use_bytes(), 3072);
702		assert_eq!(stats.in_use_allocations(), 3);
703	}
704
705	#[test]
706	fn sample_rate_one_records_every_profile_allocation() {
707		let _guard = TEST_GUARD.lock();
708		reset_tracking_state();
709
710		let alloc = PprofAlloc::new().with_pprof_sample_rate(1);
711		alloc.record_allocation(0x1000, 128);
712		alloc.record_allocation(0x2000, 64);
713
714		assert_eq!(current_pprof_sample_rate(), 1);
715		assert_eq!(POINTER_MAP.len(), 2);
716		assert_eq!(pprof_summary().alloc_space_bytes, 192);
717		assert_eq!(pprof_summary().alloc_objects, 2);
718	}
719
720	#[test]
721	fn sample_rate_zero_disables_profile_allocation_records() {
722		let _guard = TEST_GUARD.lock();
723		reset_tracking_state();
724
725		let alloc = PprofAlloc::new().with_pprof_sample_rate(0).with_stats();
726		alloc.record_allocation(0x1000, 128);
727		alloc.record_deallocation(0x1000, 128);
728
729		assert_eq!(current_pprof_sample_rate(), 0);
730		assert!(POINTER_MAP.is_empty());
731		assert!(TRACE_MAP.is_empty());
732		assert_eq!(allocation_stats().allocated, 128);
733		assert_eq!(allocation_stats().freed, 128);
734	}
735
736	#[test]
737	fn sampled_heap_values_are_scaled_to_estimates() {
738		let (count, size) = scale_heap_sample(1, 1024, 512);
739
740		assert_eq!(count, 1);
741		assert!((1180..=1190).contains(&size));
742	}
743
744	#[test]
745	fn env_sample_rate_is_read_lazily() {
746		let _guard = TEST_GUARD.lock();
747		reset_tracking_state();
748
749		unsafe {
750			std::env::set_var(PPROF_SAMPLE_RATE_ENV, "1");
751		}
752		let alloc = PprofAlloc::new().with_pprof_sample_rate_from_env(DEFAULT_PPROF_SAMPLE_RATE);
753		alloc.record_allocation(0x1000, 128);
754		unsafe {
755			std::env::remove_var(PPROF_SAMPLE_RATE_ENV);
756		}
757
758		assert_eq!(current_pprof_sample_rate(), 1);
759		assert_eq!(POINTER_MAP.len(), 1);
760	}
761
762	#[test]
763	fn env_sample_rate_uses_configured_default_when_unset() {
764		let _guard = TEST_GUARD.lock();
765		reset_tracking_state();
766
767		unsafe {
768			std::env::remove_var(PPROF_SAMPLE_RATE_ENV);
769		}
770		let alloc = PprofAlloc::new().with_pprof_sample_rate_from_env(1);
771		alloc.record_allocation(0x1000, 128);
772
773		assert_eq!(current_pprof_sample_rate(), 1);
774		assert_eq!(POINTER_MAP.len(), 1);
775	}
776
777	#[test]
778	fn deallocation_updates_live_profile_bytes() {
779		let _guard = TEST_GUARD.lock();
780		reset_tracking_state();
781
782		let alloc = PprofAlloc::new().with_pprof().with_stats();
783		let trace = HashedBacktrace::capture();
784
785		alloc.record_allocation_with_trace(0x1000, 128, trace.clone());
786		alloc.record_allocation_with_trace(0x2000, 64, trace.clone());
787		alloc.record_deallocation(0x1000, 128);
788
789		let trace_stats = TRACE_MAP.get(&trace).unwrap();
790		assert_eq!(trace_stats.allocated, 192);
791		assert_eq!(trace_stats.freed, 128);
792		assert_eq!(trace_stats.allocations, 2);
793		assert_eq!(trace_stats.frees, 1);
794		assert_eq!(trace_stats.in_use_bytes(), 64);
795		assert!(POINTER_MAP.contains_key(&0x2000));
796		assert!(!POINTER_MAP.contains_key(&0x1000));
797	}
798
799	#[test]
800	fn coarse_stats_track_allocations_and_frees() {
801		let _guard = TEST_GUARD.lock();
802		reset_tracking_state();
803
804		let alloc = PprofAlloc::new().with_stats();
805		alloc.record_allocation(0x5000, 48);
806		alloc.record_deallocation(0x5000, 48);
807
808		assert_eq!(
809			allocation_stats(),
810			stats::Allocations {
811				allocated: 48,
812				freed: 48,
813				allocations: 1,
814				frees: 1,
815			}
816		);
817	}
818
819	#[test]
820	fn reallocation_updates_live_bytes_and_pointer_ownership() {
821		let _guard = TEST_GUARD.lock();
822		reset_tracking_state();
823
824		let alloc = PprofAlloc::new().with_pprof_sample_rate(1);
825		alloc.record_allocation_with_trace(0x3000, 32, HashedBacktrace::capture());
826		alloc.record_reallocation(0x3000, 32, 0x4000, 96);
827
828		let total_live_bytes: u64 = TRACE_MAP
829			.iter()
830			.map(|entry| entry.value().in_use_bytes())
831			.sum();
832		assert_eq!(total_live_bytes, 96);
833		assert_eq!(POINTER_MAP.get(&0x4000).unwrap().size, 96);
834		assert!(!POINTER_MAP.contains_key(&0x3000));
835	}
836
837	#[test]
838	fn snapshot_reports_current_pprof_summary() {
839		let _guard = TEST_GUARD.lock();
840		reset_tracking_state();
841
842		let alloc = PprofAlloc::new().with_pprof();
843		let trace = HashedBacktrace::capture();
844
845		alloc.record_allocation_with_trace(0x1000, 128, trace.clone());
846		alloc.record_allocation_with_trace(0x2000, 64, trace);
847		alloc.record_deallocation(0x1000, 128);
848
849		let snapshot = snapshot();
850		assert_eq!(snapshot.capture_mode, capture_mode());
851		assert_eq!(
852			snapshot.pprof,
853			PprofSummary {
854				total_stacks: 1,
855				live_stacks: 1,
856				alloc_space_bytes: 192,
857				inuse_space_bytes: 64,
858				alloc_objects: 2,
859				inuse_objects: 1,
860			}
861		);
862	}
863}