LibGC: Introduce ConservativeHashMap

This commit is contained in:
Luke Wilde 2026-04-09 16:33:01 +01:00 committed by Shannon Booth
parent 883199cb39
commit f955d4e8a4
9 changed files with 220 additions and 1 deletions

View file

@ -2,6 +2,7 @@ set(SOURCES
BlockAllocator.cpp
Cell.cpp
CellAllocator.cpp
ConservativeHashMap.cpp
ConservativeVector.cpp
Root.cpp
RootHashMap.cpp

View file

@ -0,0 +1,28 @@
/*
* Copyright (c) 2026, Luke Wilde <luke@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibGC/ConservativeHashMap.h>
#include <LibGC/Heap.h>
namespace GC {
ConservativeHashMapBase::ConservativeHashMapBase()
: ConservativeHashMapBase(Heap::the())
{
}
ConservativeHashMapBase::ConservativeHashMapBase(Heap& heap)
: m_heap(&heap)
{
m_heap->did_create_conservative_hash_map({}, *this);
}
ConservativeHashMapBase::~ConservativeHashMapBase()
{
m_heap->did_destroy_conservative_hash_map({}, *this);
}
}

View file

@ -0,0 +1,88 @@
/*
* Copyright (c) 2026, Luke Wilde <luke@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Function.h>
#include <AK/HashMap.h>
#include <AK/IntrusiveList.h>
#include <LibGC/Forward.h>
namespace GC {
class GC_API ConservativeHashMapBase {
AK_MAKE_NONCOPYABLE(ConservativeHashMapBase);
public:
virtual void for_each_possible_value(AK::Function<void(FlatPtr)> callback) const = 0;
protected:
ConservativeHashMapBase();
explicit ConservativeHashMapBase(Heap&);
~ConservativeHashMapBase();
Heap* m_heap { nullptr };
IntrusiveListNode<ConservativeHashMapBase> m_list_node;
public:
using List = IntrusiveList<&ConservativeHashMapBase::m_list_node>;
};
template<typename K, typename V, typename KeyTraits = Traits<K>, typename ValueTraits = Traits<V>, bool IsOrdered = false>
class GC_API ConservativeHashMap final
: public ConservativeHashMapBase
, public HashMap<K, V, KeyTraits, ValueTraits, IsOrdered> {
using HashMapBase = HashMap<K, V, KeyTraits, ValueTraits, IsOrdered>;
public:
ConservativeHashMap()
: ConservativeHashMapBase()
{
}
ConservativeHashMap(ConservativeHashMap const& other)
: ConservativeHashMapBase(*other.m_heap)
, HashMapBase(static_cast<HashMapBase const&>(other))
{
}
ConservativeHashMap(ConservativeHashMap&& other)
: ConservativeHashMapBase(*other.m_heap)
, HashMapBase(move(static_cast<HashMapBase&>(other)))
{
}
ConservativeHashMap& operator=(ConservativeHashMap const& other)
{
if (&other == this)
return *this;
HashMapBase::operator=(static_cast<HashMapBase const&>(other));
return *this;
}
~ConservativeHashMap() = default;
virtual void for_each_possible_value(AK::Function<void(FlatPtr)> callback) const override
{
auto scan_bytes = [&](ReadonlyBytes bytes) {
for (size_t i = 0; i + sizeof(FlatPtr) <= bytes.size(); i += sizeof(FlatPtr)) {
FlatPtr value;
memcpy(&value, bytes.offset(i), sizeof(FlatPtr));
callback(value);
}
};
for (auto& [key, value] : *this) {
scan_bytes({ &key, sizeof(K) });
scan_bytes({ &value, sizeof(V) });
}
}
};
template<typename K, typename V, typename KeyTraits = Traits<K>, typename ValueTraits = Traits<V>>
using OrderedConservativeHashMap = ConservativeHashMap<K, V, KeyTraits, ValueTraits, true>;
}

View file

@ -34,6 +34,9 @@ class Root;
template<class T, size_t inline_capacity = 0>
class ConservativeVector;
template<typename K, typename V, typename KeyTraits, typename ValueTraits, bool IsOrdered>
class ConservativeHashMap;
template<class T>
class HeapVector;

View file

@ -476,6 +476,9 @@ public:
auto type = it.value.root_origin->type;
auto const* location = it.value.root_origin->location;
switch (type) {
case HeapRoot::Type::ConservativeHashMap:
node.set("root"sv, "ConservativeHashMap"sv);
break;
case HeapRoot::Type::ConservativeVector:
node.set("root"sv, "ConservativeVector"sv);
break;
@ -968,6 +971,12 @@ NO_SANITIZE_ADDRESS void Heap::gather_conservative_roots(HashMap<Cell*, HeapRoot
}
}
for (auto& hash_map : m_conservative_hash_maps) {
hash_map.for_each_possible_value([&](FlatPtr possible_value) {
add_possible_value(possible_pointers, possible_value, HeapRoot { .type = HeapRoot::Type::ConservativeHashMap }, min_block_address, max_block_address);
});
}
{
ScopedPhaseTimer timer { g_recording_phase_timings, g_phase_timings.conservative_cell_lookup_us };
for_each_cell_among_possible_pointers(m_live_heap_blocks, possible_pointers, [&](Cell* cell, FlatPtr possible_pointer) {

View file

@ -19,6 +19,7 @@
#include <LibCore/Forward.h>
#include <LibGC/Cell.h>
#include <LibGC/CellAllocator.h>
#include <LibGC/ConservativeHashMap.h>
#include <LibGC/ConservativeVector.h>
#include <LibGC/Forward.h>
#include <LibGC/HeapRoot.h>
@ -87,6 +88,8 @@ public:
void did_create_root_hash_table(Badge<RootHashTableBase>, RootHashTableBase&);
void did_destroy_root_hash_table(Badge<RootHashTableBase>, RootHashTableBase&);
void did_create_conservative_hash_map(Badge<ConservativeHashMapBase>, ConservativeHashMapBase&);
void did_destroy_conservative_hash_map(Badge<ConservativeHashMapBase>, ConservativeHashMapBase&);
void did_create_conservative_vector(Badge<ConservativeVectorBase>, ConservativeVectorBase&);
void did_destroy_conservative_vector(Badge<ConservativeVectorBase>, ConservativeVectorBase&);
@ -180,6 +183,7 @@ private:
RootVectorBase::List m_root_vectors;
RootHashMapBase::List m_root_hash_maps;
RootHashTableBase::List m_root_hash_tables;
ConservativeHashMapBase::List m_conservative_hash_maps;
ConservativeVectorBase::List m_conservative_vectors;
WeakContainer::List m_weak_containers;
@ -260,6 +264,18 @@ inline void Heap::did_destroy_root_hash_table(Badge<RootHashTableBase>, RootHash
m_root_hash_tables.remove(hash_table);
}
inline void Heap::did_create_conservative_hash_map(Badge<ConservativeHashMapBase>, ConservativeHashMapBase& hash_map)
{
VERIFY(!m_conservative_hash_maps.contains(hash_map));
m_conservative_hash_maps.append(hash_map);
}
inline void Heap::did_destroy_conservative_hash_map(Badge<ConservativeHashMapBase>, ConservativeHashMapBase& hash_map)
{
VERIFY(m_conservative_hash_maps.contains(hash_map));
m_conservative_hash_maps.remove(hash_map);
}
inline void Heap::did_create_conservative_vector(Badge<ConservativeVectorBase>, ConservativeVectorBase& vector)
{
VERIFY(!m_conservative_vectors.contains(vector));

View file

@ -14,6 +14,7 @@ namespace GC {
struct GC_API HeapRoot {
enum class Type {
ConservativeHashMap,
ConservativeVector,
HeapFunctionCapturedPointer,
MustSurviveGC,

View file

@ -120,6 +120,8 @@ static ContainsGCPtrResult record_contains_gc_ptr(clang::CXXRecordDecl const* re
"GC::Heap",
"GC::MarkedVector",
"GC::ConservativeVector",
"GC::ConservativeHashMap",
"GC::ConservativeHashMapBase",
};
if (gc_infrastructure_types.contains(qualified_name)) {
s_contains_gc_ptr_cache[record] = ContainsGCPtrResult::No;
@ -191,7 +193,7 @@ static ContainsGCPtrResult type_contains_gc_ptr(clang::QualType const& type, std
return ContainsGCPtrResult::No;
// Root types handle their own visiting
if (template_name == "GC::Root" || template_name == "GC::RootVector" || template_name == "GC::RootHashTable")
if (template_name == "GC::Root" || template_name == "GC::RootVector" || template_name == "GC::ConservativeHashMap" || template_name == "GC::RootHashTable")
return ContainsGCPtrResult::No;
// Check template arguments recursively for containers
@ -231,6 +233,7 @@ static std::vector<clang::QualType> get_all_qualified_types(clang::QualType cons
"GC::RawPtr",
"GC::RawRef",
"GC::RootVector",
"GC::ConservativeHashMap",
"GC::RootHashTable",
"GC::Root",
};

View file

@ -7,6 +7,7 @@
#include <AK/NeverDestroyed.h>
#include <LibGC/Cell.h>
#include <LibGC/CellAllocator.h>
#include <LibGC/ConservativeHashMap.h>
#include <LibGC/ConservativeVector.h>
#include <LibGC/Heap.h>
#include <LibGC/HeapHashTable.h>
@ -57,6 +58,17 @@ static bool possible_values_contain(GC::ConservativeVectorBase const& container,
return false;
}
static bool possible_values_contain(GC::ConservativeHashMapBase const& container, GC::Cell* cell)
{
bool found = false;
auto target = bit_cast<FlatPtr>(cell);
container.for_each_possible_value([&](FlatPtr value) {
if (value == target)
found = true;
});
return found;
}
TEST_CASE(root_vector_reports_roots)
{
auto& heap = test_heap();
@ -254,3 +266,61 @@ TEST_CASE(empty_containers_report_no_roots)
EXPECT_EQ(roots.size(), 0u);
}
TEST_CASE(conservative_hash_map_reports_possible_values)
{
auto& heap = test_heap();
GC::ConservativeHashMap<int, GC::Ref<TestCell>> map;
auto cell = heap.allocate<TestCell>();
map.set(42, cell);
EXPECT(possible_values_contain(map, cell.ptr()));
}
TEST_CASE(cleared_conservative_hash_map_reports_no_stale_values)
{
auto& heap = test_heap();
GC::ConservativeHashMap<int, GC::Ref<TestCell>> map;
auto cell = heap.allocate<TestCell>();
map.set(42, cell);
map.clear();
EXPECT(!possible_values_contain(map, cell.ptr()));
}
TEST_CASE(removed_conservative_hash_map_entry_not_reported)
{
auto& heap = test_heap();
GC::ConservativeHashMap<int, GC::Ref<TestCell>> map;
auto cell = heap.allocate<TestCell>();
map.set(42, cell);
map.remove(42);
EXPECT(!possible_values_contain(map, cell.ptr()));
}
TEST_CASE(conservative_hash_map_key_reports_possible_values)
{
auto& heap = test_heap();
GC::ConservativeHashMap<GC::Ref<TestCell>, int> map;
auto cell = heap.allocate<TestCell>();
map.set(cell, 42);
EXPECT(possible_values_contain(map, cell.ptr()));
}
TEST_CASE(removed_conservative_hash_map_key_not_reported)
{
auto& heap = test_heap();
GC::ConservativeHashMap<GC::Ref<TestCell>, int> map;
auto cell = heap.allocate<TestCell>();
map.set(cell, 42);
map.remove(cell);
EXPECT(!possible_values_contain(map, cell.ptr()));
}