1pub 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
30pub const DEFAULT_PPROF_SAMPLE_RATE: usize = 512 * 1024;
36
37pub 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
50pub struct PprofAlloc<A = System> {
56 inner: A,
57 pprof: bool,
59 stats: bool,
61 pprof_sample_rate: usize,
63 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)]
124pub struct PprofSummary {
129 pub total_stacks: u64,
131 pub live_stacks: u64,
133 pub alloc_space_bytes: u64,
135 pub inuse_space_bytes: u64,
137 pub alloc_objects: u64,
139 pub inuse_objects: u64,
141}
142
143#[derive(Clone, Debug, Serialize)]
144pub struct MemorySnapshot {
149 pub captured_at_unix_ms: u64,
151 pub capture_mode: CaptureMode,
153 pub allocation_stats: stats::Allocations,
155 pub pprof: PprofSummary,
157 pub allocator: allocator::AllocatorSnapshot,
159 pub cgroup: Option<stats::cgroups::MemoryStat>,
161 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 pub const fn new() -> Self {
177 Self::from_allocator(System)
178 }
179}
180
181impl<A> PprofAlloc<A> {
182 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 pub const fn with_pprof(mut self) -> Self {
195 self.pprof = true;
196 self
197 }
198
199 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 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 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 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
341pub fn allocation_stats() -> stats::Allocations {
346 GLOBAL_STATS.snapshot()
347}
348
349pub 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
486pub 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}