blob: a569e3db4168d4965d281121783174b7a02d3236 [file] [edit]
/* ----------------------------------------------------------------------------
Copyright (c) 2019-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.
-----------------------------------------------------------------------------*/
#include "mimalloc.h"
#include "mimalloc/prim/prim-tls.h"
#include "mimalloc/util/stats.h"
#include "mimalloc/alloc/page.h"
#include "mimalloc/profile/sample-guarded.h"
#include "mimalloc/profile/sample-profile.h"
//----------------------------------------------------------------------------
// General sampled allocation (called from `alloc/alloc-generic.c:mi_malloc_generic_fallback`)
// This is for both guarded and profiled sampling
//----------------------------------------------------------------------------
size_t _mi_theap_update_sample_rate(mi_theap_t* theap) {
const size_t old_sample_rate = theap->sample_rate;
theap->sample_rate = theap->profile_sample_rate;
if (theap->sample_rate == 0 || (theap->guarded_sample_rate!=0 && theap->sample_rate > theap->guarded_sample_rate)) {
theap->sample_rate = theap->guarded_sample_rate;
}
if (theap->sample_countdown > theap->sample_rate) {
theap->sample_countdown = theap->sample_rate; // todo: adjust difference?
}
return old_sample_rate;
}
// Restart the sample countdown at the nearest profile or guarded countdown.
// These bytes are counted down from both in advance, so the next sample only counts the bytes beyond it.
static void mi_theap_sample_countdown_restart(mi_theap_t* theap) {
size_t countdown = theap->sample_rate;
if (theap->profile_sample_rate!=0 && theap->profile_sample_countdown < countdown) { countdown = theap->profile_sample_countdown; }
if (theap->guarded_sample_rate!=0 && theap->guarded_sample_countdown < countdown) { countdown = theap->guarded_sample_countdown; }
if (theap->profile_sample_rate!=0) { theap->profile_sample_countdown -= countdown; }
if (theap->guarded_sample_rate!=0) { theap->guarded_sample_countdown -= countdown; }
theap->sample_countdown = countdown;
}
// The bytes beyond a derived countdown that were counted in a batch before this allocation (and count towards the next sample).
// This is at most the `rate` so any bytes beyond that are attributed to the current sample (instead of being lost).
static uint64_t mi_sample_carry(uint64_t excess, size_t req_size, size_t rate) {
const uint64_t carry = (excess > req_size ? excess - req_size : 0);
return (carry < rate ? carry : rate);
}
static size_t mi_sample_countdown_next(size_t rate, uint64_t carry) {
return (carry < rate ? rate - (size_t)carry : 0);
}
// Called when the theap sample countdown <= req_size; should call guarded or profiled allocation.
static void* mi_theap_malloc_sampled_ex(mi_theap_t* theap, size_t req_size, bool zero, mi_page_t** ppage)
{
// the size has not yet been counted against the countdown (and does not include MI_PADDING_SIZE)
mi_assert_internal(req_size <= MI_MAX_ALLOC_SIZE);
mi_assert_internal((theap->sample_countdown==SIZE_MAX && _mi_is_empty_theap(theap)) ||
(theap->sample_countdown <= MI_SAMPLE_COUNTDOWN_MAX && theap->sample_countdown < req_size));
const size_t size = req_size + MI_PADDING_SIZE;
// handle empty theap and disabled sampling
if (theap->sample_rate==0) {
if (!_mi_is_empty_theap(theap)) { // avoid writing to the initial empty theap
theap->sample_countdown = MI_SAMPLE_COUNTDOWN_MAX; // avoid the sampling path for a long time
}
return _mi_malloc_generic_no_sample(theap,size,zero,ppage);
}
// never sample allocations made by a profiler `on_alloc` callback (e.g. by `backtrace`) to avoid recursion
if mi_unlikely(theap->tld->in_profiler) {
return _mi_malloc_generic_no_sample(theap,size,zero,ppage);
}
mi_assert_internal(!_mi_is_empty_theap(theap));
mi_assert_internal(req_size <= SIZE_MAX/2);
mi_assert_internal(theap->sample_rate > 0);
mi_assert_internal(theap->sample_rate <= SIZE_MAX/2);
mi_assert_internal(theap->sample_rate >= theap->sample_countdown);
mi_assert_internal(theap->profile_sample_rate!=0 || theap->profile_sample_countdown==0);
mi_assert_internal(theap->guarded_sample_rate!=0 || theap->guarded_sample_countdown==0);
// the requested bytes beyond the countdown (which was already counted down from the derived countdowns at restart)
const uint64_t requested = (uint64_t)(req_size - theap->sample_countdown) + theap->sample_requested;
theap->sample_countdown = 0;
theap->sample_requested = 0;
// update the derived countdowns; a sample is due if the requested bytes exceed its countdown
uint64_t profile_excess = 0;
uint64_t guarded_excess = 0;
uint64_t profile_requested = 0;
if (theap->profile_sample_rate!=0) {
profile_requested = (theap->profile_sample_rate - theap->profile_sample_countdown) + requested; // since the last profile sample
if (requested <= theap->profile_sample_countdown) { theap->profile_sample_countdown -= (size_t)requested; } else { profile_excess = requested - theap->profile_sample_countdown; }
}
if (theap->guarded_sample_rate!=0) {
if (requested <= theap->guarded_sample_countdown) { theap->guarded_sample_countdown -= (size_t)requested; } else { guarded_excess = requested - theap->guarded_sample_countdown; }
}
void* p;
if (profile_excess > 0) {
const uint64_t carry = mi_sample_carry(profile_excess, req_size, theap->profile_sample_rate);
p = _mi_theap_malloc_profiled(theap,size,profile_requested - carry,zero,ppage); // can update the profile sample rate
theap->profile_sample_countdown = mi_sample_countdown_next(theap->profile_sample_rate, carry);
if (guarded_excess > 0) { theap->guarded_sample_countdown = 0; } // retry at the next allocation
}
else if (guarded_excess > 0) {
if (req_size >= theap->guarded_size_min && req_size <= theap->guarded_size_max) {
p = _mi_theap_malloc_guarded(theap,size,zero,ppage);
theap->guarded_sample_countdown = mi_sample_countdown_next(theap->guarded_sample_rate, mi_sample_carry(guarded_excess, req_size, theap->guarded_sample_rate));
}
else {
p = _mi_malloc_generic_no_sample(theap,size,zero,ppage);
theap->guarded_sample_countdown = 0; // retry at the next allocation
}
}
else {
p = _mi_malloc_generic_no_sample(theap,size,zero,ppage);
}
mi_theap_sample_countdown_restart(theap);
return p;
}
mi_decl_noinline mi_decl_restrict void* _mi_theap_malloc_sampled(mi_theap_t* theap, size_t req_size, bool zero, mi_page_t** ppage) mi_attr_noexcept
{
void* const p = mi_theap_malloc_sampled_ex(theap, req_size, zero, ppage);
if mi_unlikely(p==NULL) { errno = ENOMEM; } // as in `alloc/alloc-generic.c:mi_malloc_generic_fallback`
return p;
}
//----------------------------------------------------------------------------
// Util
//----------------------------------------------------------------------------
static mi_profiler_t* mi_heap_profiler(const mi_heap_t* heap) {
return mi_atomic_load_ptr_acquire(mi_profiler_t,&heap->profiler);
}
static mi_profiler_t* mi_theap_get_enabled_profiler(const mi_theap_t* theap) {
mi_heap_t* const heap = mi_theap_heap(theap);
mi_profiler_t* prof = mi_atomic_load_ptr_acquire(mi_profiler_t, &heap->profiler);
if (prof!=NULL && mi_profiler_is_enabled(prof) && prof->on_alloc != NULL) {
return prof;
}
else {
return NULL;
}
}
//----------------------------------------------------------------------------
// Profile an allocation
//-----------------------------------------------------------------------------
// The header of a profiled block (followed by the user data of the sample data, and then the user object)
typedef struct mi_profiled_block_s {
mi_block_t block; // `next` is `MI_BLOCK_TAG_PROFILED`
size_t profiler_state; // the profiler state when sampled; `on_free` is only called if it is still the same generation
mi_profiler_sample_data_t sample_data;
} mi_profiled_block_t;
// call `on_alloc`; allocations made by the profiler itself (e.g. by `backtrace`) are not sampled
static size_t mi_profiler_on_alloc(mi_theap_t* theap, mi_profiler_t* prof, mi_profiler_sample_data_t* data, void* p, size_t req_size, uint64_t requested_since_last_sample) {
mi_tld_t* const tld = theap->tld;
tld->in_profiler = true;
const size_t new_sample_rate = (*prof->on_alloc)(prof, data, p, req_size, theap->profile_sample_rate, requested_since_last_sample, mi_theap_heap(theap));
tld->in_profiler = false;
return new_sample_rate;
}
mi_decl_noinline mi_decl_restrict void* _mi_theap_malloc_profiled(mi_theap_t* theap, size_t size, uint64_t requested_since_last_sample, bool zero, mi_page_t** ppage) mi_attr_noexcept
{
mi_assert_internal(theap!=NULL);
mi_assert_internal(size>=MI_PADDING_SIZE);
const size_t req_size = size - MI_PADDING_SIZE;
mi_profiler_t* const prof = mi_theap_get_enabled_profiler(theap);
if (prof == NULL) { return _mi_malloc_generic_no_sample(theap,size,zero,ppage); }
mi_assert_internal(prof!=NULL && prof->on_alloc!=NULL && mi_profiler_is_enabled(prof));
void* p = NULL;
size_t new_sample_rate = 0;
if (prof->on_free==NULL || prof->sample_data_size==0) {
// just allocate without profiler data
p = _mi_malloc_generic_no_sample(theap,size,zero,ppage);
if (p==NULL) { return p; }
if (prof->on_alloc!=NULL) {
// we are just allocation profiling (not heap profiling as on_free == NULL)
new_sample_rate = mi_profiler_on_alloc(theap, prof, NULL /* no data */, p, req_size, requested_since_last_sample);
}
}
else {
// overallocate a larger block to store the profiler data
// [MI_BLOCK_TAG_PROFILED] [profiler state] [ ... profile data ... ] [... user data ...]
const size_t sample_user_data_size = mi_align_up(prof->sample_data_size > MI_PROFILE_SAMPLE_DATA_MAX_SIZE ? MI_PROFILE_SAMPLE_DATA_MAX_SIZE : prof->sample_data_size, sizeof(void*));
const size_t header_size = sizeof(mi_profiled_block_t) + sample_user_data_size; // one void* too many just in case
const size_t user_offset = mi_align_up(header_size, MI_MAX_ALIGN_SIZE);
const size_t oversize = user_offset + size;
mi_page_t* page = NULL;
mi_profiled_block_t* const pblock = (mi_profiled_block_t*)_mi_malloc_generic_no_sample(theap,oversize,zero,&page);
if (pblock==NULL) return NULL;
mi_block_t* const block = &pblock->block;
mi_assert_internal(page!=NULL);
if (ppage!=NULL) { *ppage = page; }
#if MI_PAGE_META_SMALL_IS_ALIGNED
// we should never allocate something allocated as small in a non-small page or otherwise aligned mi_free_small may fail.
// (that is why we need to limit the profiler_data_size as well)
// we should never allocate something allocated as small in a non-small page or otherwise aligned mi_free_small may fail.
if (size <= MI_SMALL_SIZE_MAX) { mi_assert_internal(mi_page_block_size(page) <= MI_SMALL_MAX_OBJ_SIZE); }
#endif
// Set up the profiled block as an interior pointer so the interior "slow" path is taken on mi_free (where we catch it to call on_free)
mi_page_set_has_interior_pointers(page, true);
block->next = MI_BLOCK_TAG_PROFILED;
p = mi_ptr_bounded((uint8_t*)block + user_offset, mi_bounds_user_size(req_size)); // exclude the profile data in front
pblock->profiler_state = mi_profiler_state(prof);
mi_profiler_sample_data_t* const sample_data = &pblock->sample_data;
sample_data->user_data_size = sample_user_data_size;
// and call the profiler on_alloc
if (prof->on_alloc!=NULL) {
new_sample_rate = mi_profiler_on_alloc(theap, prof, sample_data, p, req_size, requested_since_last_sample);
}
}
if (new_sample_rate!=0 && new_sample_rate != (size_t)theap->profile_sample_rate) {
_mi_theap_set_profile_sample_rate(theap,new_sample_rate);
}
mi_theap_stat_counter_increase(theap,profile_samples,1);
return p;
}
void _mi_page_profile_on_free(mi_page_t* page, mi_block_t* block, void* p) {
mi_assert_internal(mi_block_ptr_is_sampled(block,p));
// get the heap and profiler
mi_heap_t* const heap = mi_page_heap(page);
if (heap==NULL) return;
mi_profiler_t* prof = mi_heap_profiler(heap);
if (prof==NULL || prof->on_free==NULL) return;
// only call `on_free` if the block was sampled by this profiler since it was last attached (even if it is stopped now)
// (as the sample data may otherwise refer to deallocated data from a previous profiler)
mi_profiled_block_t* const pblock = (mi_profiled_block_t*)block;
if ((pblock->profiler_state >> 1) != (mi_profiler_state(prof) >> 1)) return; // compare generations
(*prof->on_free)(prof, &pblock->sample_data, p, heap);
}
//----------------------------------------------------------------------------
// Profiling API
//-----------------------------------------------------------------------------*
size_t _mi_theap_set_profile_sample_rate(mi_theap_t* theap, size_t sample_rate) {
const size_t old_sample_rate = theap->profile_sample_rate;
theap->profile_sample_rate = (sample_rate > MI_SAMPLE_RATE_MAX ? MI_SAMPLE_RATE_MAX : sample_rate);
if (theap->profile_sample_countdown > theap->profile_sample_rate) {
theap->profile_sample_countdown = theap->profile_sample_rate; // todo: adjust difference?
}
_mi_theap_update_sample_rate(theap);
return old_sample_rate;
}
static bool mi_heap_set_profiler(mi_heap_t* heap, mi_profiler_t* profiler) {
if (!mi_heap_handle_is_valid(heap)) return false;
mi_profiler_t* previous = (profiler==NULL ? mi_atomic_load_ptr_acquire(mi_profiler_t,&heap->profiler) : NULL); // don't overwrite unless it is NULL
return mi_atomic_cas_ptr_strong_release(mi_profiler_t, &heap->profiler, &previous, profiler);
}
// Called when a stopped profiler is attached: the fresh generation ensures `on_free` is
// only called for allocations that are sampled by this profiler after it was attached.
static void mi_profiler_new_generation(mi_profiler_t* profiler) {
if (profiler==NULL) return;
const size_t generation = mi_atomic_increment_relaxed(&_mi_process.profiler_generation) + 1;
mi_atomic_store_release(mi_profiler_pstate(profiler), generation << 1);
}
// Attach (or detach if `profiler==NULL`) a profiler to a heap or subprocess.
// The new generation must be set before the profiler is published, as otherwise a concurrent free
// could call `on_free` for a block sampled during a previous attachment (with the same generation).
static bool mi_profiler_attach(_Atomic(mi_profiler_t*)* slot, mi_profiler_t* profiler) {
mi_profiler_stop(profiler);
mi_profiler_t* expected = NULL;
if (profiler==NULL) {
expected = mi_atomic_load_ptr_acquire(mi_profiler_t,slot);
}
else {
if (mi_atomic_load_ptr_relaxed(mi_profiler_t,slot) != NULL) return false; // don't overwrite unless it is NULL
mi_profiler_new_generation(profiler);
}
return mi_atomic_cas_ptr_strong_release(mi_profiler_t, slot, &expected, profiler);
}
mi_decl_export bool mi_heap_profile(mi_heap_t* heap, mi_profiler_t* profiler) {
// if (heap==NULL) { heap = mi_heap_main(); }
if (!mi_heap_handle_is_valid(heap)) return false;
return mi_profiler_attach(&heap->profiler, profiler);
}
mi_decl_export void mi_heap_profile_disable(mi_heap_t* heap) {
static const mi_profiler_t mi_nosample_profiler = { NULL, 0, MI_SAMPLE_RATE_MAX, NULL, NULL, NULL, NULL };
// if (heap==NULL) { heap = mi_heap_main(); }
mi_heap_set_profiler(heap,NULL); // first clear any previous one
mi_heap_set_profiler(heap,(mi_profiler_t*)&mi_nosample_profiler); // and then install the nosample one
}
mi_decl_export bool mi_subproc_profile(mi_subproc_id_t subproc_id, mi_profiler_t* profiler) {
mi_subproc_t* subproc = _mi_subproc_from_id(subproc_id);
if (subproc==NULL) return false;
if (!mi_profiler_attach(&subproc->profiler, profiler)) { return false; }
mi_lock(&subproc->heaps_lock) {
for (mi_heap_t* heap = subproc->heaps; heap!=NULL; heap = heap->next) {
mi_heap_set_profiler(heap,profiler);
}
}
return true;
}
mi_decl_export bool mi_profile( mi_profiler_t* profiler) {
return mi_subproc_profile(mi_subproc_main(),profiler);
}
bool mi_profiler_start(mi_profiler_t* profiler ) {
if (profiler==NULL) return false;
const bool was_running = ((mi_atomic_or_acq_rel(mi_profiler_pstate(profiler), (size_t)1) & 1) != 0);
if (was_running) return true;
// for the main heap, if this is the profiler, start the theap more aggressively
// otherwise it will be picked up when theaps take the slow generic malloc path.
mi_heap_t* heap = mi_heap_main();
if (mi_heap_profiler(heap)==profiler) {
mi_theap_t* theap = _mi_heap_theap_peek(heap);
if (theap!=NULL) {
_mi_theap_set_profile_sample_rate(theap,1);
}
}
return false;
}
bool mi_profiler_stop(mi_profiler_t* profiler) {
if (profiler==NULL) return false;
return ((mi_atomic_and_acq_rel(mi_profiler_pstate(profiler), ~(size_t)1) & 1) != 0);
}
void mi_profiler_snapshot(mi_profiler_t* profiler) {
if (profiler==NULL) return;
if (profiler->on_snapshot==NULL) return;
mi_atomic_guard(&_mi_process.profile_snapshot_guard) {
(*profiler->on_snapshot)(profiler);
}
}