LibGC: Avoid decommit worker during full sweep teardown

Heap destruction uses CollectEverything to synchronously free every live
cell. Empty blocks from that pass do not need deferred madvise work.
Registering them with the global decommit worker can start a detached
thread during process teardown. LeakSanitizer can then hang while doing
its final thread scan with log_threads enabled.

Let full sweep deallocation opt out of deferred decommit registration.
Normal incremental sweeping stays on the background worker path. The
sanitizer LibGC container and visitor tests now exit with verbose LSan
thread logging enabled.
This commit is contained in:
Andreas Kling 2026-05-15 14:41:34 +02:00 committed by Andreas Kling
parent af7b2b5e8a
commit 42da44868b
5 changed files with 16 additions and 11 deletions

View file

@ -372,7 +372,7 @@ void* BlockAllocator::allocate_block([[maybe_unused]] char const* name)
return block;
}
void BlockAllocator::deallocate_block(void* block)
void BlockAllocator::deallocate_block(void* block, DeferDecommit defer_decommit)
{
VERIFY(block);
@ -385,12 +385,12 @@ void BlockAllocator::deallocate_block(void* block)
{
Sync::MutexLocker locker(m_mutex);
m_freshly_freed.append(block);
if (!m_in_decommit_registry) {
if (defer_decommit == DeferDecommit::Yes && !m_in_decommit_registry) {
m_in_decommit_registry = true;
need_to_register = true;
}
}
if (need_to_register)
if (need_to_register && defer_decommit == DeferDecommit::Yes)
DecommitWorker::the().register_pending(*this);
}

View file

@ -14,6 +14,11 @@
namespace GC {
enum class DeferDecommit {
No,
Yes,
};
class DecommitWorker;
class GC_API BlockAllocator {
@ -22,7 +27,7 @@ public:
~BlockAllocator();
void* allocate_block(char const* name);
void deallocate_block(void*);
void deallocate_block(void*, DeferDecommit = DeferDecommit::Yes);
size_t block_count();

View file

@ -53,13 +53,13 @@ Cell* CellAllocator::allocate_cell(Heap& heap)
return cell;
}
void CellAllocator::block_did_become_empty(Badge<Heap>, HeapBlock& block)
void CellAllocator::block_did_become_empty(Badge<Heap>, HeapBlock& block, DeferDecommit defer_decommit)
{
block.m_list_node.remove();
block.heap().m_live_heap_blocks.remove(&block);
// NOTE: HeapBlocks are managed by the BlockAllocator, so we don't want to `delete` the block here.
block.~HeapBlock();
m_block_allocator.deallocate_block(&block);
m_block_allocator.deallocate_block(&block, defer_decommit);
}
void CellAllocator::block_did_become_usable(Badge<Heap>, HeapBlock& block)

View file

@ -46,7 +46,7 @@ public:
return IterationDecision::Continue;
}
void block_did_become_empty(Badge<Heap>, HeapBlock&);
void block_did_become_empty(Badge<Heap>, HeapBlock&, DeferDecommit = DeferDecommit::Yes);
void block_did_become_usable(Badge<Heap>, HeapBlock&);
bool has_blocks_pending_sweep() const { return !m_blocks_pending_sweep.is_empty(); }

View file

@ -1125,7 +1125,7 @@ void Heap::sweep_dead_cells(bool print_report, Core::ElapsedTimer const& measure
ScopedPhaseTimer timer { g_recording_phase_timings, g_phase_timings.sweep_block_reclassify_us };
for (auto* block : empty_blocks) {
dbgln_if(HEAP_DEBUG, " - HeapBlock empty @ {}: cell_size={}", block, block->cell_size());
block->cell_allocator().block_did_become_empty({}, *block);
block->cell_allocator().block_did_become_empty({}, *block, DeferDecommit::No);
}
for (auto* block : full_blocks_that_became_usable) {
@ -1158,9 +1158,9 @@ void Heap::sweep_dead_cells(bool print_report, Core::ElapsedTimer const& measure
}
(void)measurement_timer;
// Sweep is done; kick the global decommit worker so the slots we just
// freed get madvise()'d off the GC pause path.
BlockAllocator::wake_decommit_worker_async();
// CollectEverything finishes synchronously, and is currently only used
// when the heap is going away. Do not spin up background decommit work
// from process teardown.
}
void Heap::sweep_block(HeapBlock& block)