From 49783a5bcec45d9aab0bc7d3a253783d7cc9de10 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Thu, 13 Aug 2026 21:17:50 +0000 Subject: [PATCH 1/2] purge-holes: keep the sweep's scratch state on the tld, not in __thread statics `mi_purging_holes` (the sweep re-entrancy guard) is read from `mi_page_free_collect_ex`, i.e. on the allocation slow path, and the three `mi_holes_sweep_*` counters/flags sat next to it as `mi_decl_thread` statics. On targets with emulated TLS (Android before API 29, which is what Bun ships) the first access to a `__thread` variable calls `malloc` to allocate the thread's TLS block. A fresh thread whose first slow-path allocation was also its first touch of `mi_purging_holes` therefore went malloc -> mi_page_free_collect_ex -> __emutls_get_address -> malloc -> ... until it ran out of stack (seen as silent SIGSEGVs on JSC's Wasm compiler threads). Upstream already keeps compiler TLS off the allocation path on such platforms (MI_TLS_MODEL_PTHREADS on Android); follow that: the four values now live at the tail of `mi_tld_t` and are reached through `_mi_theap_default()`, the accessor the fast path itself uses. They are state of the thread running the sweep, so `_sweep_begin` writes the sweeper's tld while still pacing the sequence off the tld being swept. --- include/mimalloc/types.h | 9 +++++ src/page.c | 78 +++++++++++++++++++++++++--------------- 2 files changed, 58 insertions(+), 29 deletions(-) diff --git a/include/mimalloc/types.h b/include/mimalloc/types.h index d7c2689be..b1f73e128 100644 --- a/include/mimalloc/types.h +++ b/include/mimalloc/types.h @@ -709,6 +709,15 @@ struct mi_tld_s { mi_tld_t* subproc_next; // list of tlds in the subproc, so the scavenger can find parked threads size_t holes_sweep_seq; // idle sweeps run over THIS tld's heaps (paces `purge_holes_full_every`) mi_msecs_t holes_sweep_last; // when this tld's heaps were last swept (paces `purge_holes_min_interval`) + + // Scratch state of a hole sweep run BY this thread (over its own or a parked thread's heaps). + // Deliberately not compiler thread-locals: `purging_holes` is read on the allocation slow path, + // and with emulated TLS (Android before API 29) the first access to a `__thread` variable + // itself calls `malloc`, which recursed without bound on a fresh thread. + bool purging_holes; // re-entrancy guard: a sweep is rewriting free lists on this thread right now + bool holes_sweep_full; // this sweep ignores `page->swept_state` + size_t holes_sweep_skipped; // per-sweep counters, folded into the process-wide stats at `_end` + size_t holes_sweep_visited; }; diff --git a/src/page.c b/src/page.c index 382d7d92c..ee475fe4a 100644 --- a/src/page.c +++ b/src/page.c @@ -472,44 +472,62 @@ static void mi_holes_count_reuse(size_t bytes, size_t blocks, bool reused) { mi_atomic_addi64_relaxed(&mi_holes_blocks, -(int64_t)blocks); } -// Re-entrancy guard: while the idle sweep is rewriting a page's free list and -// bitmap, a nested `mi_malloc` (only reachable through a user output function -// from a warning message) must not un-purge a hole from under it. -static mi_decl_thread bool mi_purging_holes; +// The sweep's per-thread scratch state (`purging_holes`, `holes_sweep_*`) lives on the tld of the +// thread RUNNING the sweep -- see `mi_tld_s`. It is reached through `_mi_theap_default()`, the same +// accessor the allocation fast path uses, so it is safe wherever `malloc` itself is (in particular +// it never goes through emulated TLS, which allocates on first access). +// +// - `purging_holes` is the re-entrancy guard: while the idle sweep is rewriting a page's free list +// and bitmap, a nested `mi_malloc` (only reachable through a user output function from a warning +// message) must not un-purge a hole from under it. +// - `holes_sweep_skipped/visited` are per-sweep counters. The sweep runs over every page of the +// thread, so a process-wide atomic per page would be a real cost on the very path we are making +// cheap: accumulate per thread and fold them in once per pass, in `_mi_page_purge_holes_end`. +// - `holes_sweep_full` is whether THIS sweep ignores `page->swept_state` (see `_mi_page_purge_holes`). +// Set once per idle sweep, in `_mi_page_purge_holes_sweep_begin`; the sequence it is paced by +// lives on the tld being swept, since one scavenger thread runs the sweeps of many. -// Per-sweep counters. The sweep runs over every page of the thread, so a process-wide atomic -// per page would be a real cost on the very path we are making cheap: accumulate thread-locally -// and fold them in once per pass, in `_mi_page_purge_holes_end`. -static mi_decl_thread size_t mi_holes_sweep_skipped; -static mi_decl_thread size_t mi_holes_sweep_visited; +// The sweeping thread's tld, or NULL on a thread that has not initialized mimalloc yet (such a +// thread cannot be inside a sweep: `_mi_page_purge_holes_begin` initializes it). +static mi_tld_t* mi_holes_sweep_tld(void) { + mi_theap_t* const theap = _mi_theap_default(); + return ((theap != NULL && mi_theap_is_initialized(theap)) ? theap->tld : NULL); +} -// Whether THIS sweep ignores `page->swept_state` (see `_mi_page_purge_holes`). Set once per idle -// sweep, in `_mi_page_purge_holes_sweep_begin`; the sequence it is paced by lives on the tld being -// swept, since one scavenger thread runs the sweeps of many. -static mi_decl_thread bool mi_holes_sweep_full; +bool _mi_page_purge_holes_in_progress(void) { + const mi_tld_t* const tld = mi_holes_sweep_tld(); + return (tld != NULL && tld->purging_holes); +} -bool _mi_page_purge_holes_in_progress(void) { return mi_purging_holes; } -void _mi_page_purge_holes_begin(void) { mi_assert_internal(!mi_purging_holes); mi_purging_holes = true; } +void _mi_page_purge_holes_begin(void) { + mi_tld_t* const tld = mi_theap_get_default()->tld; // `get_default`: make sure this thread has a tld of its own + mi_assert_internal(tld != NULL && !tld->purging_holes); + tld->purging_holes = true; +} void _mi_page_purge_holes_end(void) { - mi_assert_internal(mi_purging_holes); - mi_purging_holes = false; - if (mi_holes_sweep_skipped > 0) { - mi_atomic_addi64_relaxed(&mi_holes_pages_skipped, (int64_t)mi_holes_sweep_skipped); - mi_holes_sweep_skipped = 0; + mi_tld_t* const tld = mi_holes_sweep_tld(); + mi_assert_internal(tld != NULL && tld->purging_holes); + if (tld == NULL) return; + tld->purging_holes = false; + if (tld->holes_sweep_skipped > 0) { + mi_atomic_addi64_relaxed(&mi_holes_pages_skipped, (int64_t)tld->holes_sweep_skipped); + tld->holes_sweep_skipped = 0; } - if (mi_holes_sweep_visited > 0) { - mi_atomic_addi64_relaxed(&mi_holes_blocks_visited, (int64_t)mi_holes_sweep_visited); - mi_holes_sweep_visited = 0; + if (tld->holes_sweep_visited > 0) { + mi_atomic_addi64_relaxed(&mi_holes_blocks_visited, (int64_t)tld->holes_sweep_visited); + tld->holes_sweep_visited = 0; } } // Called once per idle sweep of `tld`'s heaps, before its passes (`mi_purge_holes_of`). void _mi_page_purge_holes_sweep_begin(mi_tld_t* tld) { + // `tld` is the tld being swept (it paces the sequence); the flag is state of the sweeping thread. + mi_tld_t* const sweeper = mi_theap_get_default()->tld; const long every = mi_option_get(mi_option_purge_holes_full_every); const size_t seq = ++tld->holes_sweep_seq; - mi_holes_sweep_full = (every > 0 && (seq % (size_t)every) == 0); - if (mi_holes_sweep_full) { mi_atomic_addi64_relaxed(&mi_holes_full_sweeps, 1); } + sweeper->holes_sweep_full = (every > 0 && (seq % (size_t)every) == 0); + if (sweeper->holes_sweep_full) { mi_atomic_addi64_relaxed(&mi_holes_full_sweeps, 1); } } static inline bool mi_page_bits_at(const uint64_t* bits, size_t k) { @@ -703,7 +721,8 @@ static bool mi_page_purge_holes_walk(mi_page_t* page) { nfree[k]++; } } - mi_holes_sweep_visited += nvisited; // folded into the process-wide counter at the end of the sweep + mi_tld_t* const sweeper = mi_holes_sweep_tld(); + if (sweeper != NULL) { sweeper->holes_sweep_visited += nvisited; } // folded into the process-wide counter at the end of the sweep // 2. an OS page can be discarded when *every* block overlapping it is free -- either on the // free list, or purged already. Of the blocks overlapping an OS page, only the first and @@ -797,12 +816,13 @@ void _mi_page_purge_holes(mi_page_t* page) { mi_page_purge_unformed_tail(page); // the blocks that are not formed yet: resident, but never handed out if (!mi_page_can_purge_holes(page)) { _mi_page_holes_count_ineligible(page); return; } - if (!mi_holes_sweep_full && page->swept_state == mi_page_sweep_state(page)) { - mi_holes_sweep_skipped++; // nothing was allocated or freed in this page since we swept it + mi_tld_t* const sweeper = mi_holes_sweep_tld(); + if ((sweeper == NULL || !sweeper->holes_sweep_full) && page->swept_state == mi_page_sweep_state(page)) { + if (sweeper != NULL) { sweeper->holes_sweep_skipped++; } // nothing was allocated or freed in this page since we swept it return; } // Record the state we LEAVE the page in, read back from the page: a nested `mi_malloc` (see - // `mi_purging_holes`) may have taken a block out of it while we walked. + // `purging_holes`) may have taken a block out of it while we walked. // // Only if the walk got everything. A failed `_mi_os_discard` (ENOMEM under pressure) puts its // blocks straight back, and changes neither `capacity` nor `used` -- so recording here would From d4add9882a73418237b11708d96ea9ddbb034e8a Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Thu, 13 Aug 2026 21:26:47 +0000 Subject: [PATCH 2/2] purge-holes: keep the scratch on the swept tld and pass it explicitly Reaching the state through `_mi_theap_default()` initialized a theap on mimalloc's own scavenger thread the first time it swept a parked thread, which then tripped `mi_page_is_valid_init`'s ownership assertion while it validated that thread's pages. The scavenger is meant to stay theap-less, so instead of "state of the sweeping thread" make it "state of the tld being swept": the sweeper holds that tld exclusively (its own, or a parked one it claimed), every sweep entry point already has it in hand, and `_mi_page_purge_holes`/`_walk` now take it as a parameter. The allocation-path reader `_mi_page_purge_holes_in_progress` takes the page and consults the page owner's tld: a nested allocation on the sweeping thread can only be served from a page under the sweep when the sweeper is that page's owner, and abandoned pages (theap == NULL) are not allocated from at all. No thread-local access of any kind remains. --- include/mimalloc/internal.h | 8 ++--- include/mimalloc/types.h | 10 +++--- src/arena.c | 6 ++-- src/page.c | 66 ++++++++++++++++--------------------- src/theap.c | 6 ++-- 5 files changed, 43 insertions(+), 53 deletions(-) diff --git a/include/mimalloc/internal.h b/include/mimalloc/internal.h index 88d943d10..20bb932a8 100644 --- a/include/mimalloc/internal.h +++ b/include/mimalloc/internal.h @@ -889,7 +889,7 @@ static inline bool mi_page_all_free(const mi_page_t* page) { // Page hole purging (see the "Page hole purging" section in `page.c`) // ------------------------------------------------------ -void _mi_page_purge_holes(mi_page_t* page); +void _mi_page_purge_holes(mi_page_t* page, mi_tld_t* tld); void _mi_page_purged_reset(mi_page_t* page); bool _mi_page_unpurge_run(mi_page_t* page); void _mi_page_unpurge_all(mi_page_t* page); @@ -898,12 +898,12 @@ void _mi_page_unpurge_unformed_upto(mi_page_t* page, uintptr_t end); size_t _mi_page_unformed_purged_bytes(const mi_page_t* page); // the bytes of this page's unformed tail that are discarded right now bool _mi_page_purge_os_page_blocks(size_t os_page_size, size_t block_size, uintptr_t page_start, size_t capacity, size_t k, size_t* first, size_t* last); -bool _mi_page_purge_holes_in_progress(void); +bool _mi_page_purge_holes_in_progress(const mi_page_t* page); void _mi_page_holes_count_page_freed(void); void _mi_page_holes_count_ineligible(const mi_page_t* page); void _mi_page_holes_reset_ineligible(void); -void _mi_page_purge_holes_begin(void); -void _mi_page_purge_holes_end(void); +void _mi_page_purge_holes_begin(mi_tld_t* tld); +void _mi_page_purge_holes_end(mi_tld_t* tld); void _mi_page_purge_holes_sweep_begin(mi_tld_t* tld); // once per idle sweep, before its passes // ------------------------------------------------------ diff --git a/include/mimalloc/types.h b/include/mimalloc/types.h index b1f73e128..330f53e75 100644 --- a/include/mimalloc/types.h +++ b/include/mimalloc/types.h @@ -710,11 +710,11 @@ struct mi_tld_s { size_t holes_sweep_seq; // idle sweeps run over THIS tld's heaps (paces `purge_holes_full_every`) mi_msecs_t holes_sweep_last; // when this tld's heaps were last swept (paces `purge_holes_min_interval`) - // Scratch state of a hole sweep run BY this thread (over its own or a parked thread's heaps). - // Deliberately not compiler thread-locals: `purging_holes` is read on the allocation slow path, - // and with emulated TLS (Android before API 29) the first access to a `__thread` variable - // itself calls `malloc`, which recursed without bound on a fresh thread. - bool purging_holes; // re-entrancy guard: a sweep is rewriting free lists on this thread right now + // Scratch state of the hole sweep running over THIS tld's heaps (by its own thread, or by the + // scavenger while it is parked). Deliberately not compiler thread-locals: `purging_holes` is + // read on the allocation slow path, and with emulated TLS (Android before API 29) the first + // access to a `__thread` variable itself calls `malloc`, which recursed without bound. + bool purging_holes; // re-entrancy guard: a sweep is rewriting this tld's free lists right now bool holes_sweep_full; // this sweep ignores `page->swept_state` size_t holes_sweep_skipped; // per-sweep counters, folded into the process-wide stats at `_end` size_t holes_sweep_visited; diff --git a/src/arena.c b/src/arena.c index af7e8fed5..df39cf961 100644 --- a/src/arena.c +++ b/src/arena.c @@ -1362,7 +1362,7 @@ static bool mi_arena_page_purge_holes_at(size_t slice_index, size_t slice_count, _mi_page_holes_count_page_freed(); return true; } - _mi_page_purge_holes(page); + _mi_page_purge_holes(page, parg->tld); mi_bitmap_set(bitmap, slice_index); // back in the map *before* unowning: unown may free the page mi_abandoned_page_unown(page, NULL); return true; @@ -1374,7 +1374,7 @@ static bool mi_arena_page_purge_holes_at(size_t slice_index, size_t slice_count, void _mi_arenas_purge_abandoned_holes(mi_heap_t* heap, mi_tld_t* tld) { if (heap == NULL) return; if (!mi_option_is_enabled(mi_option_purge_holes)) return; - _mi_page_purge_holes_begin(); + _mi_page_purge_holes_begin(tld); mi_forall_arenas(heap, ((mi_arena_t*)NULL), 0, arena) { mi_arena_pages_t* const arena_pages = mi_heap_arena_pages(heap, arena); if (arena_pages != NULL) { @@ -1389,7 +1389,7 @@ void _mi_arenas_purge_abandoned_holes(mi_heap_t* heap, mi_tld_t* tld) { } } mi_forall_arenas_end(); - _mi_page_purge_holes_end(); + _mi_page_purge_holes_end(tld); } // The read-only counterpart of the sweep above: account for the holes in the abandoned pages diff --git a/src/page.c b/src/page.c index ee475fe4a..874218565 100644 --- a/src/page.c +++ b/src/page.c @@ -472,41 +472,36 @@ static void mi_holes_count_reuse(size_t bytes, size_t blocks, bool reused) { mi_atomic_addi64_relaxed(&mi_holes_blocks, -(int64_t)blocks); } -// The sweep's per-thread scratch state (`purging_holes`, `holes_sweep_*`) lives on the tld of the -// thread RUNNING the sweep -- see `mi_tld_s`. It is reached through `_mi_theap_default()`, the same -// accessor the allocation fast path uses, so it is safe wherever `malloc` itself is (in particular -// it never goes through emulated TLS, which allocates on first access). +// The sweep's scratch state (`purging_holes`, `holes_sweep_*`) lives on the tld whose heaps are being +// swept -- see `mi_tld_s` -- and is passed down explicitly, so nothing here reads compiler thread +// locals (with emulated TLS, e.g. Android before API 29, the first access to a `__thread` variable +// allocates, and `_mi_page_purge_holes_in_progress` is reached from the allocation slow path). +// The sweeping thread owns that tld for the duration: it is either its own, or a parked thread's +// that it claimed (MI_PARK_SWEEPING). // -// - `purging_holes` is the re-entrancy guard: while the idle sweep is rewriting a page's free list -// and bitmap, a nested `mi_malloc` (only reachable through a user output function from a warning -// message) must not un-purge a hole from under it. +// - `purging_holes` is the re-entrancy guard: while the sweep is rewriting a page's free list and +// bitmap, a nested `mi_malloc` on the sweeping thread (only reachable through a user output +// function from a warning message) must not un-purge a hole from under it. Such an allocation +// can only come from a page of the swept tld when the sweeper is the owner itself, which is why +// the reader consults the page owner's tld. // - `holes_sweep_skipped/visited` are per-sweep counters. The sweep runs over every page of the // thread, so a process-wide atomic per page would be a real cost on the very path we are making -// cheap: accumulate per thread and fold them in once per pass, in `_mi_page_purge_holes_end`. +// cheap: accumulate on the tld and fold them in once per pass, in `_mi_page_purge_holes_end`. // - `holes_sweep_full` is whether THIS sweep ignores `page->swept_state` (see `_mi_page_purge_holes`). -// Set once per idle sweep, in `_mi_page_purge_holes_sweep_begin`; the sequence it is paced by -// lives on the tld being swept, since one scavenger thread runs the sweeps of many. +// Set once per idle sweep, in `_mi_page_purge_holes_sweep_begin`, paced by the same tld's sequence. -// The sweeping thread's tld, or NULL on a thread that has not initialized mimalloc yet (such a -// thread cannot be inside a sweep: `_mi_page_purge_holes_begin` initializes it). -static mi_tld_t* mi_holes_sweep_tld(void) { - mi_theap_t* const theap = _mi_theap_default(); - return ((theap != NULL && mi_theap_is_initialized(theap)) ? theap->tld : NULL); +bool _mi_page_purge_holes_in_progress(const mi_page_t* page) { + const mi_theap_t* const theap = page->theap; // NULL for an abandoned page: nobody allocates from those + return (theap != NULL && theap->tld != NULL && theap->tld->purging_holes); } -bool _mi_page_purge_holes_in_progress(void) { - const mi_tld_t* const tld = mi_holes_sweep_tld(); - return (tld != NULL && tld->purging_holes); -} - -void _mi_page_purge_holes_begin(void) { - mi_tld_t* const tld = mi_theap_get_default()->tld; // `get_default`: make sure this thread has a tld of its own +void _mi_page_purge_holes_begin(mi_tld_t* tld) { mi_assert_internal(tld != NULL && !tld->purging_holes); + if (tld == NULL) return; tld->purging_holes = true; } -void _mi_page_purge_holes_end(void) { - mi_tld_t* const tld = mi_holes_sweep_tld(); +void _mi_page_purge_holes_end(mi_tld_t* tld) { mi_assert_internal(tld != NULL && tld->purging_holes); if (tld == NULL) return; tld->purging_holes = false; @@ -520,14 +515,11 @@ void _mi_page_purge_holes_end(void) { } } -// Called once per idle sweep of `tld`'s heaps, before its passes (`mi_purge_holes_of`). void _mi_page_purge_holes_sweep_begin(mi_tld_t* tld) { - // `tld` is the tld being swept (it paces the sequence); the flag is state of the sweeping thread. - mi_tld_t* const sweeper = mi_theap_get_default()->tld; const long every = mi_option_get(mi_option_purge_holes_full_every); const size_t seq = ++tld->holes_sweep_seq; - sweeper->holes_sweep_full = (every > 0 && (seq % (size_t)every) == 0); - if (sweeper->holes_sweep_full) { mi_atomic_addi64_relaxed(&mi_holes_full_sweeps, 1); } + tld->holes_sweep_full = (every > 0 && (seq % (size_t)every) == 0); + if (tld->holes_sweep_full) { mi_atomic_addi64_relaxed(&mi_holes_full_sweeps, 1); } } static inline bool mi_page_bits_at(const uint64_t* bits, size_t k) { @@ -696,7 +688,7 @@ void _mi_page_unpurge_unformed_upto(mi_page_t* page, uintptr_t end) { // Walk the free list of a page and discard every OS page in it that holds no live block. // Returns false if any discard failed: those blocks went straight back on the free list and the // page must be swept again, so the caller must not record it as swept. -static bool mi_page_purge_holes_walk(mi_page_t* page) { +static bool mi_page_purge_holes_walk(mi_page_t* page, mi_tld_t* tld) { if (page->free == NULL) return true; // nothing to take off the free list const size_t os_size = _mi_os_page_size(); @@ -721,8 +713,7 @@ static bool mi_page_purge_holes_walk(mi_page_t* page) { nfree[k]++; } } - mi_tld_t* const sweeper = mi_holes_sweep_tld(); - if (sweeper != NULL) { sweeper->holes_sweep_visited += nvisited; } // folded into the process-wide counter at the end of the sweep + if (tld != NULL) { tld->holes_sweep_visited += nvisited; } // folded into the process-wide counter at the end of the sweep // 2. an OS page can be discarded when *every* block overlapping it is free -- either on the // free list, or purged already. Of the blocks overlapping an OS page, only the first and @@ -808,7 +799,7 @@ static bool mi_page_purge_holes_walk(mi_page_t* page) { // regardless, which caps the delay of a missed discard at N parks for 1/N of the old cost. // (An exact "was anything freed in this page" bit is the alternative, and it costs a store in // `mi_free` itself -- the hot path this whole feature stays off.) -void _mi_page_purge_holes(mi_page_t* page) { +void _mi_page_purge_holes(mi_page_t* page, mi_tld_t* tld) { mi_assert_internal(page != NULL); if (!mi_option_is_enabled(mi_option_purge_holes)) return; if (mi_page_all_free(page)) return; // the page itself is about to be freed @@ -816,9 +807,8 @@ void _mi_page_purge_holes(mi_page_t* page) { mi_page_purge_unformed_tail(page); // the blocks that are not formed yet: resident, but never handed out if (!mi_page_can_purge_holes(page)) { _mi_page_holes_count_ineligible(page); return; } - mi_tld_t* const sweeper = mi_holes_sweep_tld(); - if ((sweeper == NULL || !sweeper->holes_sweep_full) && page->swept_state == mi_page_sweep_state(page)) { - if (sweeper != NULL) { sweeper->holes_sweep_skipped++; } // nothing was allocated or freed in this page since we swept it + if ((tld == NULL || !tld->holes_sweep_full) && page->swept_state == mi_page_sweep_state(page)) { + if (tld != NULL) { tld->holes_sweep_skipped++; } // nothing was allocated or freed in this page since we swept it return; } // Record the state we LEAVE the page in, read back from the page: a nested `mi_malloc` (see @@ -828,7 +818,7 @@ void _mi_page_purge_holes(mi_page_t* page) { // blocks straight back, and changes neither `capacity` nor `used` -- so recording here would // say "already swept" for a page that still has holes, and the skip check would then park them // until the next full sweep, or forever with `purge_holes_full_every=0`. - if (mi_page_purge_holes_walk(page)) { + if (mi_page_purge_holes_walk(page, tld)) { page->swept_state = mi_page_sweep_state(page); } } @@ -1230,7 +1220,7 @@ static void mi_page_free_collect_ex(mi_page_t* page, bool force, bool allow_unpu // Free list empty but this page has discarded holes: bring a whole run of them // back. Every caller re-checks `mi_page_immediate_available` after collect, so the // page becomes usable again without touching the other holes. - if (allow_unpurge && page->free == NULL && mi_page_has_purged(page) && !_mi_page_purge_holes_in_progress()) { + if (allow_unpurge && page->free == NULL && mi_page_has_purged(page) && !_mi_page_purge_holes_in_progress(page)) { _mi_page_unpurge_run(page); } } diff --git a/src/theap.c b/src/theap.c index 1a0a9463d..89fe91c65 100644 --- a/src/theap.c +++ b/src/theap.c @@ -178,7 +178,7 @@ static bool mi_theap_page_purge_holes(mi_theap_t* theap, mi_page_queue_t* pq, mi _mi_page_free(page, pq); return true; } - _mi_page_purge_holes(page); + _mi_page_purge_holes(page, theap->tld); mi_assert_expensive(_mi_page_is_valid(page)); return true; // continue } @@ -193,9 +193,9 @@ static void mi_theap_purge_holes(mi_theap_t* theap) mi_attr_noexcept { if (theap->tld == NULL) return; if (theap->tld->thread_id != _mi_thread_id() && mi_atomic_load_acquire(&theap->tld->park_state) != MI_PARK_SWEEPING) return; - _mi_page_purge_holes_begin(); + _mi_page_purge_holes_begin(theap->tld); mi_theap_visit_pages(theap, &mi_theap_page_purge_holes, true /* include full pages */, NULL, NULL); - _mi_page_purge_holes_end(); + _mi_page_purge_holes_end(theap->tld); } // Purge the holes in every page this thread may safely touch: