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 env;
16mod pprof;
17pub mod stats;
18mod trace;
19
20pub use crate::env::{ALLOCATOR_ENV, Allocator, PPROF_BACKEND_ENV, PPROF_SAMPLE_RATE_ENV};
21use crate::env::{AllocatorSelection, PprofBackend};
22use crate::pprof::{StackProfile, WeightedStack};
23use crate::trace::HashedBacktrace;
24use dashmap::DashMap;
25use serde::Serialize;
26use std::alloc::{GlobalAlloc, Layout, System};
27use std::cell::Cell;
28use std::sync::atomic::{AtomicU8, AtomicUsize, Ordering};
29use std::time::{SystemTime, UNIX_EPOCH};
30
31pub use crate::trace::CaptureMode;
32
33/// Default average number of allocated bytes between recorded pprof samples.
34///
35/// This matches Go's default heap profiling rate: one sampled allocation per
36/// 512 KiB of allocated bytes on average. A rate of `1` records every
37/// allocation, while `0` disables pprof stack recording.
38pub const DEFAULT_PPROF_SAMPLE_RATE: usize = 512 * 1024;
39
40const MAX_FAST_EXP_RAND_MEAN: usize = 0x7000000;
41const RANDOM_BIT_COUNT: u32 = 26;
42const RESOLVED_SAMPLE_RATE_UNINITIALIZED: usize = usize::MAX;
43const STATS_FLUSH_EVENTS: u64 = 1024;
44const STATS_FLUSH_BYTES: u64 = 1024 * 1024;
45
46#[repr(u8)]
47#[derive(Clone, Copy, Debug, PartialEq, Eq)]
48#[cfg_attr(windows, allow(dead_code))]
49enum TrackingMode {
50	Uninitialized = 0,
51	System = 1,
52	Jemalloc = 2,
53	Mimalloc = 3,
54	Stats = 4,
55	Pprof = 5,
56	PprofStats = 6,
57}
58
59#[cfg_attr(windows, allow(dead_code))]
60impl TrackingMode {
61	const fn from_u8(value: u8) -> Self {
62		match value {
63			1 => Self::System,
64			2 => Self::Jemalloc,
65			3 => Self::Mimalloc,
66			4 => Self::Stats,
67			5 => Self::Pprof,
68			6 => Self::PprofStats,
69			_ => Self::Uninitialized,
70		}
71	}
72}
73
74/// Global allocator that can collect allocation counters and pprof heap profiles.
75///
76/// Use this as the process `#[global_allocator]`. The backing allocator is
77/// selected from [`ALLOCATOR_ENV`] on first allocation.
78pub struct PprofAlloc {
79	/// Enable profiling support
80	pprof: bool,
81	/// Enable coarse grained stats
82	stats: bool,
83	/// Average bytes between pprof samples. 0 disables pprof, 1 records everything.
84	pprof_sample_rate: usize,
85	/// Read the pprof sample rate from PPROF_ALLOC_SAMPLE_RATE at runtime.
86	pprof_sample_rate_from_env: bool,
87	/// Cached resolved sample rate for env-driven configuration.
88	resolved_pprof_sample_rate: AtomicUsize,
89	/// Cached wrapper work needed on allocation/deallocation.
90	tracking_mode: AtomicU8,
91	/// Allocator to use when [`ALLOCATOR_ENV`] is unset.
92	default_allocator: Allocator,
93}
94
95#[derive(Clone)]
96struct AllocationRecord {
97	size: usize,
98	trace: HashedBacktrace,
99}
100
101struct HeapSampleValues {
102	alloc_objects: i64,
103	alloc_space: i64,
104	inuse_objects: i64,
105	inuse_space: i64,
106}
107
108struct LocalAllocationStats {
109	allocated: Cell<u64>,
110	freed: Cell<u64>,
111	allocations: Cell<u64>,
112	frees: Cell<u64>,
113}
114
115impl LocalAllocationStats {
116	const fn new() -> Self {
117		Self {
118			allocated: Cell::new(0),
119			freed: Cell::new(0),
120			allocations: Cell::new(0),
121			frees: Cell::new(0),
122		}
123	}
124
125	fn record_allocation(&self, size: u64) {
126		self
127			.allocated
128			.set(self.allocated.get().saturating_add(size));
129		self
130			.allocations
131			.set(self.allocations.get().saturating_add(1));
132		self.flush_if_needed();
133	}
134
135	fn record_deallocation(&self, size: u64) {
136		self.freed.set(self.freed.get().saturating_add(size));
137		self.frees.set(self.frees.get().saturating_add(1));
138		self.flush_if_needed();
139	}
140
141	fn flush_if_needed(&self) {
142		let events = self.allocations.get().saturating_add(self.frees.get());
143		let bytes = self.allocated.get().saturating_add(self.freed.get());
144		if events >= STATS_FLUSH_EVENTS || bytes >= STATS_FLUSH_BYTES {
145			self.flush();
146		}
147	}
148
149	fn flush(&self) {
150		let allocated = self.allocated.replace(0);
151		let freed = self.freed.replace(0);
152		let allocations = self.allocations.replace(0);
153		let frees = self.frees.replace(0);
154
155		if allocated != 0 {
156			GLOBAL_STATS
157				.allocated
158				.fetch_add(allocated, Ordering::Relaxed);
159		}
160		if freed != 0 {
161			GLOBAL_STATS.freed.fetch_add(freed, Ordering::Relaxed);
162		}
163		if allocations != 0 {
164			GLOBAL_STATS
165				.allocations
166				.fetch_add(allocations, Ordering::Relaxed);
167		}
168		if frees != 0 {
169			GLOBAL_STATS.frees.fetch_add(frees, Ordering::Relaxed);
170		}
171	}
172
173	#[cfg(test)]
174	fn reset(&self) {
175		self.allocated.set(0);
176		self.freed.set(0);
177		self.allocations.set(0);
178		self.frees.set(0);
179	}
180}
181
182impl Drop for LocalAllocationStats {
183	fn drop(&mut self) {
184		self.flush();
185	}
186}
187
188impl HeapSampleValues {
189	fn from_allocations(stats: &stats::Allocations, sample_rate: usize) -> Self {
190		let (alloc_objects, alloc_space) =
191			scale_heap_sample(stats.allocations, stats.allocated, sample_rate);
192		let (inuse_objects, inuse_space) = scale_heap_sample(
193			stats.in_use_allocations(),
194			stats.in_use_bytes(),
195			sample_rate,
196		);
197		Self {
198			alloc_objects,
199			alloc_space,
200			inuse_objects,
201			inuse_space,
202		}
203	}
204}
205
206fn saturating_i64(value: u64) -> i64 {
207	value.min(i64::MAX as u64) as i64
208}
209
210fn scale_heap_sample(count: u64, size: u64, sample_rate: usize) -> (i64, i64) {
211	if count == 0 || size == 0 {
212		return (0, 0);
213	}
214
215	if sample_rate <= 1 {
216		return (saturating_i64(count), saturating_i64(size));
217	}
218
219	let average_size = size as f64 / count as f64;
220	let probability = -(-average_size / sample_rate as f64).exp_m1();
221	if probability <= 0.0 {
222		return (saturating_i64(count), saturating_i64(size));
223	}
224	let scale = 1.0 / probability;
225	(
226		(count as f64 * scale).min(i64::MAX as f64) as i64,
227		(size as f64 * scale).min(i64::MAX as f64) as i64,
228	)
229}
230
231#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
232/// Summary of the currently recorded pprof allocation profile.
233///
234/// Values are scaled according to the active pprof sample rate, so they are
235/// estimates when sampling is enabled.
236pub struct PprofSummary {
237	/// Number of distinct stack traces recorded in the profile.
238	pub total_stacks: u64,
239	/// Number of distinct stack traces with estimated live bytes greater than zero.
240	pub live_stacks: u64,
241	/// Estimated cumulative allocated bytes across all recorded stacks.
242	pub alloc_space_bytes: u64,
243	/// Estimated currently live bytes across all recorded stacks.
244	pub inuse_space_bytes: u64,
245	/// Estimated cumulative allocation count across all recorded stacks.
246	pub alloc_objects: u64,
247	/// Estimated currently live allocation count across all recorded stacks.
248	pub inuse_objects: u64,
249}
250
251#[derive(Clone, Debug, Serialize)]
252/// Best-effort snapshot of allocation and memory state for the current process created by `snapshot()`.
253///
254/// Snapshot collection never fails as a whole. Optional fields are `None` when
255/// the corresponding operating-system or allocator probe is unavailable.
256pub struct MemorySnapshot {
257	/// Wall-clock capture time as milliseconds since the Unix epoch.
258	pub captured_at_unix_ms: u64,
259	/// Stack capture implementation compiled into this build.
260	pub capture_mode: CaptureMode,
261	/// Coarse process-wide counters from [`allocation_stats`].
262	pub allocation_stats: stats::Allocations,
263	/// Summary of recorded pprof stack-attributed allocation data.
264	pub pprof: PprofSummary,
265	/// Active allocator identity and allocator-specific memory stats, if available.
266	pub allocator: allocator::AllocatorSnapshot,
267	/// cgroup v2 memory stats for this process, when available.
268	pub cgroup: Option<stats::cgroups::MemoryStat>,
269	/// `/proc/self/smaps_rollup` memory stats for this process, when available.
270	pub smaps: Option<stats::smaps::ProcessStats>,
271}
272
273impl Default for PprofAlloc {
274	fn default() -> Self {
275		Self::new()
276	}
277}
278
279impl PprofAlloc {
280	/// Create an allocator with profiling disabled.
281	///
282	/// Use [`Self::with_pprof`], [`Self::with_pprof_sample_rate`], or
283	/// [`Self::with_stats`] to enable collection.
284	pub const fn new() -> Self {
285		PprofAlloc {
286			pprof: false,
287			stats: false,
288			pprof_sample_rate: DEFAULT_PPROF_SAMPLE_RATE,
289			pprof_sample_rate_from_env: false,
290			resolved_pprof_sample_rate: AtomicUsize::new(RESOLVED_SAMPLE_RATE_UNINITIALIZED),
291			tracking_mode: AtomicU8::new(TrackingMode::Uninitialized as u8),
292			default_allocator: Allocator::System,
293		}
294	}
295
296	/// Set the allocator used when [`ALLOCATOR_ENV`] is unset.
297	pub const fn with_default(mut self, allocator: Allocator) -> Self {
298		self.default_allocator = allocator;
299		self
300	}
301
302	/// Enable sampled pprof stack profiling using [`DEFAULT_PPROF_SAMPLE_RATE`].
303	///
304	/// When the active allocator is jemalloc and the backend env selects native
305	/// profiling, builds with `allocator-jemalloc` use jemalloc's native heap
306	/// profiler instead of wrapper-side allocation tracking.
307	pub const fn with_pprof(mut self) -> Self {
308		self.pprof = true;
309		self
310	}
311
312	/// Enable sampled pprof stack profiling with an explicit byte sample rate.
313	///
314	/// A rate of `1` records every allocation. A rate of `0` disables pprof
315	/// stack recording while still allowing other enabled collectors to run.
316	/// When native jemalloc profiling is selected, this wrapper-side rate is
317	/// ignored. Use [`PPROF_SAMPLE_RATE_ENV`] with [`configure`] to set jemalloc's
318	/// runtime sample rate.
319	pub const fn with_pprof_sample_rate(mut self, bytes: usize) -> Self {
320		self.pprof = true;
321		self.pprof_sample_rate = bytes;
322		self.pprof_sample_rate_from_env = false;
323		self
324	}
325
326	/// Enable pprof stack profiling with the sample rate read from the environment.
327	///
328	/// [`PPROF_SAMPLE_RATE_ENV`] is read lazily on the first profiled allocation.
329	/// If the variable is missing or invalid, `default_rate` is used.
330	/// When native jemalloc profiling is selected, [`configure`] applies this
331	/// environment variable to jemalloc's runtime sample rate.
332	pub const fn with_pprof_sample_rate_from_env(mut self, default_rate: usize) -> Self {
333		self.pprof = true;
334		self.pprof_sample_rate = default_rate;
335		self.pprof_sample_rate_from_env = true;
336		self
337	}
338
339	/// Enable coarse process-wide allocation and free counters.
340	///
341	/// When the active allocator is jemalloc and jemalloc support is compiled
342	/// in, snapshots use jemalloc's native process stats instead of these
343	/// wrapper-side counters.
344	pub const fn with_stats(mut self) -> Self {
345		self.stats = true;
346		self
347	}
348
349	fn effective_pprof_sample_rate(&self) -> usize {
350		if self.pprof_sample_rate_from_env {
351			let resolved = self.resolved_pprof_sample_rate.load(Ordering::Relaxed);
352			if resolved != RESOLVED_SAMPLE_RATE_UNINITIALIZED {
353				return resolved;
354			}
355
356			let resolved = env_pprof_sample_rate(self.pprof_sample_rate);
357			self
358				.resolved_pprof_sample_rate
359				.store(resolved, Ordering::Relaxed);
360			resolved
361		} else {
362			self.pprof_sample_rate
363		}
364	}
365
366	#[cfg(test)]
367	fn active_pprof_sample_rate(&self) -> Option<usize> {
368		if !matches!(
369			self.tracking_mode(),
370			TrackingMode::Pprof | TrackingMode::PprofStats
371		) {
372			return None;
373		}
374
375		let sample_rate = self.effective_pprof_sample_rate();
376		CURRENT_PPROF_SAMPLE_RATE.store(sample_rate, Ordering::Relaxed);
377		(sample_rate != 0).then_some(sample_rate)
378	}
379
380	fn record_allocation_stats(&self, size: usize) {
381		if LOCAL_STATS
382			.try_with(|stats| stats.record_allocation(size as u64))
383			.is_err()
384		{
385			GLOBAL_STATS
386				.allocated
387				.fetch_add(size as u64, Ordering::Relaxed);
388			GLOBAL_STATS.allocations.fetch_add(1, Ordering::Relaxed);
389		}
390	}
391
392	fn record_deallocation_stats(&self, size: usize) {
393		if LOCAL_STATS
394			.try_with(|stats| stats.record_deallocation(size as u64))
395			.is_err()
396		{
397			GLOBAL_STATS.freed.fetch_add(size as u64, Ordering::Relaxed);
398			GLOBAL_STATS.frees.fetch_add(1, Ordering::Relaxed);
399		}
400	}
401
402	#[cfg(test)]
403	fn record_allocation(&self, ptr: usize, size: usize) {
404		if self.stats {
405			self.record_allocation_stats(size);
406		}
407		if let Some(sample_rate) = self.active_pprof_sample_rate() {
408			self.record_profile_allocation(ptr, size, sample_rate);
409		}
410	}
411
412	fn record_profile_allocation(&self, ptr: usize, size: usize, sample_rate: usize) {
413		if should_sample_allocation(size, sample_rate) {
414			enter_alloc(|| {
415				let trace = HashedBacktrace::capture();
416				self.record_allocation_with_trace(ptr, size, trace);
417			});
418		}
419	}
420
421	fn record_tracked_allocation(
422		&self,
423		ptr: *mut u8,
424		size: usize,
425		sample_rate: Option<usize>,
426		track_stats: bool,
427	) {
428		if ptr.is_null() {
429			return;
430		}
431
432		if track_stats {
433			self.record_allocation_stats(size);
434		}
435		if let Some(sample_rate) = sample_rate {
436			self.record_profile_allocation(ptr as usize, size, sample_rate);
437		}
438	}
439
440	fn tracking_is_recursive(&self, sample_rate: Option<usize>, track_stats: bool) -> bool {
441		(sample_rate.is_some() || track_stats) && IN_ALLOC.try_with(|x| x.get()).unwrap_or(true)
442	}
443
444	#[cfg(windows)]
445	fn tracking_mode(&self) -> TrackingMode {
446		self
447			.tracking_mode
448			.store(TrackingMode::System as u8, Ordering::Relaxed);
449		TrackingMode::System
450	}
451
452	#[cfg(not(windows))]
453	fn tracking_mode(&self) -> TrackingMode {
454		let mode = TrackingMode::from_u8(self.tracking_mode.load(Ordering::Relaxed));
455		if mode != TrackingMode::Uninitialized {
456			return mode;
457		}
458
459		let selected_allocator = env::selected_allocator(self.default_allocator);
460		let native_jemalloc =
461			cfg!(feature = "allocator-jemalloc") && selected_allocator == AllocatorSelection::Jemalloc;
462		let native_pprof = native_jemalloc && env::selected_pprof_backend() == PprofBackend::Native;
463		let wrapper_stats = self.stats && !native_jemalloc;
464		let wrapper_pprof_rate = (self.pprof && !native_pprof).then(|| {
465			let sample_rate = self.effective_pprof_sample_rate();
466			CURRENT_PPROF_SAMPLE_RATE.store(sample_rate, Ordering::Relaxed);
467			sample_rate
468		});
469		let wrapper_pprof = wrapper_pprof_rate.is_some_and(|sample_rate| sample_rate != 0);
470
471		let mode = match (wrapper_pprof, wrapper_stats) {
472			(false, false) => match selected_allocator {
473				AllocatorSelection::Jemalloc => TrackingMode::Jemalloc,
474				AllocatorSelection::Mimalloc => TrackingMode::Mimalloc,
475				_ => TrackingMode::System,
476			},
477			(false, true) => TrackingMode::Stats,
478			(true, false) => TrackingMode::Pprof,
479			(true, true) => TrackingMode::PprofStats,
480		};
481		self.tracking_mode.store(mode as u8, Ordering::Relaxed);
482		mode
483	}
484
485	#[cfg(all(test, feature = "allocator-jemalloc"))]
486	fn native_pprof_selected(&self) -> bool {
487		match env::selected_pprof_backend() {
488			PprofBackend::Wrapper => false,
489			PprofBackend::Native => {
490				env::selected_allocator(self.default_allocator) == AllocatorSelection::Jemalloc
491			},
492			PprofBackend::Uninitialized => false,
493		}
494	}
495
496	fn record_allocation_with_trace(&self, ptr: usize, size: usize, trace: HashedBacktrace) {
497		POINTER_MAP.insert(
498			ptr,
499			AllocationRecord {
500				size,
501				trace: trace.clone(),
502			},
503		);
504		let mut stats = TRACE_MAP.entry(trace).or_default();
505		stats.allocated += size as u64;
506		stats.allocations += 1;
507	}
508
509	fn take_allocation_record(&self, ptr: usize) -> Option<AllocationRecord> {
510		if self.pprof && self.effective_pprof_sample_rate() != 0 {
511			POINTER_MAP.remove(&ptr).map(|(_, record)| record)
512		} else {
513			None
514		}
515	}
516
517	fn restore_allocation_record(&self, ptr: usize, record: AllocationRecord) {
518		POINTER_MAP.insert(ptr, record);
519	}
520
521	fn finish_deallocation(&self, record: Option<AllocationRecord>, size: usize, track_stats: bool) {
522		let freed_size = record.as_ref().map(|record| record.size).unwrap_or(size);
523		if track_stats {
524			self.record_deallocation_stats(freed_size);
525		}
526
527		let Some(record) = record else {
528			return;
529		};
530
531		let mut stats = TRACE_MAP.entry(record.trace).or_default();
532		stats.freed += freed_size as u64;
533		stats.frees += 1;
534	}
535
536	#[cfg(test)]
537	fn record_deallocation(&self, ptr: usize, size: usize) {
538		let record = self.take_allocation_record(ptr);
539		self.finish_deallocation(record, size, self.stats);
540	}
541
542	#[cfg(test)]
543	fn record_reallocation(&self, old_ptr: usize, old_size: usize, new_ptr: usize, new_size: usize) {
544		let record = self.take_allocation_record(old_ptr);
545		self.finish_deallocation(record, old_size, self.stats);
546		self.record_allocation(new_ptr, new_size);
547	}
548
549	unsafe fn inner_alloc(&self, layout: Layout) -> *mut u8 {
550		match env::selected_allocator(self.default_allocator) {
551			#[cfg(feature = "allocator-jemalloc")]
552			AllocatorSelection::Jemalloc => unsafe { JEMALLOC_ALLOCATOR.alloc(layout) },
553			#[cfg(feature = "allocator-mimalloc")]
554			AllocatorSelection::Mimalloc => unsafe { MIMALLOC_ALLOCATOR.alloc(layout) },
555			_ => unsafe { System.alloc(layout) },
556		}
557	}
558
559	unsafe fn inner_alloc_zeroed(&self, layout: Layout) -> *mut u8 {
560		match env::selected_allocator(self.default_allocator) {
561			#[cfg(feature = "allocator-jemalloc")]
562			AllocatorSelection::Jemalloc => unsafe { JEMALLOC_ALLOCATOR.alloc_zeroed(layout) },
563			#[cfg(feature = "allocator-mimalloc")]
564			AllocatorSelection::Mimalloc => unsafe { MIMALLOC_ALLOCATOR.alloc_zeroed(layout) },
565			_ => unsafe { System.alloc_zeroed(layout) },
566		}
567	}
568
569	unsafe fn inner_dealloc(&self, ptr: *mut u8, layout: Layout) {
570		match env::selected_allocator(self.default_allocator) {
571			#[cfg(feature = "allocator-jemalloc")]
572			AllocatorSelection::Jemalloc => unsafe { JEMALLOC_ALLOCATOR.dealloc(ptr, layout) },
573			#[cfg(feature = "allocator-mimalloc")]
574			AllocatorSelection::Mimalloc => unsafe { MIMALLOC_ALLOCATOR.dealloc(ptr, layout) },
575			_ => unsafe { System.dealloc(ptr, layout) },
576		}
577	}
578
579	unsafe fn inner_realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
580		match env::selected_allocator(self.default_allocator) {
581			#[cfg(feature = "allocator-jemalloc")]
582			AllocatorSelection::Jemalloc => unsafe { JEMALLOC_ALLOCATOR.realloc(ptr, layout, new_size) },
583			#[cfg(feature = "allocator-mimalloc")]
584			AllocatorSelection::Mimalloc => unsafe { MIMALLOC_ALLOCATOR.realloc(ptr, layout, new_size) },
585			_ => unsafe { System.realloc(ptr, layout, new_size) },
586		}
587	}
588}
589
590fn enter_alloc<T>(func: impl FnOnce() -> T) -> T {
591	let Ok(current_value) = IN_ALLOC.try_with(|x| {
592		let current_value = x.get();
593		x.set(true);
594		current_value
595	}) else {
596		return func();
597	};
598	let output = func();
599	let _ = IN_ALLOC.try_with(|x| x.set(current_value));
600	output
601}
602
603thread_local! {
604	/// Used to avoid recursive alloc/dealloc calls for interior allocation.
605	static IN_ALLOC: Cell<bool> = const { Cell::new(false) };
606	static NEXT_SAMPLE: Cell<i64> = const { Cell::new(i64::MIN) };
607	static NEXT_SAMPLE_RATE: Cell<usize> = const { Cell::new(usize::MAX) };
608	static RNG_STATE: Cell<u64> = const { Cell::new(0) };
609	static LOCAL_STATS: LocalAllocationStats = const { LocalAllocationStats::new() };
610}
611
612static GLOBAL_STATS: stats::AtomicAllocations = stats::AtomicAllocations::new();
613static CURRENT_PPROF_SAMPLE_RATE: AtomicUsize = AtomicUsize::new(DEFAULT_PPROF_SAMPLE_RATE);
614lazy_static::lazy_static! {
615	static ref POINTER_MAP: DashMap<usize, AllocationRecord> = DashMap::new();
616	static ref TRACE_MAP: DashMap<HashedBacktrace, stats::Allocations> = DashMap::new();
617}
618
619/// Return a snapshot of coarse process-wide allocation counters.
620///
621/// These counters are updated only when the global allocator wrapper was
622/// configured with [`PprofAlloc::with_stats`]. Builds with jemalloc support
623/// report jemalloc's native current allocated bytes as `allocated`
624/// and leave cumulative free/object counters unset.
625pub fn allocation_stats() -> stats::Allocations {
626	if cfg!(feature = "allocator-jemalloc")
627		&& env::cached_allocator() == Some(AllocatorSelection::Jemalloc)
628	{
629		if let Some(stats) = native_allocation_stats() {
630			return stats;
631		}
632	}
633
634	let _ = LOCAL_STATS.try_with(|stats| stats.flush());
635	GLOBAL_STATS.snapshot()
636}
637
638#[cfg(feature = "allocator-jemalloc")]
639fn native_allocation_stats() -> Option<stats::Allocations> {
640	use tikv_jemalloc_ctl::{epoch, stats as jemalloc_stats};
641
642	epoch::advance().ok()?;
643	let allocated = jemalloc_stats::allocated::read().ok()? as u64;
644	Some(stats::Allocations {
645		allocated,
646		freed: 0,
647		allocations: 0,
648		frees: 0,
649	})
650}
651
652#[cfg(not(feature = "allocator-jemalloc"))]
653fn native_allocation_stats() -> Option<stats::Allocations> {
654	None
655}
656
657/// Return the stack capture mode compiled into this build.
658///
659/// Linux x86_64/aarch64 builds use the fast frame-pointer unwinder when the
660/// default `frame-pointer` feature is enabled. Other builds use the `backtrace`
661/// crate fallback.
662pub const fn capture_mode() -> CaptureMode {
663	trace::capture_mode()
664}
665
666fn current_pprof_sample_rate() -> usize {
667	CURRENT_PPROF_SAMPLE_RATE.load(Ordering::Relaxed)
668}
669
670fn env_pprof_sample_rate(default_rate: usize) -> usize {
671	env::pprof_sample_rate(default_rate)
672}
673
674fn should_sample_allocation(size: usize, sample_rate: usize) -> bool {
675	if size == 0 || sample_rate == 0 {
676		return false;
677	}
678	if sample_rate == 1 {
679		return true;
680	}
681
682	NEXT_SAMPLE
683		.try_with(|next_sample| {
684			NEXT_SAMPLE_RATE.try_with(|next_sample_rate| {
685				if next_sample_rate.get() != sample_rate {
686					next_sample.set(next_sample_distance(sample_rate));
687					next_sample_rate.set(sample_rate);
688				}
689
690				let next = next_sample
691					.get()
692					.saturating_sub(i64::try_from(size).unwrap_or(i64::MAX));
693				if next < 0 {
694					next_sample.set(next_sample_distance(sample_rate));
695					true
696				} else {
697					next_sample.set(next);
698					false
699				}
700			})
701		})
702		.ok()
703		.and_then(Result::ok)
704		.unwrap_or(false)
705}
706
707fn next_sample_distance(sample_rate: usize) -> i64 {
708	match sample_rate {
709		0 => i64::MAX,
710		1 => 0,
711		rate => i64::from(fast_exp_rand(rate)),
712	}
713}
714
715fn fast_exp_rand(mean: usize) -> i32 {
716	let mean = mean.min(MAX_FAST_EXP_RAND_MEAN);
717	if mean == 0 {
718		return 0;
719	}
720
721	let q = (cheap_random() % u64::from(1u32 << RANDOM_BIT_COUNT)) as u32 + 1;
722	let qlog = ((q as f64).log2() - RANDOM_BIT_COUNT as f64).min(0.0);
723	(qlog * (-std::f64::consts::LN_2 * mean as f64)) as i32 + 1
724}
725
726fn cheap_random() -> u64 {
727	RNG_STATE
728		.try_with(|state| {
729			let mut x = state.get();
730			if x == 0 {
731				x = random_seed();
732			}
733			x ^= x >> 12;
734			x ^= x << 25;
735			x ^= x >> 27;
736			state.set(x);
737			x.wrapping_mul(0x2545_f491_4f6c_dd1d)
738		})
739		.unwrap_or_else(|_| random_seed())
740}
741
742fn random_seed() -> u64 {
743	let stack_addr = &() as *const () as usize as u64;
744	let time = SystemTime::now()
745		.duration_since(UNIX_EPOCH)
746		.map(|duration| duration.as_nanos() as u64)
747		.unwrap_or(0);
748	let seed = stack_addr ^ time ^ 0x9e37_79b9_7f4a_7c15;
749	if seed == 0 {
750		0x9e37_79b9_7f4a_7c15
751	} else {
752		seed
753	}
754}
755
756/// Capture a best-effort process memory snapshot.
757///
758/// Individual probes that fail are represented as `None` in the returned
759/// [`MemorySnapshot`]. This function intentionally does not fail as a whole.
760pub fn snapshot() -> MemorySnapshot {
761	enter_alloc(|| MemorySnapshot {
762		captured_at_unix_ms: SystemTime::now()
763			.duration_since(UNIX_EPOCH)
764			.expect("system time must be after the UNIX epoch")
765			.as_millis()
766			.try_into()
767			.expect("timestamp must fit in u64"),
768		capture_mode: capture_mode(),
769		allocation_stats: allocation_stats(),
770		pprof: pprof_summary(),
771		allocator: allocator::snapshot(),
772		cgroup: stats::cgroups::get_stats().ok(),
773		smaps: stats::smaps::rollup().ok(),
774	})
775}
776
777fn pprof_summary() -> PprofSummary {
778	let mut summary = PprofSummary::default();
779	let sample_rate = current_pprof_sample_rate();
780	for entry in TRACE_MAP.iter() {
781		let stats = entry.value();
782		let values = HeapSampleValues::from_allocations(stats, sample_rate);
783		summary.total_stacks += 1;
784		summary.alloc_space_bytes += values.alloc_space.max(0) as u64;
785		summary.inuse_space_bytes += values.inuse_space.max(0) as u64;
786		summary.alloc_objects += values.alloc_objects.max(0) as u64;
787		summary.inuse_objects += values.inuse_objects.max(0) as u64;
788		if values.inuse_space > 0 {
789			summary.live_stacks += 1;
790		}
791	}
792	summary
793}
794
795#[cfg(feature = "allocator-jemalloc")]
796static JEMALLOC_ALLOCATOR: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc;
797#[cfg(feature = "allocator-mimalloc")]
798static MIMALLOC_ALLOCATOR: mimalloc::MiMalloc = mimalloc::MiMalloc;
799
800unsafe impl GlobalAlloc for PprofAlloc {
801	unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
802		unsafe {
803			let tracking_mode = self.tracking_mode();
804			match tracking_mode {
805				TrackingMode::System => return System.alloc(layout),
806				#[cfg(feature = "allocator-jemalloc")]
807				TrackingMode::Jemalloc => return JEMALLOC_ALLOCATOR.alloc(layout),
808				#[cfg(feature = "allocator-mimalloc")]
809				TrackingMode::Mimalloc => return MIMALLOC_ALLOCATOR.alloc(layout),
810				_ => {},
811			}
812
813			let sample_rate = matches!(
814				tracking_mode,
815				TrackingMode::Pprof | TrackingMode::PprofStats
816			)
817			.then(|| self.effective_pprof_sample_rate());
818			let track_stats = matches!(
819				tracking_mode,
820				TrackingMode::Stats | TrackingMode::PprofStats
821			);
822			if self.tracking_is_recursive(sample_rate, track_stats) {
823				return self.inner_alloc(layout);
824			}
825
826			let ptr = self.inner_alloc(layout);
827			self.record_tracked_allocation(ptr, layout.size(), sample_rate, track_stats);
828			ptr
829		}
830	}
831
832	unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
833		unsafe {
834			let tracking_mode = self.tracking_mode();
835			match tracking_mode {
836				TrackingMode::System => return System.alloc_zeroed(layout),
837				#[cfg(feature = "allocator-jemalloc")]
838				TrackingMode::Jemalloc => return JEMALLOC_ALLOCATOR.alloc_zeroed(layout),
839				#[cfg(feature = "allocator-mimalloc")]
840				TrackingMode::Mimalloc => return MIMALLOC_ALLOCATOR.alloc_zeroed(layout),
841				_ => {},
842			}
843
844			let sample_rate = matches!(
845				tracking_mode,
846				TrackingMode::Pprof | TrackingMode::PprofStats
847			)
848			.then(|| self.effective_pprof_sample_rate());
849			let track_stats = matches!(
850				tracking_mode,
851				TrackingMode::Stats | TrackingMode::PprofStats
852			);
853			if self.tracking_is_recursive(sample_rate, track_stats) {
854				return self.inner_alloc_zeroed(layout);
855			}
856
857			let ptr = self.inner_alloc_zeroed(layout);
858			self.record_tracked_allocation(ptr, layout.size(), sample_rate, track_stats);
859			ptr
860		}
861	}
862
863	unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
864		unsafe {
865			let tracking_mode = self.tracking_mode();
866			match tracking_mode {
867				TrackingMode::System => return System.dealloc(ptr, layout),
868				#[cfg(feature = "allocator-jemalloc")]
869				TrackingMode::Jemalloc => return JEMALLOC_ALLOCATOR.dealloc(ptr, layout),
870				#[cfg(feature = "allocator-mimalloc")]
871				TrackingMode::Mimalloc => return MIMALLOC_ALLOCATOR.dealloc(ptr, layout),
872				_ => {},
873			}
874
875			let sample_rate = matches!(
876				tracking_mode,
877				TrackingMode::Pprof | TrackingMode::PprofStats
878			)
879			.then(|| self.effective_pprof_sample_rate());
880			let track_stats = matches!(
881				tracking_mode,
882				TrackingMode::Stats | TrackingMode::PprofStats
883			);
884			if self.tracking_is_recursive(sample_rate, track_stats) {
885				self.inner_dealloc(ptr, layout);
886				return;
887			}
888
889			if sample_rate.is_none() {
890				self.inner_dealloc(ptr, layout);
891				if track_stats {
892					self.record_deallocation_stats(layout.size());
893				}
894				return;
895			}
896
897			enter_alloc(|| {
898				let record = self.take_allocation_record(ptr as usize);
899				self.inner_dealloc(ptr, layout);
900				self.finish_deallocation(record, layout.size(), track_stats);
901			});
902		}
903	}
904
905	unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
906		unsafe {
907			let tracking_mode = self.tracking_mode();
908			match tracking_mode {
909				TrackingMode::System => return System.realloc(ptr, layout, new_size),
910				#[cfg(feature = "allocator-jemalloc")]
911				TrackingMode::Jemalloc => return JEMALLOC_ALLOCATOR.realloc(ptr, layout, new_size),
912				#[cfg(feature = "allocator-mimalloc")]
913				TrackingMode::Mimalloc => return MIMALLOC_ALLOCATOR.realloc(ptr, layout, new_size),
914				_ => {},
915			}
916
917			let sample_rate = matches!(
918				tracking_mode,
919				TrackingMode::Pprof | TrackingMode::PprofStats
920			)
921			.then(|| self.effective_pprof_sample_rate());
922			let track_stats = matches!(
923				tracking_mode,
924				TrackingMode::Stats | TrackingMode::PprofStats
925			);
926			if self.tracking_is_recursive(sample_rate, track_stats) {
927				return self.inner_realloc(ptr, layout, new_size);
928			}
929
930			if sample_rate.is_none() {
931				let new_ptr = self.inner_realloc(ptr, layout, new_size);
932				if !new_ptr.is_null() && track_stats {
933					self.record_deallocation_stats(layout.size());
934					self.record_allocation_stats(new_size);
935				}
936				return new_ptr;
937			}
938
939			enter_alloc(|| {
940				let record = self.take_allocation_record(ptr as usize);
941				let new_ptr = self.inner_realloc(ptr, layout, new_size);
942				if !new_ptr.is_null() {
943					self.finish_deallocation(record, layout.size(), track_stats);
944					self.record_tracked_allocation(new_ptr, new_size, sample_rate, track_stats);
945				} else if let Some(record) = record {
946					self.restore_allocation_record(ptr as usize, record);
947				}
948				new_ptr
949			})
950		}
951	}
952}
953
954pub fn generate_pprof() -> anyhow::Result<Vec<u8>> {
955	if cfg!(feature = "allocator-jemalloc")
956		&& env::cached_allocator() == Some(AllocatorSelection::Jemalloc)
957		&& env::selected_pprof_backend() == PprofBackend::Native
958	{
959		return generate_jemalloc_pprof();
960	}
961
962	enter_alloc(|| {
963		let sample_rate = current_pprof_sample_rate();
964		let mut profile = StackProfile {
965			annotations: Default::default(),
966			stacks: Default::default(),
967			mappings: if let Some(m) = crate::pprof::MAPPINGS.as_deref() {
968				m.to_vec()
969			} else {
970				Default::default()
971			},
972		};
973
974		for entry in TRACE_MAP.iter() {
975			let sample_values = HeapSampleValues::from_allocations(entry.value(), sample_rate);
976			if sample_values.alloc_space == 0
977				&& sample_values.inuse_space == 0
978				&& sample_values.alloc_objects == 0
979				&& sample_values.inuse_objects == 0
980			{
981				continue;
982			}
983
984			profile.push_stack(
985				WeightedStack {
986					addrs: entry.key().addrs(),
987					values: smallvec::smallvec![
988						sample_values.alloc_objects,
989						sample_values.alloc_space,
990						sample_values.inuse_objects,
991						sample_values.inuse_space
992					],
993				},
994				None,
995			);
996		}
997
998		Ok(profile.to_pprof_with_period(
999			&[
1000				("alloc_objects", "count"),
1001				("alloc_space", "bytes"),
1002				("inuse_objects", "count"),
1003				("inuse_space", "bytes"),
1004			],
1005			("space", "bytes"),
1006			sample_rate as i64,
1007			None,
1008		))
1009	})
1010}
1011
1012/// Apply optional runtime configuration from environment variables.
1013///
1014/// Call this during application startup. Without `allocator-jemalloc`, this is a
1015/// no-op. With `allocator-jemalloc` and jemalloc selected, this applies
1016/// [`PPROF_BACKEND_ENV`] to jemalloc's runtime `prof.active` setting and
1017/// [`PPROF_SAMPLE_RATE_ENV`] to jemalloc's runtime `prof.lg_sample` setting.
1018/// The application is still responsible for enabling jemalloc profiling at
1019/// initialization time, for example with a `malloc_conf`/`MALLOC_CONF` that
1020/// includes `prof:true` and `prof_accum:true`.
1021#[cfg(feature = "allocator-jemalloc")]
1022pub fn configure() -> anyhow::Result<()> {
1023	configure_with_default(Allocator::System)
1024}
1025
1026/// Apply optional runtime configuration with an explicit fallback allocator.
1027///
1028/// Use this instead of [`configure`] when `PprofAlloc::with_default` is set to a
1029/// non-system allocator and startup configuration must run before the first
1030/// allocation.
1031#[cfg(feature = "allocator-jemalloc")]
1032pub fn configure_with_default(default: Allocator) -> anyhow::Result<()> {
1033	use tikv_jemalloc_ctl::raw;
1034
1035	if env::selected_allocator(default) != AllocatorSelection::Jemalloc {
1036		return Ok(());
1037	}
1038	if !native_jemalloc_profiling_supported() {
1039		return Ok(());
1040	}
1041
1042	let prof_enabled: bool = unsafe { raw::read(b"opt.prof\0") }?;
1043	if !prof_enabled {
1044		anyhow::bail!(
1045			"jemalloc profiling is unavailable; configure jemalloc with prof:true before allocator initialization"
1046		);
1047	}
1048
1049	let sample_rate = env_pprof_sample_rate(DEFAULT_PPROF_SAMPLE_RATE);
1050	let active = env::selected_pprof_backend() == PprofBackend::Native && sample_rate != 0;
1051	if let Some(lg_sample) = jemalloc_lg_prof_sample(sample_rate) {
1052		unsafe { raw::write(b"prof.reset\0", lg_sample) }?;
1053	}
1054	unsafe { raw::write(b"prof.active\0", active) }?;
1055	Ok(())
1056}
1057
1058#[cfg(not(feature = "allocator-jemalloc"))]
1059pub fn configure() -> anyhow::Result<()> {
1060	Ok(())
1061}
1062
1063#[cfg(not(feature = "allocator-jemalloc"))]
1064pub fn configure_with_default(_default: Allocator) -> anyhow::Result<()> {
1065	Ok(())
1066}
1067
1068/// Generate a pprof heap profile from jemalloc's native heap profiler.
1069///
1070/// This requires the `allocator-jemalloc` feature, a jemalloc build
1071/// with profiling support, and jemalloc runtime configuration such as
1072/// `prof:true` and `prof_active:true`.
1073#[cfg(feature = "allocator-jemalloc")]
1074pub fn generate_jemalloc_pprof() -> anyhow::Result<Vec<u8>> {
1075	use std::ffi::CString;
1076	use std::fs::File;
1077	use std::io::{BufReader, Read, Seek, SeekFrom};
1078	use std::os::fd::{FromRawFd, RawFd};
1079
1080	use anyhow::Context;
1081	use tikv_jemalloc_ctl::raw;
1082
1083	if !native_jemalloc_profiling_supported() {
1084		anyhow::bail!("jemalloc native profiling is disabled on musl builds");
1085	}
1086
1087	let prof_enabled: bool = unsafe { raw::read(b"opt.prof\0") }?;
1088	if !prof_enabled {
1089		anyhow::bail!(
1090			"jemalloc native profiling is unavailable; enable jemalloc profiling with prof:true"
1091		);
1092	}
1093
1094	let prof_active: bool = unsafe { raw::read(b"prof.active\0") }?;
1095	if !prof_active {
1096		anyhow::bail!(
1097			"jemalloc native profiling is inactive; enable prof_active:true or activate it before dumping"
1098		);
1099	}
1100
1101	let fd = unsafe {
1102		libc::syscall(
1103			libc::SYS_memfd_create,
1104			c"pprof-alloc-jemalloc-profile".as_ptr(),
1105			libc::MFD_CLOEXEC,
1106		)
1107	};
1108	if fd < 0 {
1109		return Err(std::io::Error::last_os_error())
1110			.context("failed to create memfd for jemalloc profile dump");
1111	}
1112
1113	let mut file = unsafe { File::from_raw_fd(fd as RawFd) };
1114	let path = CString::new(format!("/proc/self/fd/{fd}"))?;
1115
1116	unsafe {
1117		raw::write(b"prof.dump\0", path.as_ptr())?;
1118	}
1119
1120	file.seek(SeekFrom::Start(0))?;
1121	let mut dump = Vec::new();
1122	file.read_to_end(&mut dump)?;
1123
1124	let mappings = crate::pprof::MAPPINGS
1125		.as_deref()
1126		.map(|mappings| mappings.to_vec())
1127		.unwrap_or_default();
1128	let parsed =
1129		crate::pprof::parse_jemalloc_heap_profile(BufReader::new(dump.as_slice()), mappings)?;
1130	Ok(parsed.profile.to_pprof_with_period(
1131		&[
1132			("alloc_objects", "count"),
1133			("alloc_space", "bytes"),
1134			("inuse_objects", "count"),
1135			("inuse_space", "bytes"),
1136		],
1137		("space", "bytes"),
1138		parsed.sampling_rate,
1139		None,
1140	))
1141}
1142
1143#[cfg(all(feature = "allocator-jemalloc", target_env = "musl"))]
1144const fn native_jemalloc_profiling_supported() -> bool {
1145	false
1146}
1147
1148#[cfg(all(feature = "allocator-jemalloc", not(target_env = "musl")))]
1149const fn native_jemalloc_profiling_supported() -> bool {
1150	true
1151}
1152
1153#[cfg(feature = "allocator-jemalloc")]
1154fn jemalloc_lg_prof_sample(sample_rate: usize) -> Option<libc::size_t> {
1155	if sample_rate == 0 {
1156		return None;
1157	}
1158
1159	let lg_sample = usize::BITS - (sample_rate - 1).leading_zeros();
1160	Some(lg_sample.min(63) as libc::size_t)
1161}
1162
1163/// Generate a pprof heap profile from jemalloc's native heap profiler.
1164#[cfg(not(feature = "allocator-jemalloc"))]
1165pub fn generate_jemalloc_pprof() -> anyhow::Result<Vec<u8>> {
1166	anyhow::bail!(
1167		"jemalloc native profiling support is not compiled in; enable the `allocator-jemalloc` feature"
1168	)
1169}
1170
1171#[doc(hidden)]
1172#[macro_export]
1173macro_rules! __pprof_alloc_register_allocator_kind {
1174	($kind:expr) => {
1175		const _: () = {
1176			#[cfg(target_os = "linux")]
1177			#[used]
1178			#[unsafe(link_section = ".init_array")]
1179			static INIT_ARRAY: extern "C" fn() = {
1180				extern "C" fn init() {
1181					$crate::allocator::configure($kind);
1182				}
1183				init
1184			};
1185		};
1186	};
1187}
1188
1189#[macro_export]
1190macro_rules! declare_allocator_kind {
1191	($kind:expr $(;)?) => {
1192		$crate::__pprof_alloc_register_allocator_kind!($kind);
1193	};
1194}
1195
1196#[cfg(test)]
1197fn reset_tracking_state() {
1198	POINTER_MAP.clear();
1199	TRACE_MAP.clear();
1200	GLOBAL_STATS.allocated.store(0, Ordering::Relaxed);
1201	GLOBAL_STATS.freed.store(0, Ordering::Relaxed);
1202	GLOBAL_STATS.allocations.store(0, Ordering::Relaxed);
1203	GLOBAL_STATS.frees.store(0, Ordering::Relaxed);
1204	let _ = LOCAL_STATS.try_with(|stats| stats.reset());
1205	CURRENT_PPROF_SAMPLE_RATE.store(1, Ordering::Relaxed);
1206	env::reset_for_tests();
1207	let _ = NEXT_SAMPLE.try_with(|next_sample| next_sample.set(i64::MIN));
1208	let _ = NEXT_SAMPLE_RATE.try_with(|next_sample_rate| next_sample_rate.set(usize::MAX));
1209}
1210
1211#[cfg(test)]
1212mod tests {
1213	use super::*;
1214	use parking_lot::Mutex;
1215
1216	static TEST_GUARD: Mutex<()> = Mutex::new(());
1217
1218	#[test]
1219	fn allocation_stats_compute_in_use_values() {
1220		let stats = stats::Allocations {
1221			allocated: 4096,
1222			freed: 1024,
1223			allocations: 4,
1224			frees: 1,
1225		};
1226
1227		assert_eq!(stats.in_use_bytes(), 3072);
1228		assert_eq!(stats.in_use_allocations(), 3);
1229	}
1230
1231	#[test]
1232	fn sample_rate_one_records_every_profile_allocation() {
1233		let _guard = TEST_GUARD.lock();
1234		reset_tracking_state();
1235
1236		let alloc = PprofAlloc::new().with_pprof_sample_rate(1);
1237		alloc.record_allocation(0x1000, 128);
1238		alloc.record_allocation(0x2000, 64);
1239
1240		assert_eq!(current_pprof_sample_rate(), 1);
1241		assert_eq!(POINTER_MAP.len(), 2);
1242		assert_eq!(pprof_summary().alloc_space_bytes, 192);
1243		assert_eq!(pprof_summary().alloc_objects, 2);
1244	}
1245
1246	#[test]
1247	fn sample_rate_zero_disables_profile_allocation_records() {
1248		let _guard = TEST_GUARD.lock();
1249		reset_tracking_state();
1250
1251		let alloc = PprofAlloc::new().with_pprof_sample_rate(0).with_stats();
1252		alloc.record_allocation(0x1000, 128);
1253		alloc.record_deallocation(0x1000, 128);
1254
1255		assert_eq!(current_pprof_sample_rate(), 0);
1256		assert!(POINTER_MAP.is_empty());
1257		assert!(TRACE_MAP.is_empty());
1258		assert_eq!(allocation_stats().allocated, 128);
1259		assert_eq!(allocation_stats().freed, 128);
1260	}
1261
1262	#[test]
1263	fn sampled_heap_values_are_scaled_to_estimates() {
1264		let (count, size) = scale_heap_sample(1, 1024, 512);
1265
1266		assert_eq!(count, 1);
1267		assert!((1180..=1190).contains(&size));
1268	}
1269
1270	#[test]
1271	#[cfg(feature = "allocator-jemalloc")]
1272	fn pprof_backend_env_can_select_wrapper_backend() {
1273		let _guard = TEST_GUARD.lock();
1274		reset_tracking_state();
1275
1276		unsafe {
1277			std::env::set_var(ALLOCATOR_ENV, "jemalloc");
1278			std::env::set_var(PPROF_BACKEND_ENV, "wrapper");
1279		}
1280		env::reset_allocator_for_tests();
1281		env::reset_pprof_backend_for_tests();
1282		assert!(!PprofAlloc::new().native_pprof_selected());
1283
1284		unsafe {
1285			std::env::set_var(PPROF_BACKEND_ENV, "pprof-alloc");
1286		}
1287		env::reset_pprof_backend_for_tests();
1288		assert!(!PprofAlloc::new().native_pprof_selected());
1289
1290		unsafe {
1291			std::env::set_var(PPROF_BACKEND_ENV, "rust");
1292		}
1293		env::reset_pprof_backend_for_tests();
1294		assert!(!PprofAlloc::new().native_pprof_selected());
1295
1296		unsafe {
1297			std::env::remove_var(PPROF_BACKEND_ENV);
1298		}
1299		env::reset_allocator_for_tests();
1300		env::reset_pprof_backend_for_tests();
1301		assert!(PprofAlloc::new().native_pprof_selected());
1302
1303		unsafe {
1304			std::env::remove_var(ALLOCATOR_ENV);
1305		}
1306		reset_tracking_state();
1307	}
1308
1309	#[test]
1310	fn allocator_compat_env_is_used_as_fallback() {
1311		let _guard = TEST_GUARD.lock();
1312		reset_tracking_state();
1313
1314		unsafe {
1315			std::env::remove_var(ALLOCATOR_ENV);
1316			std::env::set_var("ALLOCATOR", "system");
1317		}
1318
1319		assert_eq!(
1320			env::selected_allocator(Allocator::Jemalloc),
1321			AllocatorSelection::System
1322		);
1323
1324		reset_tracking_state();
1325	}
1326
1327	#[test]
1328	#[cfg(feature = "allocator-jemalloc")]
1329	fn jemalloc_sample_rate_converts_to_log2_period() {
1330		assert_eq!(jemalloc_lg_prof_sample(0), None);
1331		assert_eq!(jemalloc_lg_prof_sample(1), Some(0));
1332		assert_eq!(jemalloc_lg_prof_sample(2), Some(1));
1333		assert_eq!(jemalloc_lg_prof_sample(3), Some(2));
1334		assert_eq!(jemalloc_lg_prof_sample(DEFAULT_PPROF_SAMPLE_RATE), Some(19));
1335	}
1336
1337	#[test]
1338	#[cfg(all(feature = "allocator-jemalloc", target_env = "musl"))]
1339	fn native_jemalloc_profiling_is_disabled_on_musl() {
1340		assert!(!native_jemalloc_profiling_supported());
1341		assert!(
1342			generate_jemalloc_pprof()
1343				.unwrap_err()
1344				.to_string()
1345				.contains("disabled on musl")
1346		);
1347	}
1348
1349	#[test]
1350	fn env_sample_rate_is_read_lazily() {
1351		let _guard = TEST_GUARD.lock();
1352		reset_tracking_state();
1353
1354		unsafe {
1355			std::env::set_var(PPROF_SAMPLE_RATE_ENV, "1");
1356		}
1357		let alloc = PprofAlloc::new().with_pprof_sample_rate_from_env(DEFAULT_PPROF_SAMPLE_RATE);
1358		alloc.record_allocation(0x1000, 128);
1359		unsafe {
1360			std::env::remove_var(PPROF_SAMPLE_RATE_ENV);
1361		}
1362
1363		assert_eq!(current_pprof_sample_rate(), 1);
1364		assert_eq!(POINTER_MAP.len(), 1);
1365	}
1366
1367	#[test]
1368	fn env_sample_rate_uses_configured_default_when_unset() {
1369		let _guard = TEST_GUARD.lock();
1370		reset_tracking_state();
1371
1372		unsafe {
1373			std::env::remove_var(PPROF_SAMPLE_RATE_ENV);
1374		}
1375		let alloc = PprofAlloc::new().with_pprof_sample_rate_from_env(1);
1376		alloc.record_allocation(0x1000, 128);
1377
1378		assert_eq!(current_pprof_sample_rate(), 1);
1379		assert_eq!(POINTER_MAP.len(), 1);
1380	}
1381
1382	#[test]
1383	fn deallocation_updates_live_profile_bytes() {
1384		let _guard = TEST_GUARD.lock();
1385		reset_tracking_state();
1386
1387		let alloc = PprofAlloc::new().with_pprof().with_stats();
1388		let trace = HashedBacktrace::capture();
1389
1390		alloc.record_allocation_with_trace(0x1000, 128, trace.clone());
1391		alloc.record_allocation_with_trace(0x2000, 64, trace.clone());
1392		alloc.record_deallocation(0x1000, 128);
1393
1394		let trace_stats = TRACE_MAP.get(&trace).unwrap();
1395		assert_eq!(trace_stats.allocated, 192);
1396		assert_eq!(trace_stats.freed, 128);
1397		assert_eq!(trace_stats.allocations, 2);
1398		assert_eq!(trace_stats.frees, 1);
1399		assert_eq!(trace_stats.in_use_bytes(), 64);
1400		assert!(POINTER_MAP.contains_key(&0x2000));
1401		assert!(!POINTER_MAP.contains_key(&0x1000));
1402	}
1403
1404	#[test]
1405	fn coarse_stats_track_allocations_and_frees() {
1406		let _guard = TEST_GUARD.lock();
1407		reset_tracking_state();
1408
1409		let alloc = PprofAlloc::new().with_stats();
1410		alloc.record_allocation(0x5000, 48);
1411		alloc.record_deallocation(0x5000, 48);
1412
1413		assert_eq!(
1414			allocation_stats(),
1415			stats::Allocations {
1416				allocated: 48,
1417				freed: 48,
1418				allocations: 1,
1419				frees: 1,
1420			}
1421		);
1422	}
1423
1424	#[test]
1425	fn reallocation_updates_live_bytes_and_pointer_ownership() {
1426		let _guard = TEST_GUARD.lock();
1427		reset_tracking_state();
1428
1429		let alloc = PprofAlloc::new().with_pprof_sample_rate(1);
1430		alloc.record_allocation_with_trace(0x3000, 32, HashedBacktrace::capture());
1431		alloc.record_reallocation(0x3000, 32, 0x4000, 96);
1432
1433		let total_live_bytes: u64 = TRACE_MAP
1434			.iter()
1435			.map(|entry| entry.value().in_use_bytes())
1436			.sum();
1437		assert_eq!(total_live_bytes, 96);
1438		assert_eq!(POINTER_MAP.get(&0x4000).unwrap().size, 96);
1439		assert!(!POINTER_MAP.contains_key(&0x3000));
1440	}
1441
1442	#[test]
1443	fn snapshot_reports_current_pprof_summary() {
1444		let _guard = TEST_GUARD.lock();
1445		reset_tracking_state();
1446
1447		let alloc = PprofAlloc::new().with_pprof();
1448		let trace = HashedBacktrace::capture();
1449
1450		alloc.record_allocation_with_trace(0x1000, 128, trace.clone());
1451		alloc.record_allocation_with_trace(0x2000, 64, trace);
1452		alloc.record_deallocation(0x1000, 128);
1453
1454		let snapshot = snapshot();
1455		assert_eq!(snapshot.capture_mode, capture_mode());
1456		assert_eq!(
1457			snapshot.pprof,
1458			PprofSummary {
1459				total_stacks: 1,
1460				live_stacks: 1,
1461				alloc_space_bytes: 192,
1462				inuse_space_bytes: 64,
1463				alloc_objects: 2,
1464				inuse_objects: 1,
1465			}
1466		);
1467	}
1468}