ladybird/Libraries/LibGC/WeakHashSet.h
Andreas Kling fc675e8634 LibGC: Amortize pruning of dead entries in WeakHashSet
Previously, every set() and remove() call would do a full O(capacity)
scan of the underlying hash table to remove dead weak references.
This caused significant stalls on pages where the set grew large, as
every insertion paid the full cost of scanning the entire table.

Fix this by tracking mutations since last prune and only performing
the scan after enough operations to amortize the cost. The threshold
scales with the table size (minimum 64), so the per-operation cost
is O(1) amortized.
2026-03-08 20:49:28 +01:00

156 lines
3.7 KiB
C++

/*
* Copyright (c) 2026, Andreas Kling <andreas@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/HashTable.h>
#include <LibGC/Weak.h>
namespace GC {
template<typename T>
class WeakHashSet {
struct InternalTraits : public DefaultTraits<Weak<T>> {
static unsigned hash(Weak<T> const& value)
{
return Traits<T*>::hash(value.ptr());
}
};
using TableType = HashTable<Weak<T>, InternalTraits>;
public:
WeakHashSet() = default;
void set(T& value)
{
maybe_prune();
m_table.set(Weak<T>(value));
}
bool remove(T& value)
{
maybe_prune();
auto it = m_table.find(Traits<T*>::hash(&value), [&](auto& entry) { return entry.ptr() == &value; });
if (it == m_table.end())
return false;
m_table.remove(it);
return true;
}
bool contains(T& value) const
{
auto it = m_table.find(Traits<T*>::hash(&value), [&](auto& entry) { return entry.ptr() == &value; });
return it != m_table.end();
}
bool is_empty() const
{
for (auto const& entry : m_table) {
if (entry.ptr())
return false;
}
return true;
}
void clear() { m_table.clear(); }
class Iterator {
public:
bool operator==(Iterator const& other) const { return m_it == other.m_it; }
bool operator!=(Iterator const& other) const { return m_it != other.m_it; }
Iterator& operator++()
{
++m_it;
skip_dead();
return *this;
}
T& operator*() { return *m_it->ptr(); }
T* operator->() { return m_it->ptr(); }
private:
friend class WeakHashSet;
using InnerIterator = typename TableType::Iterator;
explicit Iterator(InnerIterator it, InnerIterator end)
: m_it(it)
, m_end(end)
{
skip_dead();
}
void skip_dead()
{
while (m_it != m_end && !m_it->ptr())
++m_it;
}
InnerIterator m_it;
InnerIterator m_end;
};
class ConstIterator {
public:
bool operator==(ConstIterator const& other) const { return m_it == other.m_it; }
bool operator!=(ConstIterator const& other) const { return m_it != other.m_it; }
ConstIterator& operator++()
{
++m_it;
skip_dead();
return *this;
}
T& operator*() { return *(*m_it).ptr(); }
T* operator->() { return (*m_it).ptr(); }
private:
friend class WeakHashSet;
using InnerIterator = typename TableType::ConstIterator;
explicit ConstIterator(InnerIterator it, InnerIterator end)
: m_it(it)
, m_end(end)
{
skip_dead();
}
void skip_dead()
{
while (m_it != m_end && !m_it->ptr())
++m_it;
}
InnerIterator m_it;
InnerIterator m_end;
};
Iterator begin() { return Iterator(m_table.begin(), m_table.end()); }
Iterator end() { return Iterator(m_table.end(), m_table.end()); }
ConstIterator begin() const { return ConstIterator(m_table.begin(), m_table.end()); }
ConstIterator end() const { return ConstIterator(m_table.end(), m_table.end()); }
private:
void maybe_prune()
{
if (++m_mutations_since_last_prune < max(m_table.size(), static_cast<size_t>(64)))
return;
m_table.remove_all_matching([](Weak<T> const& entry) {
return !entry.ptr();
});
m_mutations_since_last_prune = 0;
}
TableType m_table;
size_t m_mutations_since_last_prune { 0 };
};
}