From 42da44868b64e2c1012947c5889f116da133e39b Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Fri, 15 May 2026 14:41:34 +0200 Subject: [PATCH] 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. --- Libraries/LibGC/BlockAllocator.cpp | 6 +++--- Libraries/LibGC/BlockAllocator.h | 7 ++++++- Libraries/LibGC/CellAllocator.cpp | 4 ++-- Libraries/LibGC/CellAllocator.h | 2 +- Libraries/LibGC/Heap.cpp | 8 ++++---- 5 files changed, 16 insertions(+), 11 deletions(-) diff --git a/Libraries/LibGC/BlockAllocator.cpp b/Libraries/LibGC/BlockAllocator.cpp index a049417c07..cb8f11cff6 100644 --- a/Libraries/LibGC/BlockAllocator.cpp +++ b/Libraries/LibGC/BlockAllocator.cpp @@ -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); } diff --git a/Libraries/LibGC/BlockAllocator.h b/Libraries/LibGC/BlockAllocator.h index 84377df16c..aa418247bf 100644 --- a/Libraries/LibGC/BlockAllocator.h +++ b/Libraries/LibGC/BlockAllocator.h @@ -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(); diff --git a/Libraries/LibGC/CellAllocator.cpp b/Libraries/LibGC/CellAllocator.cpp index 7e37102f8d..45927ea6a7 100644 --- a/Libraries/LibGC/CellAllocator.cpp +++ b/Libraries/LibGC/CellAllocator.cpp @@ -53,13 +53,13 @@ Cell* CellAllocator::allocate_cell(Heap& heap) return cell; } -void CellAllocator::block_did_become_empty(Badge, HeapBlock& block) +void CellAllocator::block_did_become_empty(Badge, 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, HeapBlock& block) diff --git a/Libraries/LibGC/CellAllocator.h b/Libraries/LibGC/CellAllocator.h index fd5fcb51ce..05056caf66 100644 --- a/Libraries/LibGC/CellAllocator.h +++ b/Libraries/LibGC/CellAllocator.h @@ -46,7 +46,7 @@ public: return IterationDecision::Continue; } - void block_did_become_empty(Badge, HeapBlock&); + void block_did_become_empty(Badge, HeapBlock&, DeferDecommit = DeferDecommit::Yes); void block_did_become_usable(Badge, HeapBlock&); bool has_blocks_pending_sweep() const { return !m_blocks_pending_sweep.is_empty(); } diff --git a/Libraries/LibGC/Heap.cpp b/Libraries/LibGC/Heap.cpp index 6869236806..c4b2ddbdd4 100644 --- a/Libraries/LibGC/Heap.cpp +++ b/Libraries/LibGC/Heap.cpp @@ -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)