| /* ---------------------------------------------------------------------------- | |
| Copyright (c) 2018-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. | |
| Disclaimer: Unlike the rest of the considerate hand-crafted mimalloc sources, | |
| this file was written with AI assistance (for the process image and protobuf | |
| format generation). | |
| -----------------------------------------------------------------------------*/ | |
| // --------------------------------------------------------------------------- | |
| // This implements a basic memory profiler that outputs pprof-compatible heap | |
| // snapshots (text or protobuf). It is basic, but it does scale with a lock-free | |
| // hash table for locations, and uses poisson distributed sampling. | |
| // It has no impact on runtime performance when profiling is not active. | |
| // | |
| // It can be used programmatically (using `mi_pprof_profiler_new` etc.) | |
| // or be invoked with environment variables. For example: | |
| // MIMALLOC_PROFILE=profile ./my_program | |
| // and then analyze the generated profile using pprof tools: | |
| // pprof -http=:8080 ./myprogram profile.* | |
| // --------------------------------------------------------------------------- | |
| #include "mimalloc.h" | |
| #include "mimalloc/prim/prim-tls.h" // _mi_theap_default | |
| #include "mimalloc/util/libc.h" | |
| #include "mimalloc/util/options.h" | |
| #include "mimalloc/util/stats.h" | |
| #include "mimalloc/profile/pprof.h" | |
| #include "mimalloc/heap/process.h" // _mi_process.profile_env_profiler | |
| // --------------------------------------------------------------------------- | |
| // Internal API | |
| // --------------------------------------------------------------------------- | |
| typedef struct mi_location_s mi_location_t; | |
| typedef struct mi_locations_s mi_locations_t; | |
| typedef struct mi_callstack_s mi_callstack_t; | |
| static bool mi_locations_init(mi_heap_t* heap, mi_locations_t* locations); | |
| static void mi_locations_done(mi_heap_t* heap, mi_locations_t* locations); | |
| static mi_location_t* mi_locations_find_or_insert(mi_heap_t* heap, mi_locations_t* locations, mi_thread_id_t thread_id, const mi_callstack_t* callstack, size_t callstack_hash); | |
| // --------------------------------------------------------------------------- | |
| // Types | |
| // --------------------------------------------------------------------------- | |
| #define MI_MAX_BACKTRACE_DEPTH (100) | |
| #define MI_BACKTRACE_SKIP_FRAMES (3) | |
| // Threshold (in bytes) for the thread-local interval countdown batching in `on_alloc` | |
| #ifndef MI_PROFILE_COUNTDOWN_THRESHOLD | |
| #define MI_PROFILE_COUNTDOWN_THRESHOLD (8*MI_KiB) | |
| #endif | |
| #define MI_PROFILE_COUNTDOWN_MIN_INTERVAL (8*MI_PROFILE_COUNTDOWN_THRESHOLD) | |
| // A callstack represents a sequence of function call frames captured at a specific point in time. | |
| struct mi_callstack_s { | |
| size_t count; | |
| void** frames; | |
| }; | |
| // A location is a unique combination of a thread and runtime callstack for | |
| // which we track allocation and deallocation statistics. | |
| // | |
| // Locations are only ever inserted, or mutated in-place after publication, and a location is only ever looked up using its own | |
| // `hash`/`thread_id`/`callstack` which allows a lock-free lookup of locations in the hash table. | |
| struct mi_location_s { | |
| _Atomic(struct mi_location_s*) next; // next location in the same hash bucket (for chaining on collision); | |
| mi_thread_id_t thread_id; | |
| mi_callstack_t callstack; | |
| size_t hash; // hash of the thread_id and callstack | |
| int64_t alloc_count; // only ever written by the single (allocating) owning thread: not atomic | |
| int64_t alloc_bytes; | |
| _Atomic(size_t) inuse_count; // currently in-use (allocated but not yet freed) objects/bytes for this location; | |
| _Atomic(size_t) inuse_bytes; // incremented in `on_alloc` and decremented in `on_free`, both possibly on any thread | |
| }; | |
| // A basic hash table of locations. The bucket array is fixed-size (allocated | |
| // once in `mi_locations_init`) and each bucket is a lock-free singly-linked | |
| // list that only ever grows by prepending (see `mi_locations_find_or_insert`). | |
| struct mi_locations_s { | |
| size_t bucket_count; // number of buckets (fixed size) | |
| _Atomic(mi_location_t*)* buckets; // array of `bucket_count` chain heads | |
| _Atomic(size_t) count; // number of locations currently stored (just a statistic) | |
| }; | |
| // Our profiler. | |
| typedef struct { | |
| mi_profiler_t profiler; // mimalloc profiler info | |
| mi_heap_t* profile_heap; // heap used for all profiler allocations (so these don't interfere) | |
| mi_locations_t locations; // hash table of call locations | |
| size_t sample_threshold; // current sample threshold | |
| size_t snapshot_count; // number of times `mi_profiler_snapshot` was called (used to number the snapshot files) | |
| char* base_file_name; // base file name for snapshot files, e.g. "<base_file_name>.<seq>.heap" | |
| bool format_text; // text or protobuf snapshot format | |
| size_t alloc_interval_size; // if >0, automatically snapshot every `alloc_interval_size` allocated bytes (capped at `MI_SSIZE_MAX`) | |
| _Atomic(mi_ssize_t) alloc_interval_countdown; // bytes remaining until the next automatic snapshot (can go negative; decremented concurrently by `on_alloc` on any thread) | |
| size_t inuse_interval_size; // if >0, automatically snapshot every time (sampled) in-use bytes grow by `inuse_interval_size` bytes | |
| _Atomic(size_t) inuse_bytes; // running total of (sampled) in-use bytes across all locations (can never exceed the address space); incremented in `on_alloc`, decremented in `on_free`, both possibly on any thread | |
| _Atomic(size_t) inuse_interval_threshold; // next `inuse_bytes` total that triggers a snapshot; advanced (monotonically increasing) whenever reached in `on_alloc` | |
| size_t time_interval_secs; // if >0, automatically snapshot every `time_interval_secs` seconds (checked from `on_alloc`) | |
| _Atomic(mi_msecs_t) time_interval_deadline; // next `_mi_clock_now()` value that triggers a snapshot; advanced (monotonically increasing) whenever reached in `on_alloc` | |
| int64_t start_time_secs; // wall-clock time at creation (seconds since the Unix epoch) | |
| mi_msecs_t start_clock; // `_mi_clock_now()` at creation (monotonic, so snapshot times are `start_time_secs` + elapsed clock) | |
| } mi_pprof_profiler_t; | |
| static inline mi_pprof_profiler_t* downcast( mi_profiler_t* prof ) { | |
| return (mi_pprof_profiler_t*)prof; | |
| } | |
| static void mi_pprof_profiler_snapshot(mi_pprof_profiler_t* prof); | |
| static size_t mi_prim_backtrace(void** buffer, size_t max_depth, size_t* hash); // `buffer` must have room for `max_depth + MI_PPROF_SKIP_FRAMES + 1` entries (see definition) | |
| static int64_t mi_prim_time_secs(void); // wall-clock seconds since the Unix epoch (0 if unavailable) | |
| // --------------------------------------------------------------------------- | |
| // Automatic profiler driven by the `MIMALLOC_PROFILE` environment variable. | |
| // This allows profiling an arbitrary program (with no code changes) by | |
| // overriding its allocator with a mimalloc shared library build and setting | |
| // `MIMALLOC_PROFILE=<base_file_name>` | |
| // (and optionally `MIMALLOC_PROFILE_ALLOC_INTERVAL=<bytes>` to periodically snapshot | |
| // while the program is running). | |
| // --------------------------------------------------------------------------- | |
| #ifndef MI_PROFILE_USE_BUILTIN | |
| #define MI_PROFILE_USE_BUILTIN (MI_PROFILE) | |
| #endif | |
| #if MI_PROFILE_USE_BUILTIN // todo: also disable profiling in secure mode? | |
| static bool mi_is_elevated_process(void); | |
| // Called once at process initialization (see `mi_process_init` in `heap/process.c`). | |
| void _mi_pprof_profiler_init(void) { | |
| #if MI_PROFILE | |
| if (mi_is_elevated_process()) return; // don't let an untrusted environment influence a privileged process | |
| char fname[1024]; | |
| if (_mi_getenv("mimalloc_profile", fname, sizeof(fname)) != 0 || fname[0] == 0) return; // `MIMALLOC_PROFILE` not set | |
| const size_t sample_rate = mi_option_get_size(mi_option_profile_sample_rate); // 512 KiB by default | |
| const size_t alloc_interval_size = mi_option_get_size(mi_option_profile_alloc_interval); // 1 GiB by default | |
| const size_t inuse_interval_size = mi_option_get_size(mi_option_profile_inuse_interval); // 0 by default (no automatic interval snapshots) | |
| const size_t time_interval_secs = mi_option_get_clamp(mi_option_profile_time_interval,0,24*60*60L); // 0 by default (no automatic interval snapshots) | |
| mi_profiler_t* profiler = mi_pprof_profiler_new(sample_rate, fname, alloc_interval_size, inuse_interval_size, time_interval_secs); | |
| if (profiler == NULL) return; | |
| _mi_process.profile_env_profiler = profiler; | |
| _mi_verbose_message("pprof profiler initialized with base file name: %s\n", fname); | |
| mi_profile(profiler); // attach to the main sub-process (and any current/future heaps in it) | |
| mi_profiler_start(profiler); | |
| #endif | |
| } | |
| // Called once at process termination (see `mi_process_done` in `heap/process.c`). | |
| void _mi_pprof_profiler_done(void) { | |
| #if MI_PROFILE | |
| mi_profiler_t* profiler = _mi_process.profile_env_profiler; | |
| if (profiler == NULL) return; | |
| _mi_process.profile_env_profiler = NULL; | |
| mi_profiler_stop(profiler); | |
| _mi_verbose_message("pprof profiler done\n"); | |
| mi_profiler_snapshot(profiler); // final snapshot so short-lived processes still get a complete profile | |
| mi_profile(NULL); // detach from the main sub-process | |
| mi_pprof_profiler_delete(profiler); | |
| #endif | |
| } | |
| #else | |
| void _mi_pprof_profiler_init(void) { | |
| } | |
| void _mi_pprof_profiler_done(void) { | |
| } | |
| #endif | |
| // --------------------------------------------------------------------------- | |
| // Get a location | |
| // --------------------------------------------------------------------------- | |
| static mi_decl_noinline mi_location_t* mi_location_get(mi_pprof_profiler_t* prof) { | |
| void* frames[MI_MAX_BACKTRACE_DEPTH + 1 + MI_BACKTRACE_SKIP_FRAMES]; // extra room: `mi_prim_backtrace` captures+shifts out its own frame and `MI_BACKTRACE_SKIP_FRAMES` wrapper frames in-place | |
| size_t callstack_hash = 0; | |
| const size_t depth = mi_prim_backtrace(frames, MI_MAX_BACKTRACE_DEPTH, &callstack_hash); | |
| if (depth==0) return NULL; | |
| mi_callstack_t callstack = { depth, frames }; | |
| const mi_thread_id_t thread_id = _mi_thread_id(); | |
| mi_location_t* loc = mi_locations_find_or_insert(prof->profile_heap, &prof->locations, thread_id, &callstack, callstack_hash); | |
| return loc; | |
| } | |
| // --------------------------------------------------------------------------- | |
| // Profiler callbacks and initialization | |
| // --------------------------------------------------------------------------- | |
| // Draws an integer approximating Exp(scale) to give a poisson distribution, | |
| // instead of a fixed period, which avoids sampling bias from allocation | |
| // patterns that happen to align with a constant period | |
| static size_t mi_exp_sample(mi_theap_t* theap, size_t scale) { | |
| if (scale == 0) return 0; // keep 0 | |
| #if MI_PROFILE==2 | |
| if (scale == 1) return 1; // keep 1 as is (to allow sampling every allocation) | |
| #endif | |
| const size_t r = _mi_theap_random_next(theap) | 1; | |
| const size_t lz = mi_clz(r); // coarse -log2(U): Geometric(1/2) | |
| // The highest set bit of `r` is at position `MI_SIZE_BITS-1-lz`; shift it out and | |
| // take the next 8 bits as a fractional refinement in [0,256). When `r == 1` | |
| // (lz == MI_SIZE_BITS-1) there are no fractional bits and the shift would be | |
| // undefined, so the fraction is 0 there. | |
| const size_t frac_q8 = (lz >= MI_SIZE_BITS - 1 ? 0 : (r << (lz + 1)) >> (MI_SIZE_BITS - 8)); | |
| const size_t bits_q8 = (lz << 8) | frac_q8; // Q8 fixed-point estimate of -log2(U) | |
| const size_t ln_q8 = (bits_q8 * 177) >> 8; // -log2(U) -> -ln(U): *ln(2) (~177/256) | |
| size_t next = ((ln_q8 * scale) >> 8) + 1; // scale by the mean; +1 avoids a zero threshold | |
| const size_t cap = scale * 32; // clamp the (rare) long tail | |
| return (next > cap ? cap : next); | |
| } | |
| // Check if the sampled bytes accumulated so far (batched per-thread) should trigger a shared | |
| // countdown decrement, and if so, whether that decrement crosses zero (triggering a snapshot and | |
| // resetting the countdown to `alloc_interval_size`). | |
| static bool mi_interval_update(uint64_t ubytes_since_last_sample, size_t alloc_interval_size, _Atomic(mi_ssize_t)* alloc_interval_countdown, mi_ssize_t* interval_pending) { | |
| mi_ssize_t sampled_bytes = 0; | |
| mi_ssize_t bytes_since_last_sample= (ubytes_since_last_sample > MI_SSIZE_MAX ? MI_SSIZE_MAX : (mi_ssize_t)ubytes_since_last_sample); | |
| if (alloc_interval_size < MI_PROFILE_COUNTDOWN_MIN_INTERVAL || MI_SSIZE_MAX - *interval_pending < bytes_since_last_sample /* overflow? */) { | |
| // interval too small relative to the batching; always adjust | |
| sampled_bytes = bytes_since_last_sample; | |
| } | |
| else { | |
| // batch locally per-thread and only touch the shared (contended) countdown | |
| // once the local pending amount reaches `MI_PROFILE_COUNTDOWN_THRESHOLD`. | |
| *interval_pending += bytes_since_last_sample; | |
| if (*interval_pending >= (mi_ssize_t)MI_PROFILE_COUNTDOWN_THRESHOLD) { | |
| sampled_bytes = *interval_pending; | |
| *interval_pending = 0; | |
| } | |
| } | |
| // update profile interval countdown if we have sampled bytes | |
| if (sampled_bytes > 0) { | |
| const mi_ssize_t new_countdown = mi_atomic_addi_relaxed(alloc_interval_countdown, -sampled_bytes) - sampled_bytes; | |
| if (new_countdown <= 0 && new_countdown + sampled_bytes > 0) { | |
| mi_atomic_storess_relaxed(alloc_interval_countdown, (mi_ssize_t)alloc_interval_size); | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| // Check if the (global) in-use byte total has reached `inuse_interval_threshold` | |
| static bool mi_inuse_threshold_update(size_t new_total, size_t inuse_interval_size, _Atomic(size_t)* inuse_interval_threshold) { | |
| size_t threshold = mi_atomic_load_relaxed(inuse_interval_threshold); | |
| while (new_total >= threshold) { | |
| size_t next_threshold = threshold + inuse_interval_size; | |
| while (next_threshold <= new_total) { next_threshold += inuse_interval_size; } // skip ahead if we grew by more than one interval | |
| if (mi_atomic_cas_weak_relaxed(inuse_interval_threshold, &threshold, next_threshold)) { | |
| return true; | |
| } | |
| // CAS failed: `threshold` now holds the current value; retry (another thread may have already advanced it far enough) | |
| } | |
| return false; | |
| } | |
| // Check if `now` has reached `time_interval_deadline`, and if so, atomically advance the | |
| // deadline to the next multiple of `time_interval_secs` beyond `now`, returning `true` (to the | |
| // single thread that won the race) so a snapshot is triggered exactly once. | |
| static bool mi_time_threshold_update(mi_msecs_t now, size_t time_interval_secs, _Atomic(mi_msecs_t)* time_interval_deadline) { | |
| const mi_msecs_t interval_msecs = (mi_msecs_t)time_interval_secs * 1000; | |
| mi_msecs_t deadline = mi_atomic_loadi64_relaxed(time_interval_deadline); | |
| while (now >= deadline) { | |
| mi_msecs_t next_deadline = deadline + interval_msecs; | |
| while (next_deadline <= now) { next_deadline += interval_msecs; } // skip ahead if more than one interval has passed | |
| if (mi_atomic_casi64_strong_relaxed(time_interval_deadline, &deadline, next_deadline)) { | |
| return true; | |
| } | |
| // CAS failed: `deadline` now holds the current value; retry (another thread may have already advanced it far enough) | |
| } | |
| return false; | |
| } | |
| // Sample allocation event and update profiling data accordingly | |
| static size_t mi_cdecl on_alloc(mi_profiler_t* profiler, mi_profiler_sample_data_t* data, void* ptr, size_t requested_size, size_t threshold, uint64_t bytes_since_last_sample, mi_heap_t* heap) | |
| { | |
| MI_UNUSED(threshold); MI_UNUSED(ptr); | |
| mi_pprof_profiler_t* prof = downcast(profiler); | |
| mi_theap_t* theap = _mi_heap_theap(heap); | |
| mi_location_t* loc = mi_location_get(prof); // nested allocations (e.g. by `backtrace`) are not sampled (see `mi_profiler_on_alloc`) | |
| const size_t new_threshold = mi_exp_sample(theap, prof->sample_threshold); // randomized (Poisson) next sample threshold | |
| const size_t alloc_size = requested_size; // or mi_heap_usable_size(heap,ptr) ? | |
| if (data!=NULL) { | |
| // pass the location to the corresponding mi_free (maybe called from another thread) | |
| mi_assert(data->user_data_size >= 2*sizeof(void*)); | |
| data->user_data[0] = loc; | |
| data->user_data[1] = (void*)((uintptr_t)alloc_size); | |
| } | |
| if (loc!=NULL) { | |
| // `on_alloc` for a given location only ever runs on its one owning (allocating) | |
| // thread, so plain increments are safe here (no concurrent writers possible). | |
| loc->alloc_bytes += (int64_t)alloc_size; | |
| loc->alloc_count += 1; | |
| // `inuse_count`/`inuse_bytes` can be decremented concurrently by `on_free` on any | |
| // thread, so these updates must be atomic. | |
| mi_atomic_add_relaxed(&loc->inuse_bytes, alloc_size); | |
| mi_atomic_increment_relaxed(&loc->inuse_count); | |
| } | |
| if (prof->alloc_interval_size > 0) { // interval-based profiling enabled | |
| mi_ssize_t* pinterval_pending = (mi_ssize_t*)&theap->profiler_reserved1; | |
| if (mi_interval_update(bytes_since_last_sample, prof->alloc_interval_size, &prof->alloc_interval_countdown, pinterval_pending)) { | |
| mi_profiler_snapshot(profiler); | |
| } | |
| } | |
| if (prof->inuse_interval_size > 0) { // snapshot whenever (sampled) in-use memory has grown by `inuse_interval_size` bytes | |
| const size_t total_inuse_bytes = mi_atomic_add_relaxed(&prof->inuse_bytes, alloc_size) + alloc_size; | |
| if (mi_inuse_threshold_update(total_inuse_bytes, prof->inuse_interval_size, &prof->inuse_interval_threshold)) { | |
| mi_profiler_snapshot(profiler); | |
| } | |
| } | |
| if (prof->time_interval_secs > 0) { // snapshot whenever `time_interval_secs` seconds have passed | |
| if (mi_time_threshold_update(_mi_clock_now(), prof->time_interval_secs, &prof->time_interval_deadline)) { | |
| mi_profiler_snapshot(profiler); | |
| } | |
| } | |
| return new_threshold; | |
| } | |
| // Sample free event and update profiling data accordingly | |
| static void mi_cdecl on_free(mi_profiler_t* profiler, mi_profiler_sample_data_t* data, void* ptr, mi_heap_t* heap) | |
| { | |
| MI_UNUSED(heap); MI_UNUSED(ptr); | |
| mi_pprof_profiler_t* prof = downcast(profiler); | |
| if (data!=NULL) { | |
| mi_assert(data->user_data_size >= 2*sizeof(void*)); | |
| mi_location_t* loc = (mi_location_t*)data->user_data[0]; | |
| const size_t freed_bytes = (size_t)((uintptr_t)data->user_data[1]); | |
| if (loc!=NULL) { | |
| // different threads can free allocations that share the same (allocating-thread, | |
| // callstack) location concurrently, so these updates must be atomic. | |
| mi_atomic_sub_relaxed(&loc->inuse_bytes, freed_bytes); | |
| mi_atomic_decrement_relaxed(&loc->inuse_count); | |
| } | |
| if (prof->inuse_interval_size > 0) { // keep the (global) in-use total in sync while interval-based snapshotting is enabled | |
| mi_atomic_sub_relaxed(&prof->inuse_bytes, freed_bytes); | |
| } | |
| } | |
| } | |
| static void mi_cdecl on_snapshot(mi_profiler_t* profiler) { | |
| mi_pprof_profiler_t* prof = downcast(profiler); | |
| mi_pprof_profiler_snapshot(prof); | |
| } | |
| // Create a new profiler. If `base_file_name` ends with a recognized extension (`.heap` or `.text`), | |
| // that extension selects the text snapshot format and is stripped from the stored base file name | |
| // (the actual snapshot files always get their own `.<seq>.heap`/`.<seq>.pb` extension, see `mi_pprof_profiler_snapshot`). | |
| // Any other extension (or none) keeps the default (protobuf) snapshot format and is left untouched. | |
| mi_profiler_t* mi_pprof_profiler_new(size_t initial_threshold, const char* base_file_name, size_t alloc_interval_size, size_t inuse_interval_size, size_t time_interval_secs) { | |
| // heap just for the profiler itself | |
| mi_heap_t* heap = mi_heap_new(); | |
| if (heap == NULL) return NULL; | |
| mi_heap_profile_disable(heap); // don't sample allocations in this heap | |
| // allocate and initialize the profiler structure from this heap | |
| mi_pprof_profiler_t* prof = mi_heap_zalloc_tp(mi_pprof_profiler_t,heap); | |
| if (prof == NULL) { mi_heap_delete(heap); return NULL; } | |
| prof->sample_threshold = initial_threshold; | |
| prof->profile_heap = heap; | |
| prof->base_file_name = (base_file_name != NULL ? mi_heap_strndup(heap, base_file_name, 1024) : NULL); | |
| prof->format_text = false; // default: protobuf format, unless overridden by a recognized extension below | |
| if (prof->base_file_name != NULL) { | |
| // only look for a '.' extension after the last path separator (if any) | |
| const char* start = prof->base_file_name; | |
| const char* sep = _mi_strrchr(start, '/'); | |
| if (sep != NULL) start = sep + 1; | |
| sep = _mi_strrchr(start, '\\'); | |
| if (sep != NULL) start = sep + 1; | |
| const char* dot = _mi_strrchr(start, '.'); | |
| if (dot != NULL) { | |
| if (_mi_streq(dot + 1, "heap") || _mi_streq(dot + 1, "text")) { | |
| prof->format_text = true; | |
| } | |
| prof->base_file_name[dot - prof->base_file_name] = 0; // always strip the extension, whether recognized or not | |
| } | |
| } | |
| prof->alloc_interval_size = (alloc_interval_size > (size_t)MI_SSIZE_MAX ? (size_t)MI_SSIZE_MAX : alloc_interval_size); // cap so it always fits in a `mi_ssize_t` | |
| mi_atomic_storess_relaxed(&prof->alloc_interval_countdown, (mi_ssize_t)prof->alloc_interval_size); // 0 if disabled: `on_alloc` never decrements/checks in that case | |
| prof->inuse_interval_size = inuse_interval_size; | |
| mi_atomic_store_relaxed(&prof->inuse_interval_threshold, prof->inuse_interval_size); // 0 if disabled (`inuse_interval_size` defaults to 0): `on_alloc` never checks in that case | |
| prof->time_interval_secs = time_interval_secs; | |
| prof->start_time_secs = mi_prim_time_secs(); | |
| prof->start_clock = _mi_clock_now(); | |
| mi_atomic_storei64_relaxed(&prof->time_interval_deadline, prof->start_clock + (mi_msecs_t)prof->time_interval_secs * 1000); // deadline stays unreachable (now, i.e. always due) if disabled, but `on_alloc` never checks in that case | |
| if (!mi_locations_init(heap, &prof->locations)) { | |
| // `mi_heap_destroy` frees the live blocks of this private heap, i.e. the | |
| // profiler object and the base file name. (`mi_heap_delete` would instead | |
| // move them to the main heap, losing our only reference.) | |
| mi_heap_destroy(heap); | |
| return NULL; | |
| } | |
| prof->profiler.initial_sample_rate = initial_threshold; | |
| prof->profiler.sample_data_size = 2*sizeof(void*); // we store the location and the allocation size in the sample data | |
| prof->profiler.on_alloc = &on_alloc; | |
| prof->profiler.on_free = &on_free; | |
| prof->profiler.on_snapshot = &on_snapshot; | |
| return &prof->profiler; | |
| } | |
| void mi_pprof_profiler_delete(mi_profiler_t* profiler) { | |
| if (profiler == NULL) return; | |
| mi_pprof_profiler_t* prof = downcast(profiler); | |
| mi_heap_t* heap = prof->profile_heap; | |
| mi_locations_done(heap, &prof->locations); | |
| mi_free(prof->base_file_name); | |
| mi_free(prof); | |
| mi_heap_delete(heap); | |
| } | |
| // --------------------------------------------------------------------------- | |
| // A basic hash table of locations. All memory (the bucket array, the | |
| // locations, and their callstack frames) is allocated from the profiler's | |
| // own heap so it does not interfere with the profiled allocations. | |
| // Collisions are resolved with simple chaining. | |
| // | |
| // `on_alloc` and `on_free` (and thus `mi_locations_find_or_insert`) can run | |
| // concurrently on multiple threads and are made thread-safe without a lock: | |
| // a bucket is a singly-linked list that only ever grows by prepending a new, | |
| // fully initialized location; once published, a location's identifying | |
| // fields (`hash`, `thread_id`, `callstack`) never change again. Moreover, | |
| // since the hash (and thus the location itself) is derived from the calling | |
| // thread's own id, a location is only ever looked up by the same thread | |
| // that creates it, so there is never a concurrent insert race for the | |
| // same location. | |
| // --------------------------------------------------------------------------- | |
| // A reasonably large prime bucket count to keep collision chains short | |
| #define MI_LOCATIONS_BUCKET_COUNT (16 * 1024 - 3) /* 16381, prime */ | |
| // FNV-1a hash, mixed over the thread id and the (precomputed) callstack hash; | |
| #if SIZE_MAX > UINT32_MAX | |
| #define MI_FNV_OFFSET_BASIS ((size_t)0xcbf29ce484222325ULL) // FNV-1a 64-bit offset basis | |
| #define MI_FNV_PRIME ((size_t)0x100000001b3ULL) // FNV-1a 64-bit prime | |
| #else | |
| #define MI_FNV_OFFSET_BASIS ((size_t)0x811c9dc5UL) // FNV-1a 32-bit offset basis | |
| #define MI_FNV_PRIME ((size_t)0x01000193UL) // FNV-1a 32-bit prime | |
| #endif | |
| static size_t mi_location_hash(mi_thread_id_t thread_id, size_t callstack_hash) { | |
| size_t hash = MI_FNV_OFFSET_BASIS; | |
| const size_t prime = MI_FNV_PRIME; | |
| hash = (hash ^ (size_t)thread_id) * prime; | |
| hash = (hash ^ callstack_hash) * prime; | |
| return hash; | |
| } | |
| // Initialize a location hash table | |
| static bool mi_locations_init(mi_heap_t* heap, mi_locations_t* locations) { | |
| locations->bucket_count = MI_LOCATIONS_BUCKET_COUNT; | |
| locations->buckets = (_Atomic(mi_location_t*)*)mi_heap_zalloc(heap, locations->bucket_count * sizeof(*locations->buckets)); | |
| mi_atomic_store_relaxed(&locations->count, (size_t)0); | |
| return (locations->buckets != NULL); | |
| } | |
| // Free all memory associated with the location hash table. | |
| // Not thread-safe: assumes no concurrent `on_alloc`/`on_free` (see the note above). | |
| static void mi_locations_done(mi_heap_t* heap, mi_locations_t* locations) { | |
| MI_UNUSED(heap); | |
| if (locations->buckets == NULL) return; | |
| for (size_t i = 0; i < locations->bucket_count; i++) { | |
| mi_location_t* loc = mi_atomic_load_ptr_relaxed(mi_location_t, &locations->buckets[i]); | |
| while (loc != NULL) { | |
| mi_location_t* next = mi_atomic_load_ptr_relaxed(mi_location_t, &loc->next); | |
| if (loc->callstack.frames != NULL) { | |
| mi_free(loc->callstack.frames); | |
| } | |
| mi_free(loc); | |
| loc = next; | |
| } | |
| } | |
| mi_free(locations->buckets); | |
| locations->buckets = NULL; | |
| mi_atomic_store_relaxed(&locations->count, (size_t)0); | |
| } | |
| // Match a location's identifying fields; these never change after publication. | |
| static bool mi_location_matches(const mi_location_t* loc, size_t hash, mi_thread_id_t thread_id, const mi_callstack_t* callstack) { | |
| return (loc->hash == hash && loc->thread_id == thread_id && | |
| loc->callstack.count == callstack->count && | |
| (callstack->count == 0 || mi_memcmp(loc->callstack.frames, callstack->frames, callstack->count * sizeof(void*)) == 0)); | |
| } | |
| // Find an existing location matching `thread_id` and `callstack`, or insert | |
| // a fresh, zero-initialized one (with `hash` and identifying fields filled in) | |
| // if none exists yet. Returns NULL only on allocation failure. | |
| // Thread-safe (see the note above): the common case (an existing location) | |
| // is a lock-free, read-only traversal using relaxed loads; only inserting a | |
| // genuinely new location uses a CAS (and even then, only ever races against | |
| // other threads inserting *different* locations into the same bucket). | |
| static mi_location_t* mi_locations_find_or_insert(mi_heap_t* heap, mi_locations_t* locations, mi_thread_id_t thread_id, const mi_callstack_t* callstack, size_t callstack_hash) { | |
| if (locations->buckets == NULL) return NULL; | |
| const size_t hash = mi_location_hash(thread_id, callstack_hash); | |
| const size_t idx = hash % locations->bucket_count; | |
| _Atomic(mi_location_t*)* bucket = &locations->buckets[idx]; | |
| // fast path: lock-free, read-only lookup in the (possibly concurrently growing) chain | |
| for (mi_location_t* loc = mi_atomic_load_ptr_relaxed(mi_location_t, bucket); loc != NULL; | |
| loc = mi_atomic_load_ptr_relaxed(mi_location_t, &loc->next)) | |
| { | |
| if (mi_location_matches(loc, hash, thread_id, callstack)) return loc; | |
| } | |
| // not found: allocate and fully initialize a new location; it is not yet | |
| // visible to any other thread so plain (non-atomic) stores are fine here. | |
| mi_location_t* loc = mi_heap_zalloc_tp(mi_location_t,heap); | |
| if (loc == NULL) return NULL; | |
| if (callstack->count > 0) { | |
| loc->callstack.frames = (void**)mi_heap_mallocn(heap, callstack->count, sizeof(void*)); | |
| if (loc->callstack.frames == NULL) { mi_free(loc); return NULL; } // free the not-yet-published location | |
| mi_memcpy(loc->callstack.frames, callstack->frames, callstack->count * sizeof(void*)); | |
| } | |
| loc->callstack.count = callstack->count; | |
| loc->thread_id = thread_id; | |
| loc->hash = hash; | |
| // publish the new location at the head of the bucket with a CAS loop. Since | |
| // a location's key includes the calling thread's own id, no other thread | |
| // can ever race to insert this *same* key -- a CAS can only fail because | |
| // some other thread prepended an unrelated location to this bucket in the | |
| // meantime, so we simply retry with the updated head (no duplicate check | |
| // needed, unlike a typical lock-free insert). | |
| mi_location_t* head = mi_atomic_load_ptr_relaxed(mi_location_t, bucket); | |
| do { | |
| mi_atomic_store_ptr_relaxed(mi_location_t, &loc->next, head); // `loc` not yet published: plain store is fine | |
| } while (!mi_atomic_cas_ptr_weak_release(mi_location_t, bucket, &head, loc)); // `head` is updated to the current value on failure | |
| mi_atomic_increment_relaxed(&locations->count); // just a statistic | |
| return loc; | |
| } | |
| // --------------------------------------------------------------------------- | |
| // Avoid OS specific code when MI_PROFILE_USE_BUILTIN is not enabled | |
| // --------------------------------------------------------------------------- | |
| #if !MI_PROFILE_USE_BUILTIN | |
| static size_t mi_prim_backtrace(void** buffer, size_t max_depth, size_t* hash) { | |
| MI_UNUSED(buffer); | |
| MI_UNUSED(max_depth); | |
| *hash = 0; | |
| return 0; | |
| } | |
| static int64_t mi_prim_time_secs(void) { | |
| return 0; | |
| } | |
| void mi_pprof_profiler_snapshot(mi_pprof_profiler_t* profiler) { | |
| MI_UNUSED(profiler); | |
| } | |
| #else | |
| #include <stdio.h> // FILE, fopen, fprintf, fclose | |
| #include <time.h> // time | |
| #if defined(_WIN32) | |
| #include <fcntl.h> // _O_CREAT, _O_TRUNC, _O_WRONLY, _O_BINARY, _O_TEXT | |
| #include <io.h> // _sopen_s, _fdopen, _close | |
| #include <share.h> // _SH_DENYNO | |
| #include <sys/stat.h> // _S_IREAD, _S_IWRITE | |
| #elif MI_HAS_UNISTDH | |
| #include <fcntl.h> // open, O_CREAT, O_EXCL, O_NOFOLLOW | |
| #include <unistd.h> // close, getuid, geteuid, getgid, getegid, issetugid | |
| #include <sys/stat.h> // S_IRUSR, S_IWUSR | |
| #endif | |
| // forward declarations; implemented further below. | |
| static void mi_pprof_write_mapped_libraries(FILE* f); | |
| static bool mi_pprof_snapshot_text(mi_pprof_profiler_t* prof, const char* fname); | |
| static bool mi_pprof_snapshot_proto(mi_pprof_profiler_t* prof, const char* fname); | |
| // Detect if we are running with elevated privileges (e.g. a setuid/setgid executable or similar), | |
| // in which case we must not trust `MIMALLOC_PROFILE` environment. | |
| #if defined(_WIN32) | |
| static bool mi_is_elevated_process(void) { | |
| return false; // no direct setuid/setgid equivalent; a UAC elevation does not inherit the parent's environment | |
| } | |
| #elif MI_HAS_UNISTDH && (defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__DragonFly__)) | |
| static bool mi_is_elevated_process(void) { | |
| return (issetugid() != 0); | |
| } | |
| #elif MI_HAS_UNISTDH | |
| static bool mi_is_elevated_process(void) { | |
| return (getuid() != geteuid() || getgid() != getegid()); | |
| } | |
| #else | |
| static bool mi_is_elevated_process(void) { | |
| return false; // no portable way to check without `unistd.h`; assume not elevated | |
| } | |
| #endif | |
| static int64_t mi_prim_time_secs(void) { | |
| const time_t t = time(NULL); | |
| return (t == (time_t)-1 ? 0 : (int64_t)t); | |
| } | |
| // Open a snapshot file for writing, refusing to follow a symlink at `fname` (to avoid an | |
| // attacker redirecting the write to an arbitrary target) and restricting the file's | |
| // permissions to the owner only (snapshots can contain sensitive address/callstack data). | |
| // On platforms without these POSIX/Win32 facilities (e.g. wasm/Emscripten), we fall | |
| // back to a fully portable plain `fopen` (without the symlink/permission hardening). | |
| static FILE* mi_pprof_fopen(const char* fname, bool binary) { | |
| #if defined(_WIN32) | |
| int oflag = _O_CREAT | _O_TRUNC | _O_WRONLY | (binary ? _O_BINARY : _O_TEXT); | |
| int fd = -1; | |
| if (_sopen_s(&fd, fname, oflag, _SH_DENYNO, _S_IREAD | _S_IWRITE) != 0 || fd < 0) return NULL; | |
| FILE* f = _fdopen(fd, binary ? "wb" : "w"); | |
| if (f == NULL) { _close(fd); } | |
| return f; | |
| #elif MI_HAS_UNISTDH | |
| int oflag = O_CREAT | O_TRUNC | O_WRONLY; | |
| #if defined(O_NOFOLLOW) | |
| oflag |= O_NOFOLLOW; // fail (ELOOP) if `fname` is a symlink | |
| #endif | |
| const int fd = open(fname, oflag, S_IRUSR | S_IWUSR); // restrict to owner read/write only | |
| if (fd < 0) return NULL; | |
| FILE* f = fdopen(fd, binary ? "wb" : "w"); | |
| if (f == NULL) { close(fd); } | |
| return f; | |
| #else | |
| return fopen(fname, binary ? "wb" : "w"); // fully portable fallback: no symlink/permission hardening available | |
| #endif | |
| } | |
| // Write out the current profiler data to a new snapshot file named | |
| // `<base_file_name>.<seq>.<ext>` with an incrementing sequence number | |
| // (`.heap` for the text format, mimicking the naming used by the original | |
| // (gperftools) pprof heap profiler; `.pb` for the (uncompressed) protobuf | |
| // format). The format is chosen when the profiler is created (see | |
| // `mi_pprof_profiler_new`). The base file name is also set at creation time; | |
| // if it is NULL, no snapshot is written and this function does nothing. | |
| static void mi_pprof_profiler_snapshot(mi_pprof_profiler_t* prof) { | |
| if (prof == NULL) return; | |
| bool was_running = mi_profiler_stop(&prof->profiler); | |
| mi_locations_t* locations = &prof->locations; | |
| if (locations->buckets != NULL && prof->base_file_name != NULL) { | |
| const size_t seq = ++prof->snapshot_count; | |
| char fname[1024]; | |
| bool ok; | |
| if (!prof->format_text) { | |
| _mi_snprintf(fname, sizeof(fname), "%s.%04zu.pb", prof->base_file_name, seq); | |
| ok = mi_pprof_snapshot_proto(prof, fname); | |
| } | |
| else { | |
| _mi_snprintf(fname, sizeof(fname), "%s.%04zu.heap", prof->base_file_name, seq); | |
| ok = mi_pprof_snapshot_text(prof, fname); | |
| } | |
| // A snapshot is best effort: it can fail when the profiler's own heap is out | |
| // of memory (or the file cannot be created), in which case no (or only a | |
| // partial) snapshot is written. Never let that affect the profiled program. | |
| if (ok) { | |
| _mi_verbose_message("pprof profiler snapshot: %s\n", fname); | |
| } | |
| else { | |
| _mi_verbose_message("pprof profiler: unable to write snapshot (out of memory?)\n"); | |
| } | |
| } | |
| if (was_running) { mi_profiler_start(&prof->profiler); } | |
| } | |
| // --------------------------------------------------------------------------- | |
| // Basic backtraces | |
| // --------------------------------------------------------------------------- | |
| #if MI_HAS_EXECINFOH || MI_HAS_UNWINDH | |
| static size_t mi_backtrace_hash(void** frames, size_t depth) { | |
| size_t hash = MI_FNV_OFFSET_BASIS; | |
| for (size_t i = 0; i < depth; i++) { | |
| hash = (hash ^ (size_t)((uintptr_t)frames[i])) * MI_FNV_PRIME; | |
| } | |
| return hash; | |
| } | |
| #endif | |
| #if _WIN32 | |
| #include <windows.h> // CaptureStackBackTrace | |
| static mi_decl_noinline size_t mi_prim_backtrace(void** buffer, size_t max_depth, size_t* hash) { | |
| if (max_depth > ULONG_MAX) { max_depth = ULONG_MAX; } | |
| // On Windows XP/Server 2003, `FramesToSkip + FramesToCapture` must be less than 63; | |
| // clamp defensively so we never exceed this, regardless of the actual OS version. | |
| const size_t skip = 1 + MI_BACKTRACE_SKIP_FRAMES; // +1 for `mi_prim_backtrace`'s own frame | |
| const size_t win_limit = 62; // strictly less than 63 | |
| if (skip + max_depth >= win_limit) { max_depth = (skip <= win_limit ? win_limit - skip : 0); } | |
| if (max_depth == 0) { *hash = 0; return 0; } | |
| DWORD win_hash = 0; | |
| const size_t depth = (size_t)CaptureStackBackTrace((ULONG)skip, (ULONG)max_depth, buffer, &win_hash); | |
| *hash = (size_t)win_hash; // use the hash computed by `CaptureStackBackTrace` itself | |
| return depth; | |
| } | |
| #elif MI_HAS_EXECINFOH | |
| #include <execinfo.h> // backtrace | |
| static mi_decl_noinline size_t mi_prim_backtrace(void** buffer, size_t max_depth, size_t* hash) { | |
| const size_t skip = 1 + MI_BACKTRACE_SKIP_FRAMES; // +1 for `mi_prim_backtrace`'s own frame | |
| if (max_depth <= skip) { *hash = 0; return 0; } | |
| if (max_depth > INT_MAX) { max_depth = INT_MAX; } | |
| const size_t raw_depth = (size_t)backtrace(buffer, (int)(max_depth - skip)); | |
| if (raw_depth <= skip) { *hash = 0; return 0; } | |
| const size_t depth = raw_depth - skip; | |
| memmove(buffer, buffer + skip, depth * sizeof(void*)); | |
| *hash = mi_backtrace_hash(buffer, depth); // no platform-provided hash, compute our own | |
| return depth; | |
| } | |
| #elif MI_HAS_UNWINDH | |
| #include <unwind.h> // _Unwind_Backtrace (from the compiler's unwinder: libgcc_s or LLVM libunwind) | |
| typedef struct mi_unwind_state_s { | |
| void** buffer; | |
| size_t max_depth; | |
| size_t skip; | |
| size_t depth; | |
| } mi_unwind_state_t; | |
| static _Unwind_Reason_Code mi_unwind_frame(struct _Unwind_Context* ctx, void* arg) { | |
| mi_unwind_state_t* const st = (mi_unwind_state_t*)arg; | |
| const uintptr_t ip = (uintptr_t)_Unwind_GetIP(ctx); | |
| if (ip == 0) return _URC_END_OF_STACK; | |
| if (st->skip > 0) { st->skip--; return _URC_NO_REASON; } | |
| st->buffer[st->depth++] = (void*)ip; | |
| return (st->depth >= st->max_depth ? _URC_END_OF_STACK : _URC_NO_REASON); | |
| } | |
| static mi_decl_noinline size_t mi_prim_backtrace(void** buffer, size_t max_depth, size_t* hash) { | |
| // used as a fallback on platforms without `execinfo.h` (e.g. musl-based Linux like Alpine) | |
| mi_unwind_state_t st = { buffer, max_depth, 1 + MI_BACKTRACE_SKIP_FRAMES /* +1 for `mi_prim_backtrace`'s own frame */, 0 }; | |
| if (max_depth > 0) { _Unwind_Backtrace(&mi_unwind_frame, &st); } | |
| *hash = mi_backtrace_hash(buffer, st.depth); // no platform-provided hash, compute our own | |
| return st.depth; | |
| } | |
| #else | |
| static size_t mi_prim_backtrace(void** buffer, size_t max_depth, size_t* hash) { | |
| MI_UNUSED(buffer); | |
| MI_UNUSED(max_depth); | |
| *hash = 0; | |
| return 0; | |
| } | |
| #endif | |
| // --------------------------------------------------------------------------- | |
| // Write out mapped libraries | |
| // --------------------------------------------------------------------------- | |
| // A loaded module's address range and (if known) its file path; used both to | |
| // print the `MAPPED_LIBRARIES` trailer of the text format and to build the | |
| // `Mapping` entries of the protobuf format (see `mi_pprof_snapshot_proto`). | |
| typedef struct { | |
| uintptr_t start; | |
| uintptr_t end; | |
| char* path; // heap-allocated (owned), or NULL if unknown | |
| } mi_pprof_module_t; | |
| typedef struct { | |
| mi_pprof_module_t* modules; | |
| size_t count; | |
| size_t capacity; | |
| bool failed; // set if an allocation failed; the mapping list is then incomplete | |
| } mi_pprof_modules_t; | |
| mi_decl_maybe_unused static void mi_pprof_modules_add(mi_heap_t* heap, mi_pprof_modules_t* mods, uintptr_t start, uintptr_t end, const char* path) { | |
| if (mods->failed) return; | |
| if (mods->count == mods->capacity) { | |
| const size_t new_capacity = (mods->capacity == 0 ? 16 : mods->capacity * 2); | |
| mi_pprof_module_t* const new_modules = (mi_pprof_module_t*)mi_heap_realloc(heap, mods->modules, new_capacity * sizeof(*mods->modules)); | |
| if (new_modules == NULL) { mods->failed = true; return; } | |
| mods->modules = new_modules; | |
| mods->capacity = new_capacity; | |
| } | |
| char* const copy = (path != NULL ? mi_heap_strndup(heap, path, 1024) : NULL); | |
| if (path != NULL && copy == NULL) { mods->failed = true; return; } | |
| mi_pprof_module_t* m = &mods->modules[mods->count++]; | |
| m->start = start; | |
| m->end = end; | |
| m->path = copy; | |
| } | |
| static void mi_pprof_modules_done(mi_pprof_modules_t* mods) { | |
| for (size_t i = 0; i < mods->count; i++) { mi_free(mods->modules[i].path); } | |
| mi_free(mods->modules); | |
| mods->modules = NULL; | |
| mods->count = 0; | |
| mods->capacity = 0; | |
| } | |
| // On Linux, `pprof` can symbolize addresses using the `MAPPED_LIBRARIES` trailer | |
| // which is just a copy of `/proc/self/maps`. On macOS there is no equivalent | |
| // file so we reconstruct a similarly formatted line (`start-end perm ... path`) | |
| // per loaded Mach-O image using the dyld APIs: the range is taken from the | |
| // image's `__TEXT` segment, which is also the "load address" that tools like | |
| // `atos` use to symbolize addresses (`atos -o <path> -l <start> <addr>`). On | |
| // Windows we walk the process address space with `VirtualQuery` | |
| // On other platforms we leave this section empty (`pprof` can still work | |
| // without it if not using PIE, or when combined with tools like `atos` / | |
| // `addr2line` / a symbol server). | |
| // A small wrapper that formats with our own minimal `_mi_vsnprintf` (so call | |
| // sites can use its `%l8x`/`%l8d`/`%tx` extensions instead of <inttypes.h>'s | |
| // `PRIx64`/`PRIu64`/`PRIxPTR` macros) and writes the result directly to a `FILE*`. | |
| // (Distinct from the existing `_mi_fprintf(mi_output_fun*, void*, ...)` in | |
| // `util/options.c`, which targets mimalloc's redirectable output callback instead | |
| // of an arbitrary `FILE*`.) | |
| static void mi_fprintf(FILE* f, const char* fmt, ...) { | |
| char buf[512]; | |
| va_list args; | |
| va_start(args, fmt); | |
| _mi_vsnprintf(buf, sizeof(buf), fmt, args); | |
| va_end(args); | |
| fputs(buf, f); | |
| } | |
| #if defined(_WIN32) | |
| #include <windows.h> | |
| // Walk the process address space, calling `on_module(ctx, base, end, path)` | |
| // once for each loaded module. Consecutive `MEM_IMAGE` regions that belong to | |
| // the same module (same allocation base) are merged into a single range. | |
| static void mi_walk_modules_win32(void (*on_module)(void* ctx, uintptr_t base, uintptr_t end, const char* path), void* ctx) { | |
| SYSTEM_INFO si; | |
| GetSystemInfo(&si); | |
| uint8_t* addr = (uint8_t*)si.lpMinimumApplicationAddress; | |
| uint8_t* const addr_max = (uint8_t*)si.lpMaximumApplicationAddress; | |
| void* cur_base = NULL; | |
| uintptr_t cur_end = 0; | |
| while (addr < addr_max) { | |
| MEMORY_BASIC_INFORMATION mbi; | |
| const SIZE_T n = VirtualQuery(addr, &mbi, sizeof(mbi)); | |
| if (n == 0) break; // no more (queryable) regions | |
| if (mbi.Type == MEM_IMAGE && mbi.State != MEM_FREE) { | |
| if (mbi.AllocationBase == cur_base) { | |
| // extend the current module's range | |
| cur_end = (uintptr_t)mbi.BaseAddress + mbi.RegionSize; | |
| } | |
| else { | |
| // a new module starts: flush the previous one first | |
| if (cur_base != NULL) { | |
| char path[MAX_PATH]; | |
| const DWORD len = GetModuleFileNameA((HMODULE)cur_base, path, (DWORD)sizeof(path)); | |
| on_module(ctx, (uintptr_t)cur_base, cur_end, (len > 0 ? path : "")); | |
| } | |
| cur_base = mbi.AllocationBase; | |
| cur_end = (uintptr_t)mbi.BaseAddress + mbi.RegionSize; | |
| } | |
| } | |
| else if (cur_base != NULL) { | |
| // the module's regions ended: flush it | |
| char path[MAX_PATH]; | |
| const DWORD len = GetModuleFileNameA((HMODULE)cur_base, path, (DWORD)sizeof(path)); | |
| on_module(ctx, (uintptr_t)cur_base, cur_end, (len > 0 ? path : "")); | |
| cur_base = NULL; | |
| } | |
| const uint8_t* next = (const uint8_t*)mbi.BaseAddress + mbi.RegionSize; | |
| if (next <= addr) break; // guard against a non-advancing region | |
| addr = (uint8_t*)next; | |
| } | |
| if (cur_base != NULL) { | |
| char path[MAX_PATH]; | |
| const DWORD len = GetModuleFileNameA((HMODULE)cur_base, path, (DWORD)sizeof(path)); | |
| on_module(ctx, (uintptr_t)cur_base, cur_end, (len > 0 ? path : "")); | |
| } | |
| } | |
| static void mi_write_mapped_libraries_on_module_win32(void* ctx, uintptr_t base, uintptr_t end, const char* path) { | |
| FILE* f = (FILE*)ctx; | |
| mi_fprintf(f, "%tx-%tx r-xp 00000000 00:00 0 %s\n", base, end, path); | |
| } | |
| static void mi_write_mapped_libraries_win32(FILE* f) { | |
| mi_walk_modules_win32(&mi_write_mapped_libraries_on_module_win32, f); | |
| } | |
| typedef struct { mi_heap_t* heap; mi_pprof_modules_t* mods; } mi_pprof_collect_modules_win32_ctx_t; | |
| static void mi_pprof_collect_modules_win32_on_module(void* ctx, uintptr_t base, uintptr_t end, const char* path) { | |
| mi_pprof_collect_modules_win32_ctx_t* c = (mi_pprof_collect_modules_win32_ctx_t*)ctx; | |
| mi_pprof_modules_add(c->heap, c->mods, base, end, (path[0] != 0 ? path : NULL)); | |
| } | |
| static void mi_pprof_collect_modules_win32(mi_heap_t* heap, mi_pprof_modules_t* mods) { | |
| mi_pprof_collect_modules_win32_ctx_t ctx = { heap, mods }; | |
| mi_walk_modules_win32(&mi_pprof_collect_modules_win32_on_module, &ctx); | |
| } | |
| #elif defined(__APPLE__) | |
| #include <mach-o/dyld.h> | |
| #include <mach-o/loader.h> | |
| // Find the (slid) virtual address range of the `__TEXT` segment of a loaded | |
| // Mach-O image; this is the image's load address as used for symbolication. | |
| static bool mi_macho_image_range(const struct mach_header* mh, intptr_t slide, uintptr_t* start, uintptr_t* end) { | |
| const bool is64 = (mh->magic == MH_MAGIC_64 || mh->magic == MH_CIGAM_64); | |
| const uint8_t* cmd_ptr = (const uint8_t*)mh + (is64 ? sizeof(struct mach_header_64) : sizeof(struct mach_header)); | |
| for (uint32_t c = 0; c < mh->ncmds; c++) { | |
| const struct load_command* lc = (const struct load_command*)cmd_ptr; | |
| if (is64 && lc->cmd == LC_SEGMENT_64) { | |
| const struct segment_command_64* seg = (const struct segment_command_64*)lc; | |
| if (_mi_streq(seg->segname, SEG_TEXT)) { | |
| *start = (uintptr_t)seg->vmaddr + (uintptr_t)slide; | |
| *end = *start + (uintptr_t)seg->vmsize; | |
| return true; | |
| } | |
| } | |
| else if (!is64 && lc->cmd == LC_SEGMENT) { | |
| const struct segment_command* seg = (const struct segment_command*)lc; | |
| if (_mi_streq(seg->segname, SEG_TEXT)) { | |
| *start = (uintptr_t)seg->vmaddr + (uintptr_t)slide; | |
| *end = *start + (uintptr_t)seg->vmsize; | |
| return true; | |
| } | |
| } | |
| cmd_ptr += lc->cmdsize; | |
| } | |
| return false; // no __TEXT segment found | |
| } | |
| static void mi_write_mapped_libraries_macos(FILE* f) { | |
| const uint32_t count = _dyld_image_count(); | |
| for (uint32_t i = 0; i < count; i++) { | |
| const struct mach_header* mh = _dyld_get_image_header(i); | |
| const char* name = _dyld_get_image_name(i); | |
| if (mh == NULL || name == NULL) continue; | |
| const intptr_t slide = _dyld_get_image_vmaddr_slide(i); | |
| uintptr_t start = 0; | |
| uintptr_t end = 0; | |
| if (!mi_macho_image_range(mh, slide, &start, &end)) continue; | |
| mi_fprintf(f, "%tx-%tx r-xp 00000000 00:00 0 %s\n", start, end, name); | |
| } | |
| } | |
| static void mi_pprof_collect_modules_macos(mi_heap_t* heap, mi_pprof_modules_t* mods) { | |
| const uint32_t count = _dyld_image_count(); | |
| for (uint32_t i = 0; i < count; i++) { | |
| const struct mach_header* mh = _dyld_get_image_header(i); | |
| const char* name = _dyld_get_image_name(i); | |
| if (mh == NULL || name == NULL) continue; | |
| const intptr_t slide = _dyld_get_image_vmaddr_slide(i); | |
| uintptr_t start = 0; | |
| uintptr_t end = 0; | |
| if (!mi_macho_image_range(mh, slide, &start, &end)) continue; | |
| mi_pprof_modules_add(heap, mods, start, end, name); | |
| } | |
| } | |
| #elif defined(__linux__) | |
| #include <stdlib.h> // strtoull | |
| // Copy `/proc/self/maps` verbatim; this is exactly the format `pprof` expects. | |
| static void mi_write_mapped_libraries_linux(FILE* f) { | |
| FILE* maps = fopen("/proc/self/maps", "r"); | |
| if (maps != NULL) { | |
| char line[512]; | |
| while (fgets(line, sizeof(line), maps) != NULL) { | |
| fputs(line, f); | |
| } | |
| fclose(maps); | |
| } | |
| } | |
| // Parse `/proc/self/maps` lines of the form | |
| // `<start>-<end> <perms> <offset> <dev> <inode> <path>` (path may be absent | |
| // for anonymous mappings, which are skipped here since they are not useful | |
| // for symbolization). | |
| static void mi_pprof_collect_modules_linux(mi_heap_t* heap, mi_pprof_modules_t* mods) { | |
| FILE* maps = fopen("/proc/self/maps", "r"); | |
| if (maps == NULL) return; | |
| char line[512]; | |
| while (fgets(line, sizeof(line), maps) != NULL) { | |
| char* end_ptr = NULL; | |
| const uintptr_t start = (uintptr_t)strtoull(line, &end_ptr, 16); | |
| if (end_ptr == NULL || *end_ptr != '-') continue; | |
| const uintptr_t end = (uintptr_t)strtoull(end_ptr + 1, &end_ptr, 16); | |
| // the path (if present) is the last whitespace-separated field on the line | |
| const char* path = strrchr(line, ' '); | |
| if (path == NULL) continue; | |
| while (*path == ' ') path++; | |
| size_t len = strlen(path); | |
| while (len > 0 && (path[len-1] == '\n' || path[len-1] == '\r')) { len--; } | |
| if (len == 0 || path[0] == '[') continue; // skip anonymous mappings like `[heap]`, `[stack]` | |
| char pathbuf[512]; | |
| if (len >= sizeof(pathbuf)) { len = sizeof(pathbuf) - 1; } | |
| memcpy(pathbuf, path, len); | |
| pathbuf[len] = 0; | |
| mi_pprof_modules_add(heap, mods, start, end, pathbuf); | |
| } | |
| fclose(maps); | |
| } | |
| #elif defined(__FreeBSD__) || defined(__DragonFly__) | |
| #include <sys/user.h> // struct kinfo_vmentry | |
| #include <libutil.h> // kinfo_getvmmap | |
| #include <unistd.h> // getpid | |
| #include <stdlib.h> // free | |
| // FreeBSD (and DragonFly, which forked from FreeBSD) do not reliably provide Linux's | |
| // `/proc/self/maps` (procfs is optional and rarely mounted by default); use the native | |
| // `kinfo_getvmmap` API instead (declared in <libutil.h>, requires linking `-lutil`) to | |
| // enumerate the process's virtual memory mappings. | |
| static void mi_write_mapped_libraries_bsd(FILE* f) { | |
| int cnt = 0; | |
| struct kinfo_vmentry* vmmap = kinfo_getvmmap(getpid(), &cnt); | |
| if (vmmap == NULL) return; | |
| for (int i = 0; i < cnt; i++) { | |
| const struct kinfo_vmentry* kve = &vmmap[i]; | |
| if (kve->kve_path[0] == 0) continue; // skip anonymous mappings | |
| mi_fprintf(f, "%l8x-%l8x r-xp 00000000 00:00 0 %s\n", (uint64_t)kve->kve_start, (uint64_t)kve->kve_end, kve->kve_path); | |
| } | |
| free(vmmap); | |
| } | |
| static void mi_pprof_collect_modules_bsd(mi_heap_t* heap, mi_pprof_modules_t* mods) { | |
| int cnt = 0; | |
| struct kinfo_vmentry* vmmap = kinfo_getvmmap(getpid(), &cnt); | |
| if (vmmap == NULL) return; | |
| for (int i = 0; i < cnt; i++) { | |
| const struct kinfo_vmentry* kve = &vmmap[i]; | |
| if (kve->kve_path[0] == 0) continue; // skip anonymous mappings | |
| mi_pprof_modules_add(heap, mods, (uintptr_t)kve->kve_start, (uintptr_t)kve->kve_end, kve->kve_path); | |
| } | |
| free(vmmap); | |
| } | |
| #endif | |
| static void mi_pprof_collect_modules(mi_heap_t* heap, mi_pprof_modules_t* mods) { | |
| mods->modules = NULL; | |
| mods->count = 0; | |
| mods->capacity = 0; | |
| mods->failed = false; | |
| #if defined(__linux__) | |
| mi_pprof_collect_modules_linux(heap, mods); | |
| #elif defined(_WIN32) | |
| mi_pprof_collect_modules_win32(heap, mods); | |
| #elif defined(__APPLE__) | |
| mi_pprof_collect_modules_macos(heap, mods); | |
| #elif defined(__FreeBSD__) || defined(__DragonFly__) | |
| mi_pprof_collect_modules_bsd(heap, mods); | |
| #else | |
| MI_UNUSED(heap); | |
| #endif | |
| } | |
| static void mi_pprof_write_mapped_libraries(FILE* f) { | |
| fprintf(f, "MAPPED_LIBRARIES:\n"); | |
| #if defined(__linux__) | |
| mi_write_mapped_libraries_linux(f); | |
| #elif defined(_WIN32) | |
| mi_write_mapped_libraries_win32(f); | |
| #elif defined(__APPLE__) | |
| mi_write_mapped_libraries_macos(f); | |
| #elif defined(__FreeBSD__) || defined(__DragonFly__) | |
| mi_write_mapped_libraries_bsd(f); | |
| #endif | |
| } | |
| // --------------------------------------------------------------------------- | |
| // Writing the profile in the (original, textual) pprof heap profile format | |
| // --------------------------------------------------------------------------- | |
| // Write out the current profiler data in the pprof heap profile text format: | |
| // a header line with the totals, followed by one line per unique call | |
| // location, and a trailer with the mapped libraries (used by `pprof` to | |
| // symbolize the addresses). | |
| static bool mi_pprof_snapshot_text(mi_pprof_profiler_t* prof, const char* fname) { | |
| mi_locations_t* locations = &prof->locations; | |
| FILE* f = mi_pprof_fopen(fname, false); | |
| if (f == NULL) return false; | |
| // first pass: compute the totals over all locations | |
| // (snapshot is assumed to run single-threaded while sampling is stopped, so | |
| // `alloc_count`/`alloc_bytes` can be read directly; `inuse_count`/`inuse_bytes` | |
| // are still `_Atomic` fields so we use a relaxed load for those) | |
| uint64_t total_inuse_objects = 0; | |
| uint64_t total_inuse_bytes = 0; | |
| uint64_t total_alloc_objects = 0; | |
| uint64_t total_alloc_bytes = 0; | |
| for (size_t i = 0; i < locations->bucket_count; i++) { | |
| for (mi_location_t* loc = mi_atomic_load_ptr_relaxed(mi_location_t, &locations->buckets[i]); loc != NULL; loc = mi_atomic_load_ptr_relaxed(mi_location_t, &loc->next)) { | |
| total_inuse_objects += (uint64_t)mi_atomic_load_relaxed(&loc->inuse_count); | |
| total_inuse_bytes += (uint64_t)mi_atomic_load_relaxed(&loc->inuse_bytes); | |
| total_alloc_objects += (uint64_t)loc->alloc_count; | |
| total_alloc_bytes += (uint64_t)loc->alloc_bytes; | |
| } | |
| } | |
| mi_fprintf(f, "heap profile: %6l8d: %8l8d [%6l8d: %8l8d] @ heapprofile\n", | |
| total_inuse_objects, total_inuse_bytes, total_alloc_objects, total_alloc_bytes); | |
| // second pass: write one line per unique call location | |
| for (size_t i = 0; i < locations->bucket_count; i++) { | |
| for (mi_location_t* loc = mi_atomic_load_ptr_relaxed(mi_location_t, &locations->buckets[i]); loc != NULL; loc = mi_atomic_load_ptr_relaxed(mi_location_t, &loc->next)) { | |
| const size_t inuse_count = mi_atomic_load_relaxed(&loc->inuse_count); | |
| const size_t inuse_bytes = mi_atomic_load_relaxed(&loc->inuse_bytes); | |
| mi_fprintf(f, "%6l8d: %8l8d [%6l8d: %8l8d] @", | |
| (uint64_t)inuse_count, (uint64_t)inuse_bytes, | |
| (uint64_t)loc->alloc_count, (uint64_t)loc->alloc_bytes); | |
| for (size_t j = 0; j < loc->callstack.count; j++) { | |
| mi_fprintf(f, " 0x%tx", (size_t)(uintptr_t)loc->callstack.frames[j]); | |
| } | |
| fprintf(f, "\n"); | |
| } | |
| } | |
| mi_pprof_write_mapped_libraries(f); | |
| const bool ok = (ferror(f) == 0); | |
| if (fclose(f) != 0) return false; | |
| return ok; | |
| } | |
| // --------------------------------------------------------------------------- | |
| // Writing the profile in the modern `pprof` protobuf format | |
| // (`perftools.profiles.Profile`, see | |
| // https://github.com/google/pprof/blob/main/proto/profile.proto) | |
| // | |
| // On-disk, `pprof` profiles are normally gzip-compressed, but `pprof` also | |
| // accepts a *raw* (uncompressed) serialized `Profile` message: it only tries | |
| // to gunzip the input if it starts with the gzip magic bytes, and otherwise | |
| // parses it directly as a protobuf message. We take advantage of that so we | |
| // don't need to implement a deflate/gzip writer. | |
| // | |
| // We still need the locations hash table for this format: samples are | |
| // aggregated per call site (thread + callstack), exactly as for the text | |
| // format, rather than emitting one `Sample` per individual allocation. What | |
| // changes is the *representation* of a callstack: instead of a flat list of | |
| // hex addresses embedded directly in a text line, each frame address becomes | |
| // its own (deduplicated) `Location` entry, referenced by id from `Sample. | |
| // location_id`. The originating thread id -- which the text format doesn't | |
| // record explicitly (it is only implicit in which addresses got sampled) -- | |
| // is attached to each `Sample` via a numeric `Label` (pprof has no dedicated | |
| // thread-id field, but labels are the generic mechanism for this). | |
| // --------------------------------------------------------------------------- | |
| // A simple growable byte buffer, allocated from the profiler's own heap. | |
| // If an allocation fails, `failed` is set and all further writes become no-ops; | |
| // the caller must check it and discard the whole snapshot. | |
| typedef struct { | |
| mi_heap_t* heap; | |
| uint8_t* data; | |
| size_t size; | |
| size_t capacity; | |
| bool failed; | |
| } mi_pb_buf_t; | |
| static void mi_pb_init(mi_pb_buf_t* b, mi_heap_t* heap) { | |
| b->heap = heap; | |
| b->data = NULL; | |
| b->size = 0; | |
| b->capacity = 0; | |
| b->failed = false; | |
| } | |
| static void mi_pb_done(mi_pb_buf_t* b) { | |
| mi_free(b->data); | |
| b->data = NULL; | |
| b->size = 0; | |
| b->capacity = 0; | |
| b->failed = false; | |
| } | |
| static bool mi_pb_reserve(mi_pb_buf_t* b, size_t extra) { | |
| if (b->failed) return false; | |
| if (b->size + extra <= b->capacity) return true; | |
| size_t new_capacity = (b->capacity == 0 ? 4096 : b->capacity * 2); | |
| while (new_capacity < b->size + extra) { new_capacity *= 2; } | |
| uint8_t* const new_data = (uint8_t*)mi_heap_realloc(b->heap, b->data, new_capacity); | |
| if (new_data == NULL) { b->failed = true; return false; } // keep the old buffer (and capacity) intact | |
| b->data = new_data; | |
| b->capacity = new_capacity; | |
| return true; | |
| } | |
| static void mi_pb_bytes(mi_pb_buf_t* b, const void* p, size_t n) { | |
| if (n == 0) return; // avoid calling memcpy with a NULL source | |
| if (!mi_pb_reserve(b, n)) return; | |
| memcpy(b->data + b->size, p, n); | |
| b->size += n; | |
| } | |
| static void mi_pb_byte(mi_pb_buf_t* b, uint8_t byte) { | |
| mi_pb_bytes(b, &byte, 1); | |
| } | |
| // Unsigned LEB128 (protobuf "varint") encoding. | |
| static void mi_pb_varint(mi_pb_buf_t* b, uint64_t v) { | |
| do { | |
| uint8_t byte = (uint8_t)(v & 0x7F); | |
| v >>= 7; | |
| if (v != 0) { byte |= 0x80; } | |
| mi_pb_byte(b, byte); | |
| } while (v != 0); | |
| } | |
| static void mi_pb_tag(mi_pb_buf_t* b, uint32_t field, uint32_t wiretype) { | |
| mi_pb_varint(b, ((uint64_t)field << 3) | wiretype); | |
| } | |
| // Scalar varint fields; proto3 encoders may omit fields set to their default | |
| // (0/false) value, which we do here to keep the output compact. | |
| static void mi_pb_uint64_field(mi_pb_buf_t* b, uint32_t field, uint64_t v) { | |
| if (v == 0) return; | |
| mi_pb_tag(b, field, 0); | |
| mi_pb_varint(b, v); | |
| } | |
| static void mi_pb_int64_field(mi_pb_buf_t* b, uint32_t field, int64_t v) { | |
| if (v == 0) return; | |
| mi_pb_tag(b, field, 0); | |
| mi_pb_varint(b, (uint64_t)v); // proto3 plain int64 is encoded as a varint of its 2's complement bit pattern | |
| } | |
| // Length-delimited fields (string/bytes/embedded message) are always emitted, | |
| // even when empty: this matters for `string_table[0]` (which must be the | |
| // empty string) and for repeated message fields (an empty message is still a | |
| // valid, distinct list entry). | |
| static void mi_pb_len_field(mi_pb_buf_t* b, uint32_t field, const void* data, size_t len) { | |
| mi_pb_tag(b, field, 2); | |
| mi_pb_varint(b, len); | |
| mi_pb_bytes(b, data, len); | |
| } | |
| static void mi_pb_message_field(mi_pb_buf_t* b, uint32_t field, const mi_pb_buf_t* msg) { | |
| if (msg->failed) { b->failed = true; return; } // propagate a failure from the nested message | |
| mi_pb_len_field(b, field, msg->data, msg->size); | |
| } | |
| // A packed repeated scalar field (used for `Sample.location_id`/`Sample.value`): | |
| // all elements are varint-encoded and concatenated into one length-delimited blob. | |
| static void mi_pb_packed_uint64_field(mi_pb_buf_t* b, uint32_t field, const uint64_t* values, size_t count) { | |
| if (count == 0) return; | |
| mi_pb_buf_t tmp; | |
| mi_pb_init(&tmp, b->heap); | |
| for (size_t i = 0; i < count; i++) { mi_pb_varint(&tmp, values[i]); } | |
| mi_pb_message_field(b, field, &tmp); | |
| mi_pb_done(&tmp); | |
| } | |
| static void mi_pb_packed_int64_field(mi_pb_buf_t* b, uint32_t field, const int64_t* values, size_t count) { | |
| mi_pb_packed_uint64_field(b, field, (const uint64_t*)values, count); // same varint bit pattern | |
| } | |
| // --------------------------------------------------------------------------- | |
| // String table: dedups strings into `Profile.string_table` (field 6) and | |
| // hands back the (1-based; index 0 is reserved for "") index of each string. | |
| // A linear search is fine here: a profile typically only has a handful to a | |
| // few hundred distinct strings (sample type names, mapped file paths). | |
| // --------------------------------------------------------------------------- | |
| typedef struct { | |
| char* str; | |
| int64_t index; | |
| } mi_pb_string_entry_t; | |
| typedef struct { | |
| mi_heap_t* heap; | |
| mi_pb_string_entry_t* entries; | |
| size_t count; | |
| size_t capacity; | |
| bool failed; // set if an allocation failed; the table is then incomplete | |
| mi_pb_buf_t table; // the serialized `string_table` entries (Profile field 6) | |
| } mi_pb_strings_t; | |
| static void mi_pb_strings_init(mi_pb_strings_t* strings, mi_heap_t* heap) { | |
| strings->heap = heap; | |
| strings->entries = NULL; | |
| strings->count = 0; | |
| strings->capacity = 0; | |
| strings->failed = false; | |
| mi_pb_init(&strings->table, heap); | |
| mi_pb_len_field(&strings->table, 6, "", 0); // index 0 must always be "" | |
| } | |
| static void mi_pb_strings_done(mi_pb_strings_t* strings) { | |
| for (size_t i = 0; i < strings->count; i++) { mi_free(strings->entries[i].str); } | |
| mi_free(strings->entries); | |
| mi_pb_done(&strings->table); | |
| } | |
| static int64_t mi_pb_strings_intern(mi_pb_strings_t* strings, const char* s) { | |
| if (strings->failed) return 0; | |
| if (s == NULL) { s = ""; } | |
| for (size_t i = 0; i < strings->count; i++) { | |
| if (_mi_streq(strings->entries[i].str, s)) { return strings->entries[i].index; } | |
| } | |
| if (strings->count == strings->capacity) { | |
| const size_t new_capacity = (strings->capacity == 0 ? 32 : strings->capacity * 2); | |
| mi_pb_string_entry_t* const new_entries = (mi_pb_string_entry_t*)mi_heap_realloc(strings->heap, strings->entries, new_capacity * sizeof(*strings->entries)); | |
| if (new_entries == NULL) { strings->failed = true; return 0; } | |
| strings->entries = new_entries; | |
| strings->capacity = new_capacity; | |
| } | |
| char* const copy = mi_heap_strndup(strings->heap, s, 1024); | |
| if (copy == NULL) { strings->failed = true; return 0; } | |
| const int64_t index = (int64_t)(strings->count + 1); // index 0 is reserved for "" | |
| strings->entries[strings->count].str = copy; | |
| strings->entries[strings->count].index = index; | |
| strings->count++; | |
| mi_pb_len_field(&strings->table, 6, s, strlen(s)); | |
| if (strings->table.failed) { strings->failed = true; return 0; } | |
| return index; | |
| } | |
| // --------------------------------------------------------------------------- | |
| // A small single-threaded hash map from a raw frame address to the id of its | |
| // (deduplicated) `Location` entry. Only ever built and consulted from within | |
| // `mi_pprof_snapshot_proto`, so (unlike the locations hash table) it needs no | |
| // atomics at all. | |
| // --------------------------------------------------------------------------- | |
| typedef struct mi_pb_addr_entry_s { | |
| struct mi_pb_addr_entry_s* next; | |
| uintptr_t addr; | |
| uint64_t id; | |
| } mi_pb_addr_entry_t; | |
| typedef struct { | |
| mi_heap_t* heap; | |
| size_t bucket_count; | |
| mi_pb_addr_entry_t** buckets; | |
| bool failed; // set if an allocation failed (the map is then unusable) | |
| } mi_pb_addr_map_t; | |
| static void mi_pb_addr_map_init(mi_pb_addr_map_t* map, mi_heap_t* heap, size_t bucket_count) { | |
| map->heap = heap; | |
| map->bucket_count = (bucket_count == 0 ? 1 : bucket_count); | |
| map->buckets = (mi_pb_addr_entry_t**)mi_heap_zalloc(heap, map->bucket_count * sizeof(*map->buckets)); | |
| map->failed = (map->buckets == NULL); | |
| } | |
| static void mi_pb_addr_map_done(mi_pb_addr_map_t* map) { | |
| if (map->buckets == NULL) return; | |
| for (size_t i = 0; i < map->bucket_count; i++) { | |
| mi_pb_addr_entry_t* e = map->buckets[i]; | |
| while (e != NULL) { | |
| mi_pb_addr_entry_t* const next = e->next; | |
| mi_free(e); | |
| e = next; | |
| } | |
| } | |
| mi_free(map->buckets); | |
| map->buckets = NULL; | |
| } | |
| // Returns the existing location id for `addr`, or assigns it `next_id` and | |
| // returns that (setting `*is_new` to true) if this is the first time `addr` | |
| // is seen. | |
| static uint64_t mi_pb_addr_map_find_or_insert(mi_pb_addr_map_t* map, uintptr_t addr, uint64_t next_id, bool* is_new) { | |
| if (map->failed || map->buckets == NULL) { map->failed = true; *is_new = false; return 0; } | |
| const size_t idx = (size_t)(addr ^ (addr >> 16)) % map->bucket_count; | |
| for (mi_pb_addr_entry_t* e = map->buckets[idx]; e != NULL; e = e->next) { | |
| if (e->addr == addr) { *is_new = false; return e->id; } | |
| } | |
| mi_pb_addr_entry_t* e = mi_heap_malloc_tp(mi_pb_addr_entry_t, map->heap); | |
| if (e == NULL) { map->failed = true; *is_new = false; return 0; } | |
| e->addr = addr; | |
| e->id = next_id; | |
| e->next = map->buckets[idx]; | |
| map->buckets[idx] = e; | |
| *is_new = true; | |
| return next_id; | |
| } | |
| // Find the (1-based) `Mapping` id of the module containing `addr`, or 0 if | |
| // `addr` doesn't fall within any collected module range. | |
| static uint64_t mi_pprof_find_mapping_id(const mi_pprof_modules_t* mods, uintptr_t addr) { | |
| for (size_t i = 0; i < mods->count; i++) { | |
| if (addr >= mods->modules[i].start && addr < mods->modules[i].end) { return (uint64_t)(i + 1); } | |
| } | |
| return 0; | |
| } | |
| // Build the current profiler data as a raw (uncompressed) `perftools.profiles.Profile` | |
| // protobuf message in `profile` (see the file-level comment above). | |
| // Returns `false` if any allocation failed; the caller releases all buffers in either case. | |
| static bool mi_pprof_build_proto(mi_pprof_profiler_t* prof, mi_pb_buf_t* profile, mi_pb_strings_t* strings, | |
| mi_pprof_modules_t* mods, mi_pb_addr_map_t* addr_map) | |
| { | |
| mi_heap_t* heap = prof->profile_heap; | |
| mi_locations_t* locations = &prof->locations; | |
| const int64_t str_count = mi_pb_strings_intern(strings, "count"); | |
| const int64_t str_bytes = mi_pb_strings_intern(strings, "bytes"); | |
| const int64_t str_alloc_objects = mi_pb_strings_intern(strings, "alloc_objects"); | |
| const int64_t str_alloc_bytes = mi_pb_strings_intern(strings, "alloc_bytes"); | |
| const int64_t str_inuse_objects = mi_pb_strings_intern(strings, "inuse_objects"); | |
| const int64_t str_inuse_bytes = mi_pb_strings_intern(strings, "inuse_bytes"); | |
| const int64_t str_space = mi_pb_strings_intern(strings, "space"); | |
| const int64_t str_thread = mi_pb_strings_intern(strings, "thread"); | |
| if (strings->failed) return false; | |
| // Profile.sample_type = 1: [alloc_objects:count, alloc_bytes:bytes, inuse_objects:count, inuse_bytes:bytes] | |
| { | |
| const int64_t types[4] = { str_alloc_objects, str_alloc_bytes, str_inuse_objects, str_inuse_bytes }; | |
| const int64_t units[4] = { str_count, str_bytes, str_count, str_bytes }; | |
| for (size_t i = 0; i < 4; i++) { | |
| mi_pb_buf_t vt; | |
| mi_pb_init(&vt, heap); | |
| mi_pb_int64_field(&vt, 1, types[i]); | |
| mi_pb_int64_field(&vt, 2, units[i]); | |
| mi_pb_message_field(profile, 1, &vt); | |
| mi_pb_done(&vt); | |
| } | |
| } | |
| // Profile.mapping = 3: the modules loaded into this process, so `pprof` can | |
| // symbolize the (raw) addresses recorded in each `Location` below. | |
| mi_pprof_collect_modules(heap, mods); | |
| if (mods->failed) return false; | |
| for (size_t i = 0; i < mods->count; i++) { | |
| const mi_pprof_module_t* m = &mods->modules[i]; | |
| mi_pb_buf_t mapping; | |
| mi_pb_init(&mapping, heap); | |
| mi_pb_uint64_field(&mapping, 1, (uint64_t)(i + 1)); // id | |
| mi_pb_uint64_field(&mapping, 2, (uint64_t)m->start); // memory_start | |
| mi_pb_uint64_field(&mapping, 3, (uint64_t)m->end); // memory_limit | |
| if (m->path != NULL) { | |
| mi_pb_int64_field(&mapping, 5, mi_pb_strings_intern(strings, m->path)); // filename | |
| } | |
| mi_pb_message_field(profile, 3, &mapping); | |
| mi_pb_done(&mapping); | |
| if (profile->failed || strings->failed) return false; | |
| } | |
| // Profile.location = 4 and Profile.sample = 2: one (deduplicated) `Location` | |
| // per unique frame address, and one `Sample` per unique call site (i.e. one | |
| // per entry in our own locations hash table), exactly mirroring the text | |
| // format's aggregation. | |
| mi_pb_addr_map_init(addr_map, heap, locations->bucket_count); | |
| if (addr_map->failed) return false; | |
| uint64_t next_location_id = 1; | |
| for (size_t i = 0; i < locations->bucket_count; i++) { | |
| for (mi_location_t* loc = mi_atomic_load_ptr_relaxed(mi_location_t, &locations->buckets[i]); loc != NULL; loc = mi_atomic_load_ptr_relaxed(mi_location_t, &loc->next)) { | |
| const size_t inuse_count = mi_atomic_load_relaxed(&loc->inuse_count); | |
| const size_t inuse_bytes = mi_atomic_load_relaxed(&loc->inuse_bytes); | |
| const size_t frame_count = (loc->callstack.count > MI_MAX_BACKTRACE_DEPTH ? MI_MAX_BACKTRACE_DEPTH : loc->callstack.count); | |
| uint64_t location_ids[MI_MAX_BACKTRACE_DEPTH]; | |
| for (size_t j = 0; j < frame_count; j++) { | |
| const uintptr_t addr = (uintptr_t)loc->callstack.frames[j]; | |
| bool is_new = false; | |
| const uint64_t id = mi_pb_addr_map_find_or_insert(addr_map, addr, next_location_id, &is_new); | |
| if (addr_map->failed) return false; | |
| location_ids[j] = id; | |
| if (is_new) { | |
| next_location_id++; | |
| mi_pb_buf_t location; | |
| mi_pb_init(&location, heap); | |
| mi_pb_uint64_field(&location, 1, id); // id | |
| mi_pb_uint64_field(&location, 2, mi_pprof_find_mapping_id(mods, addr)); // mapping_id | |
| mi_pb_uint64_field(&location, 3, (uint64_t)addr); // address | |
| mi_pb_message_field(profile, 4, &location); | |
| mi_pb_done(&location); | |
| } | |
| } | |
| mi_pb_buf_t sample; | |
| mi_pb_init(&sample, heap); | |
| mi_pb_packed_uint64_field(&sample, 1, location_ids, frame_count); // location_id (leaf-first, matching our capture order) | |
| const int64_t values[4] = { | |
| loc->alloc_count, loc->alloc_bytes, | |
| (int64_t)inuse_count, (int64_t)inuse_bytes | |
| }; | |
| mi_pb_packed_int64_field(&sample, 2, values, 4); // value | |
| { | |
| // attach the originating thread id as a numeric label: pprof has no | |
| // dedicated thread-id field, but `Sample.label` is the generic | |
| // mechanism for this kind of per-sample metadata. | |
| mi_pb_buf_t label; | |
| mi_pb_init(&label, heap); | |
| mi_pb_int64_field(&label, 1, str_thread); // key | |
| mi_pb_int64_field(&label, 3, (int64_t)loc->thread_id); // num | |
| mi_pb_message_field(&sample, 3, &label); | |
| mi_pb_done(&label); | |
| } | |
| mi_pb_message_field(profile, 2, &sample); | |
| mi_pb_done(&sample); | |
| if (profile->failed) return false; | |
| } | |
| } | |
| // Profile.period_type = 11 / Profile.period = 12: matches the convention | |
| // used by Go's heap profiles ("space"/"bytes", sampling period in bytes). | |
| { | |
| mi_pb_buf_t period_type; | |
| mi_pb_init(&period_type, heap); | |
| mi_pb_int64_field(&period_type, 1, str_space); | |
| mi_pb_int64_field(&period_type, 2, str_bytes); | |
| mi_pb_message_field(profile, 11, &period_type); | |
| mi_pb_done(&period_type); | |
| } | |
| mi_pb_int64_field(profile, 12, (int64_t)prof->sample_threshold); // period | |
| const int64_t elapsed_nanos = (int64_t)(_mi_clock_now() - prof->start_clock) * 1000000LL; // clock is in milli-secs | |
| mi_pb_int64_field(profile, 9, prof->start_time_secs * 1000000000LL + elapsed_nanos); // time_nanos | |
| mi_pb_int64_field(profile, 10, elapsed_nanos); // duration_nanos | |
| // the string table (Profile.string_table = 6) can only be finalized now, | |
| // since strings were interned into it while building the message above. | |
| if (strings->failed) return false; | |
| mi_pb_bytes(profile, strings->table.data, strings->table.size); | |
| if (profile->failed) return false; | |
| return true; | |
| } | |
| static bool mi_pprof_write_file(const char* fname, const mi_pb_buf_t* profile) { | |
| FILE* f = mi_pprof_fopen(fname, true); | |
| if (f == NULL) return false; | |
| bool ok = (fwrite(profile->data, 1, profile->size, f) == profile->size); | |
| if (fclose(f) != 0) { ok = false; } | |
| return ok; | |
| } | |
| // Write out the current profiler data as a protobuf `Profile` message. | |
| // Returns `false` if any allocation failed (in which case no file is written): | |
| // the snapshot is best effort and must never crash the profiled program. | |
| static bool mi_pprof_snapshot_proto(mi_pprof_profiler_t* prof, const char* fname) { | |
| mi_heap_t* heap = prof->profile_heap; | |
| mi_pb_buf_t profile; | |
| mi_pb_init(&profile, heap); | |
| mi_pb_strings_t strings; | |
| mi_pb_strings_init(&strings, heap); | |
| mi_pprof_modules_t mods; | |
| mods.modules = NULL; | |
| mods.count = 0; | |
| mods.capacity = 0; | |
| mods.failed = false; | |
| mi_pb_addr_map_t addr_map; | |
| addr_map.heap = heap; | |
| addr_map.bucket_count = 0; | |
| addr_map.buckets = NULL; | |
| addr_map.failed = false; | |
| bool ok = mi_pprof_build_proto(prof, &profile, &strings, &mods, &addr_map); | |
| mi_pb_addr_map_done(&addr_map); | |
| mi_pprof_modules_done(&mods); | |
| mi_pb_strings_done(&strings); | |
| if (ok) { ok = mi_pprof_write_file(fname, &profile); } | |
| mi_pb_done(&profile); | |
| return ok; | |
| } | |
| #endif // MI_PROFILE |