mimalloc_pprof/lib.rs
1//! Rust global allocator support for the in-tree mimalloc build.
2//!
3//! ```no_run
4//! use mimalloc_pprof::{prof, MiMalloc};
5//! #[global_allocator] static ALLOCATOR: MiMalloc = MiMalloc;
6//! # fn main() -> std::io::Result<()> {
7//! prof::start(512 * 1024);
8//! prof::dump_file(std::path::Path::new("heap.prof"))?;
9//! # Ok(()) }
10//! ```
11//!
12//! See the README's Rust integration guide for frame-pointer and line-table
13//! build flags. Open the resulting profile with `pprof -http=: app.exe heap.prof`.
14
15use core::alloc::{GlobalAlloc, Layout};
16use core::ffi::c_void;
17use std::ffi::CString;
18use std::path::PathBuf;
19
20pub mod sys;
21
22/// A `#[global_allocator]` implementation backed by mimalloc.
23pub struct MiMalloc;
24
25unsafe impl GlobalAlloc for MiMalloc {
26 unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
27 sys::mi_malloc_aligned(layout.size(), layout.align()).cast()
28 }
29
30 unsafe fn dealloc(&self, ptr: *mut u8, _layout: Layout) {
31 sys::mi_free(ptr.cast::<c_void>());
32 }
33
34 unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
35 sys::mi_realloc_aligned(ptr.cast::<c_void>(), new_size, layout.align()).cast()
36 }
37
38 unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
39 sys::mi_zalloc_aligned(layout.size(), layout.align()).cast()
40 }
41}
42
43/// Allocate `size` bytes from mimalloc's raw-OS-layer "unwrapped" path.
44///
45/// Thin wrapper around `mi_unwrapped_malloc` (include/mimalloc/memory-events.h):
46/// backed directly by `_mi_os_alloc_aligned`, never by the hooked `mi_malloc`
47/// family. Page granular, so this is not meant for hot-path/small allocations
48/// — it exists for low-level instrumentation and recursion avoidance (e.g.
49/// scratch storage for a memory-change callback that must not recursively
50/// enter mimalloc). Excluded from normal mimalloc allocation stats and from
51/// the memory-change accounting.
52///
53/// Returns a null pointer on failure (including invalid `alignment`; see
54/// `# Safety` below).
55///
56/// # Safety
57///
58/// - `alignment` must be `0` (treated as `align_of::<*const ()>()`, i.e.
59/// pointer size) or a power of two. A non-power-of-two, non-zero alignment
60/// is a validated input on the C side: `mi_unwrapped_malloc` returns a null
61/// pointer rather than invoking undefined behavior, but callers should not
62/// rely on that as anything other than a defined-failure contract — treat
63/// the alignment argument as a precondition to get right, not a value to
64/// probe.
65/// - The returned pointer, if non-null, must be passed only to
66/// [`unwrapped_free`] or [`unwrapped_realloc`] — never to `mi_free`, this
67/// crate's [`MiMalloc`] allocator, or Rust's global allocator, and vice
68/// versa (a pointer from `mi_malloc`/the Rust global allocator must never
69/// be passed to [`unwrapped_free`]/[`unwrapped_realloc`]). Mixing these
70/// families corrupts allocator-internal bookkeeping.
71/// - The memory is uninitialized; reading it before writing is undefined
72/// behavior, as with any raw allocation.
73pub unsafe fn unwrapped_malloc(size: usize, alignment: usize) -> *mut u8 {
74 unsafe { sys::mi_unwrapped_malloc(size, alignment).cast() }
75}
76
77/// Free a pointer returned by [`unwrapped_malloc`] or [`unwrapped_realloc`].
78///
79/// Thin wrapper around `mi_unwrapped_free` (include/mimalloc/memory-events.h).
80///
81/// # Safety
82///
83/// - `p` must be either a null pointer (a documented, safe no-op on the C
84/// side) or a pointer previously returned by [`unwrapped_malloc`] or
85/// [`unwrapped_realloc`] that has not already been freed.
86/// - `p` must never have come from `mi_malloc`, this crate's [`MiMalloc`]
87/// allocator, or Rust's global allocator — passing such a pointer here is
88/// undefined behavior (the "unwrapped" and normal allocation families use
89/// incompatible header layouts and are validated by a magic-number check
90/// that a foreign pointer will not satisfy).
91pub unsafe fn unwrapped_free(p: *mut u8) {
92 unsafe { sys::mi_unwrapped_free(p.cast()) }
93}
94
95/// Resize a pointer returned by [`unwrapped_malloc`] or [`unwrapped_realloc`].
96///
97/// Thin wrapper around `mi_unwrapped_realloc` (include/mimalloc/memory-events.h).
98/// If `p` is null, this behaves like [`unwrapped_malloc`]. If `new_size` is
99/// `0`, this frees `p` (like [`unwrapped_free`]) and returns a null pointer.
100/// Otherwise the existing contents are copied into a freshly allocated
101/// unwrapped block (up to `min(old payload size, new_size)` bytes) and `p` is
102/// freed; `p` must not be used again after this call, whether or not it
103/// returns null.
104///
105/// Returns a null pointer on failure (including invalid `alignment`; see
106/// [`unwrapped_malloc`]'s `# Safety` section), in which case `p` is left
107/// valid and unfreed.
108///
109/// # Safety
110///
111/// - `p` must be either a null pointer or a pointer previously returned by
112/// [`unwrapped_malloc`] or [`unwrapped_realloc`] that has not already been
113/// freed, per the same family-isolation rule as [`unwrapped_free`].
114/// - `alignment` has the same power-of-two-or-zero contract as
115/// [`unwrapped_malloc`].
116/// - After this call, `p` must not be read, written, or freed again — treat
117/// it as consumed regardless of whether the return value is null.
118pub unsafe fn unwrapped_realloc(p: *mut u8, new_size: usize, alignment: usize) -> *mut u8 {
119 unsafe { sys::mi_unwrapped_realloc(p.cast(), new_size, alignment).cast() }
120}
121
122/// Grow or shrink an allocation, zeroing any newly-exposed tail.
123///
124/// Thin wrapper around `mi_rezalloc`. This is the operation Rust's [`GlobalAlloc`]
125/// cannot express — that trait has no `grow_zeroed` — so without it a caller has to
126/// grow and then `memset` by hand, repeating work the allocator has already done, and
127/// (with zero-tracking) work it may be able to skip entirely.
128///
129/// # What is actually zeroed
130///
131/// **Not** `[old_requested_size, new_size)`. mimalloc measures from the block's old
132/// *usable* size, so the slack between what you asked for and what the block actually
133/// holds is left untouched:
134///
135/// ```text
136/// requested 64 -> usable 80 -> rezalloc to 70
137/// bytes [64,70) are NOT zeroed: the grow was served in place, within the old block
138/// ```
139///
140/// The guarantee is: everything past [`usable_size`] of the *original* block is zero.
141/// If you need a specific range zeroed, capture [`usable_size`] before the call and
142/// zero the remainder yourself.
143///
144/// (This is documented so precisely because a fuzz harness asserted the intuitive
145/// version and was falsified within seconds — see issue #87.)
146///
147/// # Safety
148///
149/// - `p` must be null, or a pointer from the **plain** allocation family — the global
150/// allocator, [`sys::mi_malloc`], or a previous [`rezalloc`]/[`recalloc`] — that has
151/// not been freed.
152/// - **Not interchangeable with [`unwrapped_malloc`]/[`unwrapped_realloc`].** Those
153/// place a header before the pointer, so passing one here fails the pointer check
154/// (`mi_usable_size: invalid pointer`) rather than working by accident.
155/// - After this call `p` is consumed: do not read, write, or free it again, whether or
156/// not the return value is null.
157/// - On failure a null pointer is returned and `p` is left valid and unfreed.
158pub unsafe fn rezalloc(p: *mut u8, new_size: usize) -> *mut u8 {
159 unsafe { sys::mi_rezalloc(p.cast(), new_size).cast() }
160}
161
162/// Grow or shrink an allocation to `count * size` bytes, zeroing any newly-exposed tail.
163///
164/// The [`rezalloc`] contract applies, including what is and is not zeroed. Thin wrapper
165/// around `mi_recalloc`; the element-count form exists to mirror `calloc`.
166///
167/// # Safety
168///
169/// Same contract as [`rezalloc`].
170pub unsafe fn recalloc(p: *mut u8, count: usize, size: usize) -> *mut u8 {
171 unsafe { sys::mi_recalloc(p.cast(), count, size).cast() }
172}
173
174/// Try to grow an allocation **in place**, without moving it.
175///
176/// Returns a null pointer if the block cannot be extended where it is — in which case
177/// `p` remains valid and unchanged, unlike [`rezalloc`]. Useful when moving would be
178/// more expensive than falling back to a different strategy.
179///
180/// # Safety
181///
182/// - `p` must be a pointer from this allocator that has not been freed.
183/// - Unlike [`rezalloc`], `p` is **not** consumed: on failure it is still live and must
184/// still be freed.
185pub unsafe fn expand(p: *mut u8, new_size: usize) -> *mut u8 {
186 unsafe { sys::mi_expand(p.cast(), new_size).cast() }
187}
188
189/// Bytes actually available in an allocation, which may exceed what was requested.
190///
191/// # Safety
192///
193/// `p` must be a live pointer from this allocator.
194pub unsafe fn usable_size(p: *const u8) -> usize {
195 unsafe { sys::mi_usable_size(p.cast()) }
196}
197
198/// Turn on sampled heap profiling at the default sample rate.
199///
200/// Convenience entry point for wiring profiling to a command-line flag:
201///
202/// ```no_run
203/// # let args_profile_heap = true;
204/// if args_profile_heap {
205/// mimalloc_pprof::enable_heap_profiling();
206/// }
207/// ```
208///
209/// Uses the built-in default rate (one sample per ~512 KiB allocated;
210/// `MIMALLOC_PROF_SAMPLE_RATE` still overrides it). Call [`prof::start`]
211/// instead to pick a rate programmatically. Allocations made before this
212/// call — including process-startup and static initialization — are not
213/// tracked; profiles reflect steady-state behavior from this point on,
214/// which is the usual intent for an opt-in CLI switch. To capture startup
215/// as well, set `MIMALLOC_PROF=1` in the environment instead.
216///
217/// Returns `false` if profiling was already enabled (the earlier session,
218/// and its sample rate, stay active).
219pub fn enable_heap_profiling() -> bool {
220 prof::start(0)
221}
222
223/// How [`ProfConfig`] fields interact with the profiler's environment
224/// variables and `mi_option_*` settings.
225///
226/// Mirrors `mi_prof_config_mode_t` (include/mimalloc/profile.h); see that
227/// header for the full FALLBACK/OVERRIDE semantics, including the caveat
228/// that in `Override` mode `accum == false`, `dump_format == Text`, and
229/// `max_profiler_bytes == None` cannot be distinguished from "unset" and so
230/// always fall back to env-then-default rather than forcing the off/default
231/// value.
232#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
233pub enum ProfConfigMode {
234 /// Struct fields are used only where the corresponding env var / option is absent.
235 #[default]
236 Fallback,
237 /// Non-default struct fields win over env vars / options (see the caveat above).
238 Override,
239}
240
241/// Output format for [`ProfConfig::dump_at_exit`].
242///
243/// Mirrors `MI_PROF_FORMAT_TEXT` / `MI_PROF_FORMAT_PROTO` (include/mimalloc/profile.h).
244#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
245pub enum DumpFormat {
246 /// Legacy "heap profile:" text format (see [`prof::dump_to_vec`]).
247 #[default]
248 Text,
249 /// Binary pprof `profile.proto` format (see [`prof::dump_proto_to_vec`]).
250 Proto,
251}
252
253/// Ergonomic, Rust-facing sibling of `mi_prof_config_t`
254/// (include/mimalloc/profile.h) for [`enable_heap_profiling_with`].
255///
256/// Fields mirror the C struct one-for-one, but trade its 0/NULL-means-unset
257/// raw-integer conventions for `Option<T>` and enums where that reads
258/// better. `#[non_exhaustive]` + `Default` keeps future fields additive:
259/// build from `Default::default()` and set the fields you need, e.g.
260///
261/// ```
262/// use mimalloc_pprof::ProfConfig;
263/// let mut config = ProfConfig::default();
264/// config.sample_interval = Some(4096);
265/// ```
266///
267/// (Within this crate, struct-update syntax like
268/// `ProfConfig { sample_interval: Some(4096), ..Default::default() }` also
269/// works; `#[non_exhaustive]` only blocks struct-literal construction from
270/// *other* crates, so new fields stay non-breaking for them.)
271#[non_exhaustive]
272#[derive(Debug, Clone, Default)]
273pub struct ProfConfig {
274 /// See [`ProfConfigMode`].
275 pub mode: ProfConfigMode,
276 /// Average bytes between samples. `None` = env/default (512 KiB).
277 pub sample_interval: Option<usize>,
278 /// Budget (bytes) for profiler-internal persistent sampling state
279 /// (sample records, the stack intern table, interned stack entries).
280 /// `None` = unbudgeted (cap-bounded only).
281 pub max_profiler_bytes: Option<usize>,
282 /// `None` = nondeterministic.
283 pub seed: Option<u64>,
284 pub accum: bool,
285 /// `None` = default (32); compile cap 128.
286 pub max_stack_depth: Option<usize>,
287 /// Path to dump the profile to at process exit. `None` = no exit dump.
288 pub dump_at_exit: Option<PathBuf>,
289 /// Format used for the exit dump. Ignored if `dump_at_exit` is `None`.
290 pub dump_format: DumpFormat,
291}
292
293/// Turn on sampled heap profiling using a struct-based configuration.
294///
295/// Sibling of [`enable_heap_profiling`] for callers that need more than a
296/// single sample rate -- e.g. seeding the sampler, capping profiler-arena
297/// memory, or registering an exit-time dump path/format. See [`ProfConfig`]
298/// and, for the full FALLBACK/OVERRIDE semantics, `mi_prof_config_mode_t` in
299/// `include/mimalloc/profile.h`.
300///
301/// Returns `false` if profiling was already enabled (the earlier session
302/// stays active), or if `config.dump_at_exit` is set but is not
303/// representable as a NUL-free C string (non-UTF-8 or an embedded NUL byte)
304/// -- in that case `mi_prof_start_ex` is never called.
305pub fn enable_heap_profiling_with(config: &ProfConfig) -> bool {
306 // `dump_at_exit_c` must outlive the `mi_prof_start_ex` call below since
307 // `raw.dump_at_exit` borrows its bytes; it does, as both live to the end
308 // of this function.
309 let dump_at_exit_c: Option<CString> = match &config.dump_at_exit {
310 Some(path) => match path.to_str().and_then(|s| CString::new(s).ok()) {
311 Some(c) => Some(c),
312 None => return false,
313 },
314 None => None,
315 };
316
317 let mut raw: sys::mi_prof_config_t = unsafe { core::mem::zeroed() };
318 raw.size = core::mem::size_of::<sys::mi_prof_config_t>();
319 raw.version = sys::MI_PROF_CONFIG_VERSION;
320 raw.mode = match config.mode {
321 ProfConfigMode::Fallback => sys::MI_PROF_CONFIG_FALLBACK,
322 ProfConfigMode::Override => sys::MI_PROF_CONFIG_OVERRIDE,
323 };
324 raw.sample_interval = config.sample_interval.unwrap_or(0);
325 raw.max_profiler_bytes = config.max_profiler_bytes.unwrap_or(0);
326 raw.seed = config.seed.unwrap_or(0);
327 raw.accum = config.accum;
328 raw.max_stack_depth = config.max_stack_depth.unwrap_or(0);
329 raw.dump_at_exit = dump_at_exit_c
330 .as_ref()
331 .map_or(core::ptr::null(), |c| c.as_ptr());
332 raw.dump_format = match config.dump_format {
333 DumpFormat::Text => sys::MI_PROF_FORMAT_TEXT,
334 DumpFormat::Proto => sys::MI_PROF_FORMAT_PROTO,
335 };
336
337 unsafe { sys::mi_prof_start_ex(&raw) }
338}
339
340/// Safe controls for mimalloc's sampled heap profiler.
341pub mod prof {
342 use core::ffi::{c_char, c_void};
343 use std::ffi::{CStr, CString};
344 use std::io;
345 use std::panic::{catch_unwind, AssertUnwindSafe};
346 use std::path::Path;
347
348 use crate::sys;
349
350 pub fn start(sample_rate: usize) -> bool {
351 unsafe { sys::mi_prof_start(sample_rate) }
352 }
353 #[doc(hidden)]
354 pub fn start_seeded(sample_rate: usize, seed: u64) -> bool {
355 unsafe { sys::mi_prof_start_seeded(sample_rate, seed) }
356 }
357 pub fn stop() {
358 unsafe { sys::mi_prof_stop() }
359 }
360 pub fn is_enabled() -> bool {
361 unsafe { sys::mi_prof_is_enabled() }
362 }
363 pub fn reset() {
364 unsafe { sys::mi_prof_reset() }
365 }
366
367 pub fn dump_file(path: &Path) -> io::Result<()> {
368 let path = path.to_str().ok_or_else(|| {
369 io::Error::new(io::ErrorKind::InvalidInput, "profile path is not UTF-8")
370 })?;
371 let path = CString::new(path).map_err(|_| {
372 io::Error::new(io::ErrorKind::InvalidInput, "profile path contains NUL")
373 })?;
374 if unsafe { sys::mi_prof_dump(path.as_ptr()) } {
375 Ok(())
376 } else {
377 Err(io::Error::last_os_error())
378 }
379 }
380
381 unsafe extern "C" fn write_cb(arg: *mut c_void, buf: *const c_char, len: usize) {
382 let out = &mut *(arg as *mut Vec<u8>);
383 out.extend_from_slice(core::slice::from_raw_parts(buf.cast::<u8>(), len));
384 }
385
386 /// Serialize the current heap profile without holding the profiler lock.
387 pub fn dump_to_vec() -> Vec<u8> {
388 let mut out = Vec::new();
389 let ok =
390 unsafe { sys::mi_prof_dump_writer(Some(write_cb), (&mut out as *mut Vec<u8>).cast()) };
391 if ok {
392 out
393 } else {
394 Vec::new()
395 }
396 }
397
398 /// Serialize the current heap profile as a binary pprof `profile.proto`
399 /// `Profile` message (see [google/pprof's `profile.proto`][proto]),
400 /// without holding the profiler lock.
401 ///
402 /// Sample values are pre-scaled the same way Go's `runtime/pprof` scales
403 /// legacy heap samples (the `protomem.go` convention: `alloc_objects`,
404 /// `alloc_space`, `inuse_objects`, `inuse_space`, each already corrected
405 /// for Poisson sampling bias rather than left for a downstream tool to
406 /// rescale). The `Mapping` table is included, so external symbolizers
407 /// need only the binary — no text parsing of a "heap profile:" header or
408 /// a `MAPPED_LIBRARIES:` section. This is the compact, machine-oriented
409 /// counterpart to [`dump_to_vec`]'s text format, intended for API and
410 /// transport use (issue #23) where a `pprof`-compatible tool consumes
411 /// the bytes directly.
412 ///
413 /// [proto]: https://github.com/google/pprof/blob/main/proto/profile.proto
414 pub fn dump_proto_to_vec() -> Vec<u8> {
415 let mut out = Vec::new();
416 let ok = unsafe {
417 sys::mi_prof_dump_proto_writer(Some(write_cb), (&mut out as *mut Vec<u8>).cast())
418 };
419 if ok {
420 out
421 } else {
422 Vec::new()
423 }
424 }
425
426 /// Write the current heap profile to `path` in `profile.proto` format.
427 ///
428 /// See [`dump_proto_to_vec`] for the format details.
429 pub fn dump_proto_file(path: &Path) -> io::Result<()> {
430 let path = path.to_str().ok_or_else(|| {
431 io::Error::new(io::ErrorKind::InvalidInput, "profile path is not UTF-8")
432 })?;
433 let path = CString::new(path).map_err(|_| {
434 io::Error::new(io::ErrorKind::InvalidInput, "profile path contains NUL")
435 })?;
436 if unsafe { sys::mi_prof_dump_proto(path.as_ptr()) } {
437 Ok(())
438 } else {
439 Err(io::Error::last_os_error())
440 }
441 }
442
443 /// Snapshot of `mi_prof_stats_get`'s counters, translated from the raw
444 /// sys struct into plain Rust types.
445 #[derive(Debug, Clone, Default)]
446 pub struct ProfStats {
447 pub enabled: bool,
448 pub accum: bool,
449 pub sample_rate: usize,
450 pub live_samples: usize,
451 pub live_bytes: usize,
452 pub accum_samples: usize,
453 pub accum_bytes: usize,
454 pub unique_stacks: usize,
455 pub arena_committed: usize,
456 pub stack_table_overflows: usize,
457 /// Count of ALL dropped samples (record-alloc failure, stack-intern
458 /// failure, including the stack-table cap); a superset of
459 /// `stack_table_overflows`, so `dropped_samples >=
460 /// stack_table_overflows` always.
461 pub dropped_samples: usize,
462 /// Allocator-level ("ground truth") counters, read from the mimalloc v3
463 /// engine's per-heap statistics at the time of the call. Every field
464 /// above is *sampled*; these are exact, so comparing them against
465 /// `live_bytes` measures the sampler's error directly -- which is what
466 /// makes an assertion on a sampled profile meaningful in a test.
467 pub heap: HeapStats,
468 }
469
470 /// Exact allocator counters accompanying a [`ProfStats`] reading.
471 ///
472 /// These come from mimalloc v3's per-heap statistics
473 /// (`mi_heap_stats_get`/`mi_subproc_stats_get`), which the v2 engine did not
474 /// expose. They are valid even when the profiler is stopped.
475 #[derive(Debug, Clone, Default)]
476 pub struct HeapStats {
477 /// Bytes currently committed from the OS.
478 pub committed: usize,
479 /// Bytes currently reserved from the OS (always `>= committed`).
480 pub reserved: usize,
481 /// Bytes the application actually requested and still holds.
482 ///
483 /// Only maintained when the C library was built with `MI_STAT >= 2`;
484 /// otherwise this is 0. Check [`HeapStats::detailed`] before using it.
485 pub malloc_requested: usize,
486 /// Live mimalloc pages.
487 pub pages: usize,
488 /// Pages abandoned by exited threads.
489 pub pages_abandoned: usize,
490 /// Live first-class heaps.
491 pub heaps: usize,
492 /// Live thread-local heaps. The main thread's statically-initialized
493 /// theap is not counted, so a single-threaded process reports 0.
494 pub theaps: usize,
495 /// Cumulative bytes purged back to the OS.
496 pub purged: usize,
497 /// Whether the C library was built with `MI_STAT >= 2` ("detailed"
498 /// statistics), which upstream enables by default only for debug
499 /// builds. [`HeapStats::malloc_requested`] is maintained only at that
500 /// level; every other field here is maintained at any level.
501 ///
502 /// Without this flag you cannot tell "the application allocated
503 /// nothing" from "this build does not track that counter".
504 pub detailed: bool,
505 }
506
507 /// Read the profiler's current counters via `mi_prof_stats_get`.
508 ///
509 /// Returns `ProfStats::default()` (all zero/false) if the call fails,
510 /// e.g. because the sys struct's `size`/`version` header does not match
511 /// what the linked mimalloc build expects.
512 pub fn stats() -> ProfStats {
513 let mut raw: sys::mi_prof_stats_t = unsafe { core::mem::zeroed() };
514 raw.size = core::mem::size_of::<sys::mi_prof_stats_t>();
515 raw.version = sys::MI_PROF_STAT_VERSION;
516 if unsafe { sys::mi_prof_stats_get(&mut raw) } {
517 ProfStats {
518 enabled: raw.enabled,
519 accum: raw.accum,
520 sample_rate: raw.sample_rate,
521 live_samples: raw.live_samples,
522 live_bytes: raw.live_bytes,
523 accum_samples: raw.accum_samples,
524 accum_bytes: raw.accum_bytes,
525 unique_stacks: raw.unique_stacks,
526 arena_committed: raw.arena_committed,
527 stack_table_overflows: raw.stack_table_overflows,
528 dropped_samples: raw.dropped_samples,
529 heap: HeapStats {
530 committed: raw.heap_committed,
531 reserved: raw.heap_reserved,
532 malloc_requested: raw.heap_malloc_requested,
533 pages: raw.heap_pages,
534 pages_abandoned: raw.heap_pages_abandoned,
535 heaps: raw.heap_count,
536 theaps: raw.theap_count,
537 purged: raw.heap_purged,
538 detailed: raw.heap_stats_detailed,
539 },
540 }
541 } else {
542 ProfStats::default()
543 }
544 }
545
546 /// One sampled call stack, copied out of the profiler by [`samples`].
547 #[derive(Debug, Clone)]
548 pub struct Sample {
549 pub stack: Vec<usize>,
550 pub live_objects: usize,
551 pub live_bytes: usize,
552 pub accum_objects: usize,
553 pub accum_bytes: usize,
554 }
555
556 impl Sample {
557 /// Estimate the un-sampled byte volume behind this sample.
558 ///
559 /// Mirrors pprof's legacy heap-sample scaling formula
560 /// (`scaleHeapSample` in pprof's `profile/legacy_profile.go`),
561 /// which corrects for the bias a Poisson sampling process with mean
562 /// interval `sample_rate` introduces toward larger allocations.
563 pub fn estimated_bytes(&self, sample_rate: usize) -> u64 {
564 if self.live_objects == 0 || self.live_bytes == 0 {
565 return 0;
566 }
567 if sample_rate <= 1 {
568 return self.live_bytes as u64;
569 }
570 let avg = self.live_bytes as f64 / self.live_objects as f64;
571 let scale = 1.0 / (1.0 - (-avg / sample_rate as f64).exp());
572 (self.live_bytes as f64 * scale) as u64
573 }
574 }
575
576 /// Frees the snapshot handle on drop, including on unwind, so a panic
577 /// partway through collection never leaks profiler-arena memory.
578 struct SnapshotGuard(*mut sys::mi_prof_snapshot_t);
579
580 impl Drop for SnapshotGuard {
581 fn drop(&mut self) {
582 unsafe { sys::mi_prof_snapshot_free(self.0) }
583 }
584 }
585
586 unsafe extern "C" fn collect_visitor(
587 info: *const sys::mi_prof_sample_info_t,
588 arg: *mut c_void,
589 ) -> bool {
590 let result = catch_unwind(AssertUnwindSafe(|| unsafe {
591 let out = &mut *(arg as *mut Vec<Sample>);
592 let info = &*info;
593 let stack = (0..info.depth)
594 .map(|i| *info.stack.add(i) as usize)
595 .collect();
596 out.push(Sample {
597 stack,
598 live_objects: info.live_objects,
599 live_bytes: info.live_bytes,
600 accum_objects: info.accum_objects,
601 accum_bytes: info.accum_bytes,
602 });
603 }));
604 result.is_ok()
605 }
606
607 /// Collect a point-in-time copy of every live sampled stack.
608 ///
609 /// This snapshots under the profiler lock via `mi_prof_snapshot_new`,
610 /// then walks and frees the snapshot outside that lock. Using
611 /// `mi_prof_visit` directly here would run the (allocating) collection
612 /// below from inside the visitor while the profiler lock is held,
613 /// risking reentrant profiler-hook allocation and deadlock — the
614 /// reentrancy hazard the snapshot API exists to avoid (issue #2,
615 /// decisions 11-13).
616 pub fn samples() -> Vec<Sample> {
617 let snap = unsafe { sys::mi_prof_snapshot_new() };
618 if snap.is_null() {
619 return Vec::new();
620 }
621 let guard = SnapshotGuard(snap);
622 let mut out: Vec<Sample> = Vec::new();
623 unsafe {
624 sys::mi_prof_snapshot_visit(
625 guard.0,
626 collect_visitor,
627 (&mut out as *mut Vec<Sample>).cast(),
628 );
629 }
630 out
631 }
632
633 /// One loaded module (shared library or the main executable), copied out
634 /// of the OS module list by [`modules`].
635 #[derive(Debug, Clone)]
636 pub struct ModuleInfo {
637 pub path: String,
638 pub base: usize,
639 pub size: usize,
640 }
641
642 unsafe extern "C" fn modules_visitor(
643 info: *const sys::mi_prof_module_info_t,
644 arg: *mut c_void,
645 ) -> bool {
646 let result = catch_unwind(AssertUnwindSafe(|| unsafe {
647 let out = &mut *(arg as *mut Vec<ModuleInfo>);
648 let info = &*info;
649 // `info.path` is only valid for the duration of this callback (it
650 // points into OS-owned module-list storage), so it must be copied
651 // into an owned `String` right here rather than stashed for later.
652 let path = CStr::from_ptr(info.path).to_string_lossy().into_owned();
653 out.push(ModuleInfo {
654 path,
655 base: info.base,
656 size: info.size,
657 });
658 }));
659 result.is_ok()
660 }
661
662 /// Enumerate the process's loaded modules (shared libraries and the main
663 /// executable), e.g. to build pprof `Mapping` entries yourself.
664 ///
665 /// Unlike [`samples`]'s `collect_visitor`, this callback is free to
666 /// allocate: `mi_prof_modules_visit` never takes the profiler lock (the
667 /// module list is OS-owned, not part of the sampled-allocation table), so
668 /// there is no reentrant-allocation-under-the-lock hazard here.
669 pub fn modules() -> Vec<ModuleInfo> {
670 let mut out: Vec<ModuleInfo> = Vec::new();
671 unsafe {
672 sys::mi_prof_modules_visit(modules_visitor, (&mut out as *mut Vec<ModuleInfo>).cast());
673 }
674 out
675 }
676}
677
678#[cfg(test)]
679mod tests {
680 use super::*;
681 use std::sync::Mutex;
682
683 // The profiler is process-global state, and unit tests within this
684 // binary may run concurrently by default, so serialize everything that
685 // starts/stops it. `unwrap_or_else` rides through a poisoned lock rather
686 // than cascading a single panicking test into every other one.
687 static PROF_TEST_LOCK: Mutex<()> = Mutex::new(());
688
689 fn reset_profiler() {
690 if prof::is_enabled() {
691 prof::stop();
692 }
693 }
694
695 #[test]
696 fn enable_heap_profiling_with_default_config_starts_profiler() {
697 let _guard = PROF_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
698 reset_profiler();
699
700 let config = ProfConfig::default();
701 assert!(enable_heap_profiling_with(&config));
702 assert!(prof::is_enabled());
703
704 prof::stop();
705 }
706
707 #[test]
708 fn enable_heap_profiling_with_override_mode_sets_sample_interval() {
709 let _guard = PROF_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
710 reset_profiler();
711
712 let config = ProfConfig {
713 mode: ProfConfigMode::Override,
714 sample_interval: Some(4096),
715 ..Default::default()
716 };
717 assert!(enable_heap_profiling_with(&config));
718 assert!(prof::is_enabled());
719 assert_eq!(prof::stats().sample_rate, 4096);
720
721 prof::stop();
722 }
723
724 #[test]
725 fn unwrapped_malloc_write_realloc_grow_verify_free() {
726 unsafe {
727 let size = 64usize;
728 let p = unwrapped_malloc(size, 0);
729 assert!(!p.is_null());
730
731 for i in 0..size {
732 *p.add(i) = (i % 256) as u8;
733 }
734
735 let new_size = 256usize;
736 let p2 = unwrapped_realloc(p, new_size, 0);
737 assert!(!p2.is_null());
738
739 for i in 0..size {
740 assert_eq!(*p2.add(i), (i % 256) as u8);
741 }
742
743 unwrapped_free(p2);
744 }
745 }
746
747 #[test]
748 fn unwrapped_free_null_is_noop() {
749 unsafe {
750 unwrapped_free(core::ptr::null_mut());
751 }
752 }
753
754 #[test]
755 fn unwrapped_malloc_rejects_non_power_of_two_alignment() {
756 unsafe {
757 let p = unwrapped_malloc(16, 3);
758 assert!(p.is_null());
759 }
760 }
761
762 #[test]
763 fn unwrapped_realloc_with_null_ptr_behaves_like_malloc() {
764 unsafe {
765 let p = unwrapped_realloc(core::ptr::null_mut(), 32, 0);
766 assert!(!p.is_null());
767 unwrapped_free(p);
768 }
769 }
770
771 #[test]
772 fn unwrapped_realloc_with_zero_size_frees_and_returns_null() {
773 unsafe {
774 let p = unwrapped_malloc(32, 0);
775 assert!(!p.is_null());
776 let p2 = unwrapped_realloc(p, 0, 0);
777 assert!(p2.is_null());
778 }
779 }
780}