blob: 8af09d57426e95ecefab655453ec27fb9d7121e0 [file] [edit]
/* ----------------------------------------------------------------------------
Copyright (c) 2026-2026, Microsoft Research, Daan Leijen
This is free software; you can redistribute it and/or modify it under the
terms of the MIT license. A copy of the license can be found in the file
"LICENSE" at the root of this distribution.
-----------------------------------------------------------------------------*/
// Tests for the mimalloc pprof heap profiler (src/profile/pprof.c).
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#if !defined(_WIN32)
#include <pthread.h>
#include <unistd.h>
#endif
#include "mimalloc.h"
#include "mimalloc-profile.h"
#include "testhelper.h"
// ---------------------------------------------------------------------------
// The pprof profiler is not (yet) exposed through a public header;
// declare the API here as it is exported from `src/profile/pprof.c`.
// ---------------------------------------------------------------------------
#define TEST_THRESHOLD (1 * 1024)
#define PROFILE_BASE_NAME "profile/test-pprof-profile"
// Build the name of the `seq`-th snapshot file for `base`, matching the
// `<base>.<seq>.heap` naming used by `mi_profiler_snapshot`.
static void pprof_snapshot_file_name(char* buf, size_t bufsize, const char* base, unsigned seq) {
snprintf(buf, bufsize, "%s.%04u.heap", base, seq);
}
// Remove any snapshot files (`<base>.0001.heap` .. `<base>.<max_seq>.heap`) left behind.
static void pprof_remove_snapshot_files(const char* base, unsigned max_seq) {
char fname[1024];
for (unsigned seq = 1; seq <= max_seq; seq++) {
pprof_snapshot_file_name(fname, sizeof(fname), base, seq);
remove(fname);
}
}
// Allocate and free enough memory to be reasonably sure that samples were taken.
static void allocate_and_free(size_t n, size_t block_size) {
for (size_t i = 0; i < n; i++) {
void* p = mi_malloc(block_size + (i % 37));
mi_free(p);
}
}
// ---------------------------------------------------------------------------
// Ten distinct allocation call sites: each `alloc_site_N` below has its own
// `mi_malloc` call at a unique source location so the captured call stack
// (and thus the pprof location) differs between sites. Each site only
// allocates; freeing is managed by the caller (see `allocate_and_free_multi_site`)
// so that some allocations are still alive (i.e. positive inuse counts) at
// the point of an intermediate snapshot.
// ---------------------------------------------------------------------------
#define MI_TEST_PPROF_SITE_COUNT 10
// Written to (but never meaningfully read) after each allocation below, purely
// to give the `mi_malloc` call a side effect that must happen before the
// function returns. Without this, an optimizing (Release) compiler can turn
// `return mi_malloc(...);` into a tail call, which reuses the caller's return
// address instead of pushing a new stack frame -- collapsing what should be
// distinct call-site backtraces from `alloc_site_0`..`alloc_site_9` into far
// fewer distinct ones (or even just one).
static void* volatile mi_test_pprof_sink;
// Prevent inlining of the sites (as that would also collapse the backtraces).
#if defined(_MSC_VER)
#define mi_test_noinline __declspec(noinline)
#elif defined(__GNUC__)
#define mi_test_noinline __attribute__((noinline))
#else
#define mi_test_noinline
#endif
#define MI_TEST_PPROF_DEFINE_SITE(N) \
static mi_test_noinline void* alloc_site_##N(size_t block_size, size_t i) { \
void* p = mi_malloc(block_size + (i % 37)); \
mi_test_pprof_sink = p; \
return p; \
}
MI_TEST_PPROF_DEFINE_SITE(0)
MI_TEST_PPROF_DEFINE_SITE(1)
MI_TEST_PPROF_DEFINE_SITE(2)
MI_TEST_PPROF_DEFINE_SITE(3)
MI_TEST_PPROF_DEFINE_SITE(4)
MI_TEST_PPROF_DEFINE_SITE(5)
MI_TEST_PPROF_DEFINE_SITE(6)
MI_TEST_PPROF_DEFINE_SITE(7)
MI_TEST_PPROF_DEFINE_SITE(8)
MI_TEST_PPROF_DEFINE_SITE(9)
typedef void* (*mi_test_alloc_site_fun_t)(size_t block_size, size_t i);
static const mi_test_alloc_site_fun_t alloc_sites[MI_TEST_PPROF_SITE_COUNT] = {
&alloc_site_0, &alloc_site_1, &alloc_site_2, &alloc_site_3, &alloc_site_4,
&alloc_site_5, &alloc_site_6, &alloc_site_7, &alloc_site_8, &alloc_site_9
};
// Number of allocations to keep alive (per site, per call) before freeing;
// this needs to comfortably exceed the sampling gap (allocations are only
// sampled roughly every `TEST_THRESHOLD / block_size` bytes) so that at any
// point in time -- including at the intermediate snapshot halfway through --
// enough *sampled* allocations are still kept alive to show positive inuse
// counts. We simply keep every allocation alive until explicitly freed by
// `allocate_and_free_multi_site_cleanup`.
static void** alloc_site_kept = NULL;
static size_t alloc_site_kept_count = 0;
static size_t alloc_site_kept_capacity = 0;
static void alloc_site_keep_alive(void* p) {
if (alloc_site_kept_count == alloc_site_kept_capacity) {
const size_t new_capacity = (alloc_site_kept_capacity == 0 ? 4096 : alloc_site_kept_capacity * 2);
alloc_site_kept = (void**)mi_realloc(alloc_site_kept, new_capacity * sizeof(void*));
alloc_site_kept_capacity = new_capacity;
}
alloc_site_kept[alloc_site_kept_count++] = p;
}
// Allocate from each of the `MI_TEST_PPROF_SITE_COUNT` distinct call sites,
// `n` times each, so each site accumulates enough bytes to be sampled. None
// of the allocations are freed here (see `alloc_site_keep_alive`); they are
// only freed later by `allocate_and_free_multi_site_cleanup`, so that at any
// point in time -- including at the intermediate snapshot halfway through --
// there are allocations still in use (i.e. positive inuse counts).
static void allocate_and_free_multi_site(mi_profiler_t* prof, size_t n, size_t block_size) {
for (size_t site = 0; site < MI_TEST_PPROF_SITE_COUNT; site++) {
if (site == MI_TEST_PPROF_SITE_COUNT/2) {
mi_profiler_snapshot(prof);
}
for (size_t i = 0; i < n; i++) {
void* p = (*alloc_sites[site])(block_size, i);
alloc_site_keep_alive(p);
}
}
}
// Free all allocations kept alive by `alloc_site_keep_alive` (see above).
static void allocate_and_free_multi_site_cleanup(void) {
for (size_t i = 0; i < alloc_site_kept_count; i++) {
mi_free(alloc_site_kept[i]);
}
mi_free(alloc_site_kept);
alloc_site_kept = NULL;
alloc_site_kept_count = 0;
alloc_site_kept_capacity = 0;
}
// Count the number of location entry lines in a pprof snapshot, i.e. lines of the
// form `<inuse>: <inuse_bytes> [<alloc>: <alloc_bytes>] @ <addr1> <addr2> ...`.
static size_t count_location_entries(const char* contents) {
size_t count = 0;
const char* p = contents;
while ((p = strstr(p, "] @ 0x")) != NULL) {
count++;
p += 6;
}
return count;
}
// Parse the total inuse object count from the pprof header line, i.e. the
// first number in `heap profile: <inuse_objects>: <inuse_bytes> [...] @ heapprofile`.
static int64_t parse_total_inuse_objects(const char* contents) {
const char* p = (contents == NULL ? NULL : strstr(contents, "heap profile:"));
if (p == NULL) return -1;
p += strlen("heap profile:");
return (int64_t)strtoll(p, NULL, 10);
}
// Read the whole contents of a file into a heap allocated (0-terminated) buffer.
static char* read_file(const char* fname) {
FILE* f = fopen(fname, "rb");
if (f == NULL) return NULL;
fseek(f, 0, SEEK_END);
const long len = ftell(f);
fseek(f, 0, SEEK_SET);
if (len < 0) { fclose(f); return NULL; }
char* buf = (char*)mi_malloc((size_t)len + 1);
if (buf != NULL) {
const size_t nread = fread(buf, 1, (size_t)len, f);
buf[nread] = 0;
}
fclose(f);
return buf;
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
bool test_pprof_snapshot_creates_file(void) {
CHECK_BODY("pprof: snapshot creates a <base>.0001.heap file") {
pprof_remove_snapshot_files(PROFILE_BASE_NAME, 1);
mi_profiler_t* prof = mi_pprof_profiler_new(TEST_THRESHOLD, PROFILE_BASE_NAME ".heap", 0, 0, 0);
result = (prof != NULL);
if (result) {
mi_profile(prof);
mi_profiler_start(prof);
allocate_and_free(200000, 64);
mi_profiler_stop(prof);
mi_profiler_snapshot(prof);
char fname[1024];
pprof_snapshot_file_name(fname, sizeof(fname), PROFILE_BASE_NAME, 1);
char* contents = read_file(fname);
result = (contents != NULL && contents[0] != 0);
mi_free(contents);
mi_profile(NULL); // unregister before deleting
mi_pprof_profiler_delete(prof);
}
pprof_remove_snapshot_files(PROFILE_BASE_NAME, 1);
}
return true;
}
bool test_pprof_snapshot_format(void) {
CHECK_BODY("pprof: snapshot uses the pprof heap profile text format") {
pprof_remove_snapshot_files(PROFILE_BASE_NAME, 1);
mi_profiler_t* prof = mi_pprof_profiler_new(TEST_THRESHOLD, PROFILE_BASE_NAME ".heap", 0, 0, 0);
result = (prof != NULL);
if (result) {
mi_profile(prof);
mi_profiler_start(prof);
allocate_and_free(200000, 64);
mi_profiler_stop(prof);
mi_profiler_snapshot(prof);
char fname[1024];
pprof_snapshot_file_name(fname, sizeof(fname), PROFILE_BASE_NAME, 1);
char* contents = read_file(fname);
result = (contents != NULL &&
strncmp(contents, "heap profile:", 13) == 0 &&
strstr(contents, "@ heapprofile") != NULL &&
strstr(contents, "MAPPED_LIBRARIES:") != NULL);
mi_free(contents);
mi_profile(NULL);
mi_pprof_profiler_delete(prof);
}
pprof_remove_snapshot_files(PROFILE_BASE_NAME, 1);
}
return true;
}
#define PROFILE_PROTO_BASE_NAME "test-pprof-profile-proto"
// Read a raw protobuf varint starting at `*p` (bounded by `end`), advancing `*p`.
static uint64_t pb_read_varint(const uint8_t** p, const uint8_t* end) {
uint64_t v = 0;
int shift = 0;
while (*p < end) {
const uint8_t byte = *(*p)++;
v |= ((uint64_t)(byte & 0x7F)) << shift;
if ((byte & 0x80) == 0) break;
shift += 7;
}
return v;
}
// A very small sanity check that the captured bytes are plausible protobuf: no
// C-side protobuf parser is available, so we just walk the top-level
// `(tag, value)` pairs and check they are well-formed and that at least one
// `Profile.sample` (field 2) and one `Profile.location` (field 4) entry are
// present, without fully decoding their contents.
static bool pb_looks_like_valid_profile(const uint8_t* data, size_t size) {
const uint8_t* p = data;
const uint8_t* end = data + size;
bool saw_sample = false;
bool saw_location = false;
while (p < end) {
const uint64_t tag = pb_read_varint(&p, end);
const uint32_t field = (uint32_t)(tag >> 3);
const uint32_t wiretype = (uint32_t)(tag & 0x7);
if (field == 2) saw_sample = true;
if (field == 4) saw_location = true;
if (wiretype == 0) { // varint
pb_read_varint(&p, end);
}
else if (wiretype == 2) { // length-delimited
const uint64_t len = pb_read_varint(&p, end);
if ((size_t)(end - p) < len) return false;
p += len;
}
else {
return false; // no other wiretypes are used by this writer
}
}
return (p == end && saw_sample && saw_location);
}
bool test_pprof_snapshot_proto_format(void) {
CHECK_BODY("pprof: snapshot can use the protobuf pprof format") {
char fname[1024];
snprintf(fname, sizeof(fname), "%s.0001.pb", PROFILE_PROTO_BASE_NAME);
remove(fname);
mi_profiler_t* prof = mi_pprof_profiler_new(TEST_THRESHOLD, PROFILE_PROTO_BASE_NAME, 0, 0, 0);
result = (prof != NULL);
if (result) {
mi_profile(prof);
mi_profiler_start(prof);
allocate_and_free(200000, 64);
mi_profiler_stop(prof);
mi_profiler_snapshot(prof);
FILE* f = fopen(fname, "rb");
result = (f != NULL);
if (result) {
fseek(f, 0, SEEK_END);
const long size = ftell(f);
fseek(f, 0, SEEK_SET);
result = (size > 0);
if (result) {
uint8_t* buf = (uint8_t*)mi_malloc((size_t)size);
const size_t nread = fread(buf, 1, (size_t)size, f);
result = (nread == (size_t)size && pb_looks_like_valid_profile(buf, nread));
mi_free(buf);
}
fclose(f);
}
mi_profile(NULL);
mi_pprof_profiler_delete(prof);
}
remove(fname);
}
return true;
}
#define PROFILE_INTERVAL_BASE_NAME "profile/test-pprof-profile-interval"
#define TEST_INTERVAL_SIZE (8 * 1024) // small enough that `allocate_and_free` below reliably triggers several automatic snapshots, even with a coarser sample rate
#define TEST_INTERVAL_MAX_SEQ 64 // generous upper bound used only for cleanup
bool test_pprof_snapshot_interval(void) {
CHECK_BODY("pprof: interval_size triggers automatic snapshots from on_alloc") {
pprof_remove_snapshot_files(PROFILE_INTERVAL_BASE_NAME, TEST_INTERVAL_MAX_SEQ);
mi_profiler_t* prof = mi_pprof_profiler_new(TEST_THRESHOLD, PROFILE_INTERVAL_BASE_NAME ".heap", TEST_INTERVAL_SIZE, 0, 0);
result = (prof != NULL);
if (result) {
mi_profile(prof);
mi_profiler_start(prof);
// note: `mi_profiler_snapshot` is never called explicitly here -- any
// snapshot files that appear must have been triggered automatically by
// `on_alloc` once `TEST_INTERVAL_SIZE` sampled bytes have accumulated.
allocate_and_free(200000, 64);
mi_profiler_stop(prof);
// the first snapshot should exist...
char fname1[1024];
pprof_snapshot_file_name(fname1, sizeof(fname1), PROFILE_INTERVAL_BASE_NAME, 1);
char* contents1 = read_file(fname1);
result = (contents1 != NULL && strncmp(contents1, "heap profile:", 13) == 0);
mi_free(contents1);
// ...and so should a later one, confirming the interval repeats rather
// than firing only once.
if (result) {
char fname6[1024];
pprof_snapshot_file_name(fname6, sizeof(fname6), PROFILE_INTERVAL_BASE_NAME, 6);
char* contents6 = read_file(fname6);
result = (contents6 != NULL && strncmp(contents6, "heap profile:", 13) == 0);
mi_free(contents6);
}
mi_profile(NULL); // unregister before deleting
mi_pprof_profiler_delete(prof);
}
pprof_remove_snapshot_files(PROFILE_INTERVAL_BASE_NAME, TEST_INTERVAL_MAX_SEQ);
}
return true;
}
bool test_pprof_snapshot_records_samples(void) {
CHECK_BODY("pprof: snapshot records samples from multiple distinct call sites") {
// `allocate_and_free_multi_site` itself snapshots once, halfway through its
// 10 call sites, so each call produces 2 snapshot files: one with only the
// first half of the sites recorded, and one (after the call returns)
// with all `MI_TEST_PPROF_SITE_COUNT` sites recorded. We call it twice
// below, for a total of 4 snapshots; only the *last* one is guaranteed to
// have seen every site, so that is the one we check. A 5th, final snapshot
// is taken after freeing everything, to check inuse drops back to 0.
#define MI_TEST_PPROF_RECORDS_SNAPSHOT_COUNT 4
#define MI_TEST_PPROF_FINAL_SNAPSHOT_SEQ (MI_TEST_PPROF_RECORDS_SNAPSHOT_COUNT + 1)
pprof_remove_snapshot_files(PROFILE_BASE_NAME, MI_TEST_PPROF_FINAL_SNAPSHOT_SEQ);
mi_profiler_t* prof = mi_pprof_profiler_new(TEST_THRESHOLD, PROFILE_BASE_NAME ".heap", 0, 0, 0);
result = (prof != NULL);
if (result) {
mi_profile(prof);
mi_profiler_start(prof);
// allocate plenty from each of the 10 distinct call sites so every
// site accumulates enough bytes to trigger at least one sample.
allocate_and_free_multi_site(prof, 20000, 64);
mi_profiler_snapshot(prof);
allocate_and_free_multi_site(prof, 20000, 64);
mi_profiler_snapshot(prof);
allocate_and_free_multi_site_cleanup(); // free any allocations still kept alive by the ring buffers
mi_profiler_snapshot(prof); // final snapshot, taken after everything was freed
// the intermediate snapshot (seq 1, taken halfway through the first call)
// should show a positive inuse count: some allocations are still kept
// alive in the ring buffers at that point.
char fname1[1024];
pprof_snapshot_file_name(fname1, sizeof(fname1), PROFILE_BASE_NAME, 1);
char* contents1 = read_file(fname1);
result = (contents1 != NULL && parse_total_inuse_objects(contents1) > 0);
mi_free(contents1);
char fname[1024];
pprof_snapshot_file_name(fname, sizeof(fname), PROFILE_BASE_NAME, MI_TEST_PPROF_RECORDS_SNAPSHOT_COUNT);
char* contents = read_file(fname);
// an entry line looks like: ` 0: 0 [ 1234: 56789] @ 0x... 0x... ...`
const size_t entry_count = (contents == NULL ? 0 : count_location_entries(contents));
result = (result && contents != NULL && entry_count >= MI_TEST_PPROF_SITE_COUNT);
mi_free(contents);
// the final snapshot (taken after everything was freed) should show a
// total inuse count of 0.
char fname_final[1024];
pprof_snapshot_file_name(fname_final, sizeof(fname_final), PROFILE_BASE_NAME, MI_TEST_PPROF_FINAL_SNAPSHOT_SEQ);
char* contents_final = read_file(fname_final);
result = (result && contents_final != NULL && parse_total_inuse_objects(contents_final) == 0);
mi_free(contents_final);
mi_profile(NULL);
mi_pprof_profiler_delete(prof);
}
// pprof_remove_snapshot_files(PROFILE_BASE_NAME, MI_TEST_PPROF_FINAL_SNAPSHOT_SEQ);
#undef MI_TEST_PPROF_FINAL_SNAPSHOT_SEQ
#undef MI_TEST_PPROF_RECORDS_SNAPSHOT_COUNT
}
return true;
}
bool test_pprof_snapshot_increments_sequence(void) {
CHECK_BODY("pprof: repeated snapshots use an incrementing sequence number") {
#define MI_TEST_PPROF_SNAPSHOT_COUNT 3
pprof_remove_snapshot_files(PROFILE_BASE_NAME, MI_TEST_PPROF_SNAPSHOT_COUNT);
mi_profiler_t* prof = mi_pprof_profiler_new(TEST_THRESHOLD, PROFILE_BASE_NAME ".heap", 0, 0, 0);
result = (prof != NULL);
if (result) {
mi_profile(prof);
mi_profiler_start(prof);
for (unsigned seq = 1; seq <= MI_TEST_PPROF_SNAPSHOT_COUNT && result; seq++) {
allocate_and_free(20000, 64);
mi_profiler_snapshot(prof);
char fname[1024];
pprof_snapshot_file_name(fname, sizeof(fname), PROFILE_BASE_NAME, seq);
char* contents = read_file(fname);
result = (contents != NULL && contents[0] != 0);
mi_free(contents);
}
mi_profiler_stop(prof);
mi_profile(NULL);
mi_pprof_profiler_delete(prof);
}
pprof_remove_snapshot_files(PROFILE_BASE_NAME, MI_TEST_PPROF_SNAPSHOT_COUNT);
#undef MI_TEST_PPROF_SNAPSHOT_COUNT
}
return true;
}
bool test_pprof_profiler_new_delete(void) {
CHECK_BODY("pprof: profiler can be created and deleted without use") {
mi_profiler_t* prof = mi_pprof_profiler_new(TEST_THRESHOLD, PROFILE_BASE_NAME ".heap", 0, 0, 0);
result = (prof != NULL);
if (prof != NULL) {
mi_pprof_profiler_delete(prof);
}
}
return true;
}
// ---------------------------------------------------------------------------
// Concurrent test: several threads allocate and free from a shared pool of
// pointers at random, so a block allocated by one thread is often freed by a
// *different* thread -- exercising the thread safety of `on_alloc`/`on_free`
// and the lock-free locations hash table. A few snapshots are taken while the
// threads are still running (and are left on disk afterwards for inspection).
// Not available on Windows (no pthreads); guarded by `!defined(_WIN32)`.
// ---------------------------------------------------------------------------
#if !defined(_WIN32)
#define PROFILE_THREADS_BASE_NAME "profile/test-pprof-profile-threads"
#define MI_TEST_PPROF_THREAD_COUNT 8
#define MI_TEST_PPROF_THREAD_ITERS 50000
#define MI_TEST_PPROF_THREAD_POOL_SIZE 1024
// number of snapshot files to retain: 3 taken while threads are running, plus 1 final one
#define MI_TEST_PPROF_THREAD_SNAPSHOT_COUNT 4
static pthread_mutex_t thread_pool_mutex = PTHREAD_MUTEX_INITIALIZER;
static void* thread_pool[MI_TEST_PPROF_THREAD_POOL_SIZE];
typedef struct {
unsigned seed;
} thread_arg_t;
// Repeatedly pick a random slot in the shared pool: if it holds a block
// (possibly allocated by another thread), free it; otherwise allocate a new
// block and try to install it. The pool is only ever touched while holding
// `thread_pool_mutex`, but the actual `mi_malloc`/`mi_free` calls happen
// outside the lock so that they run genuinely concurrently across threads.
static void* thread_alloc_free_fun(void* varg) {
thread_arg_t* arg = (thread_arg_t*)varg;
unsigned seed = arg->seed;
for (size_t i = 0; i < MI_TEST_PPROF_THREAD_ITERS; i++) {
const size_t idx = (size_t)(rand_r(&seed) % MI_TEST_PPROF_THREAD_POOL_SIZE);
pthread_mutex_lock(&thread_pool_mutex);
void* p = thread_pool[idx];
if (p != NULL) { thread_pool[idx] = NULL; }
pthread_mutex_unlock(&thread_pool_mutex);
if (p != NULL) {
mi_free(p); // may free a block that was allocated by a different thread
}
else {
const size_t size = 8 + (rand_r(&seed) % 249);
void* np = mi_malloc(size);
pthread_mutex_lock(&thread_pool_mutex);
if (thread_pool[idx] == NULL) {
thread_pool[idx] = np;
np = NULL;
}
pthread_mutex_unlock(&thread_pool_mutex);
if (np != NULL) { mi_free(np); } // slot got filled by another thread meanwhile
}
}
return NULL;
}
// Free any blocks still left in the shared pool (e.g. if a thread allocated
// but the corresponding free never got scheduled before the threads stopped).
static void thread_pool_cleanup(void) {
for (size_t i = 0; i < MI_TEST_PPROF_THREAD_POOL_SIZE; i++) {
mi_free(thread_pool[i]);
thread_pool[i] = NULL;
}
}
bool test_pprof_concurrent_threads(void) {
CHECK_BODY("pprof: snapshot is thread safe with concurrently allocating/freeing threads") {
pprof_remove_snapshot_files(PROFILE_THREADS_BASE_NAME, MI_TEST_PPROF_THREAD_SNAPSHOT_COUNT);
mi_profiler_t* prof = mi_pprof_profiler_new(TEST_THRESHOLD, PROFILE_THREADS_BASE_NAME ".heap", 0, 0, 0);
result = (prof != NULL);
if (result) {
mi_profile(prof);
mi_profiler_start(prof);
pthread_t threads[MI_TEST_PPROF_THREAD_COUNT];
thread_arg_t args[MI_TEST_PPROF_THREAD_COUNT];
for (int t = 0; t < MI_TEST_PPROF_THREAD_COUNT && result; t++) {
args[t].seed = (unsigned)(t + 1);
result = (pthread_create(&threads[t], NULL, &thread_alloc_free_fun, &args[t]) == 0);
}
// take a few snapshots while the threads are still running
for (unsigned seq = 1; seq < MI_TEST_PPROF_THREAD_SNAPSHOT_COUNT; seq++) {
usleep(2000); // 2ms
mi_profiler_snapshot(prof);
}
for (int t = 0; t < MI_TEST_PPROF_THREAD_COUNT; t++) {
pthread_join(threads[t], NULL);
}
thread_pool_cleanup();
mi_profiler_snapshot(prof); // final snapshot, after all threads have finished
// every retained snapshot should be a valid, well formed pprof snapshot with a
// non-negative inuse count (a negative inuse count would indicate a
// race in the alloc/free counters).
for (unsigned seq = 1; seq <= MI_TEST_PPROF_THREAD_SNAPSHOT_COUNT && result; seq++) {
char fname[1024];
pprof_snapshot_file_name(fname, sizeof(fname), PROFILE_THREADS_BASE_NAME, seq);
char* contents = read_file(fname);
result = (contents != NULL &&
strncmp(contents, "heap profile:", 13) == 0 &&
strstr(contents, "MAPPED_LIBRARIES:") != NULL &&
parse_total_inuse_objects(contents) >= 0);
mi_free(contents);
}
mi_profiler_stop(prof);
mi_profile(NULL);
mi_pprof_profiler_delete(prof);
}
// snapshot files are intentionally left on disk for inspection.
}
return true;
}
#endif // !defined(_WIN32)
// ---------------------------------------------------------------------------
// Allocation failure: exhaust the mimalloc arenas (with OS allocation
// disabled) and then drain the profiler's own heap by creating many distinct
// sampled call stacks. Every following allocation from the profiler heap must
// then fail, so taking a snapshot exercises the out-of-memory paths
// deterministically: it must neither crash nor leave a partial snapshot file.
// Linux only for now: the arena exhaustion below is verified on Linux, and the
// pprof test is also run on macOS where it has not been verified yet.
// ---------------------------------------------------------------------------
#if defined(__linux__)
#define PPROF_OOM_BASE_NAME "profile/test-pprof-oom"
#define PPROF_OOM_MAX_BLOCKS (20000)
#define PPROF_OOM_BLOCK_SIZE (64 * 1024)
#define PPROF_OOM_RECURSION_DEPTH (120)
#define PPROF_OOM_RECURSION_REPS (4)
static void* pprof_oom_blocks[PPROF_OOM_MAX_BLOCKS];
static void* volatile pprof_oom_sink;
// Four distinct recursive allocation sites: each recursion depth produces a
// different (truncated) call stack, so sampling them creates many distinct
// locations -- and thus many allocations -- in the profiler's own heap.
#define PPROF_OOM_DEFINE_RECURSION(N) \
static void pprof_oom_rec_##N(size_t depth) { \
void* p = mi_malloc(24 + (depth & 31)); \
pprof_oom_sink = p; \
if (depth < PPROF_OOM_RECURSION_DEPTH) { pprof_oom_rec_##N(depth + 1); } \
mi_free(p); \
}
PPROF_OOM_DEFINE_RECURSION(0)
PPROF_OOM_DEFINE_RECURSION(1)
PPROF_OOM_DEFINE_RECURSION(2)
PPROF_OOM_DEFINE_RECURSION(3)
// Returns the size of `fname`, or -1 if it does not exist.
static long file_size(const char* fname) {
FILE* f = fopen(fname, "rb");
if (f == NULL) return -1;
fseek(f, 0, SEEK_END);
const long size = ftell(f);
fclose(f);
return size;
}
bool test_pprof_snapshot_oom(void) {
CHECK_BODY("pprof: snapshot degrades gracefully when profiler allocations fail") {
char proto_fname[1024];
snprintf(proto_fname, sizeof(proto_fname), "%s.0001.pb", PPROF_OOM_BASE_NAME);
char recovery_fname[1024];
snprintf(recovery_fname, sizeof(recovery_fname), "%s.0002.pb", PPROF_OOM_BASE_NAME);
remove(proto_fname);
remove(recovery_fname);
// sample every allocation so the recursion below creates a location per depth
mi_profiler_t* prof = mi_pprof_profiler_new(1, PPROF_OOM_BASE_NAME, 0, 0, 0);
result = (prof != NULL);
if (result) {
mi_profile(prof);
mi_profiler_start(prof);
// the failures below are expected: do not print them
const long old_show_errors = mi_option_get(mi_option_show_errors);
mi_option_set(mi_option_show_errors, 0);
// 1. exhaust the arenas: from now on no new pages can be allocated
mi_option_set(mi_option_disallow_os_alloc, 1);
size_t block_count = 0;
while (block_count < PPROF_OOM_MAX_BLOCKS) {
void* b = mi_malloc(PPROF_OOM_BLOCK_SIZE);
if (b == NULL) break;
pprof_oom_blocks[block_count++] = b;
}
result = (block_count > 0);
// 2. drain the profiler heap's remaining free space: these allocations
// still fit in its current pages, so they keep creating locations
// until no room is left for another one.
for (size_t rep = 0; rep < PPROF_OOM_RECURSION_REPS; rep++) {
pprof_oom_rec_0(1);
pprof_oom_rec_1(1);
pprof_oom_rec_2(1);
pprof_oom_rec_3(1);
}
// 3. the snapshot cannot allocate: it must return normally and leave no file
mi_profiler_snapshot(prof);
result = result && (file_size(proto_fname) < 0);
// 4. restore memory and check that snapshots work again
for (size_t i = 0; i < block_count; i++) { mi_free(pprof_oom_blocks[i]); }
block_count = 0;
mi_option_set(mi_option_disallow_os_alloc, 0);
mi_option_set(mi_option_show_errors, old_show_errors);
allocate_and_free(20000, 64);
mi_profiler_snapshot(prof);
result = result && (file_size(recovery_fname) > 0);
remove(proto_fname);
remove(recovery_fname);
mi_profile(NULL);
mi_pprof_profiler_delete(prof);
}
}
return true;
}
#endif // defined(__linux__)
// ---------------------------------------------------------------------------
// main
// ---------------------------------------------------------------------------
int main(void) {
test_pprof_profiler_new_delete();
test_pprof_snapshot_creates_file();
test_pprof_snapshot_format();
test_pprof_snapshot_proto_format();
test_pprof_snapshot_interval();
test_pprof_snapshot_increments_sequence();
// last test leaves the pprof files
test_pprof_snapshot_records_samples();
#if !defined(_WIN32)
// this test also leaves its pprof files
test_pprof_concurrent_threads();
#endif
#if defined(__linux__)
// last: this one deliberately exhausts the allocator before restoring it
test_pprof_snapshot_oom();
#endif
return print_test_summary();
}