1pub 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
33pub 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
74pub struct PprofAlloc {
79 pprof: bool,
81 stats: bool,
83 pprof_sample_rate: usize,
85 pprof_sample_rate_from_env: bool,
87 resolved_pprof_sample_rate: AtomicUsize,
89 tracking_mode: AtomicU8,
91 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)]
232pub struct PprofSummary {
237 pub total_stacks: u64,
239 pub live_stacks: u64,
241 pub alloc_space_bytes: u64,
243 pub inuse_space_bytes: u64,
245 pub alloc_objects: u64,
247 pub inuse_objects: u64,
249}
250
251#[derive(Clone, Debug, Serialize)]
252pub struct MemorySnapshot {
257 pub captured_at_unix_ms: u64,
259 pub capture_mode: CaptureMode,
261 pub allocation_stats: stats::Allocations,
263 pub pprof: PprofSummary,
265 pub allocator: allocator::AllocatorSnapshot,
267 pub cgroup: Option<stats::cgroups::MemoryStat>,
269 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 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 pub const fn with_default(mut self, allocator: Allocator) -> Self {
298 self.default_allocator = allocator;
299 self
300 }
301
302 pub const fn with_pprof(mut self) -> Self {
308 self.pprof = true;
309 self
310 }
311
312 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 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 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 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
619pub 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
657pub 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
756pub 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#[cfg(feature = "allocator-jemalloc")]
1022pub fn configure() -> anyhow::Result<()> {
1023 configure_with_default(Allocator::System)
1024}
1025
1026#[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#[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#[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}