LibJS: Account keyed collection storage as external memory

Report Map, WeakMap, and WeakSet entry storage through the GC
external memory hook.

Replace the public values() accessors on WeakMap and WeakSet with
explicit methods so all mutations update external memory accounting.
This commit is contained in:
Andreas Kling 2026-05-06 11:33:55 +02:00 committed by Andreas Kling
parent c934ebd4a2
commit ccf178a0e7
10 changed files with 148 additions and 34 deletions

View file

@ -339,7 +339,7 @@ ErrorOr<void> print_set(JS::PrintContext& print_context, JS::Set const& set, Has
ErrorOr<void> print_weak_map(JS::PrintContext& print_context, JS::WeakMap const& weak_map, HashTable<JS::Object*>&)
{
TRY(print_type(print_context, "WeakMap"sv));
TRY(js_out(print_context, " ({})", weak_map.values().size()));
TRY(js_out(print_context, " ({})", weak_map.weak_map_size()));
// Note: We could tell you what's actually inside, but not in insertion order.
return {};
}
@ -347,7 +347,7 @@ ErrorOr<void> print_weak_map(JS::PrintContext& print_context, JS::WeakMap const&
ErrorOr<void> print_weak_set(JS::PrintContext& print_context, JS::WeakSet const& weak_set, HashTable<JS::Object*>&)
{
TRY(print_type(print_context, "WeakSet"sv));
TRY(js_out(print_context, " ({})", weak_set.values().size()));
TRY(js_out(print_context, " ({})", weak_set.weak_set_size()));
// Note: We could tell you what's actually inside, but not in insertion order.
return {};
}

View file

@ -4,6 +4,7 @@
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibJS/Runtime/ExternalMemory.h>
#include <LibJS/Runtime/Map.h>
namespace JS {
@ -23,8 +24,10 @@ Map::Map(Object& prototype)
// 24.1.3.1 Map.prototype.clear ( ), https://tc39.es/ecma262/#sec-map.prototype.clear
void Map::map_clear()
{
auto old_external_memory_size = external_memory_size();
m_keys.clear();
m_entries.clear();
account_external_memory_change(old_external_memory_size);
}
// 24.1.3.3 Map.prototype.delete ( key ), https://tc39.es/ecma262/#sec-map.prototype.delete
@ -42,8 +45,10 @@ bool Map::map_remove(Value const& key)
if (!index.has_value())
return false;
auto old_external_memory_size = external_memory_size();
m_keys.remove(*index);
m_entries.remove(key);
account_external_memory_change(old_external_memory_size);
return true;
}
@ -68,9 +73,11 @@ void Map::map_set(Value const& key, Value value)
if (it != m_entries.end()) {
it->value = value;
} else {
auto old_external_memory_size = external_memory_size();
auto index = m_next_insertion_id++;
m_keys.insert(index, key);
m_entries.set(key, value);
account_external_memory_change(old_external_memory_size);
}
}
@ -79,6 +86,31 @@ size_t Map::map_size() const
return m_keys.size();
}
static size_t map_key_tree_external_memory_size(RedBlackTree<size_t, Value> const& keys)
{
constexpr auto approximate_node_size = sizeof(AK::BaseRedBlackTree<size_t>::Node) + sizeof(Value);
if (keys.size() > NumericLimits<size_t>::max() / approximate_node_size)
return NumericLimits<size_t>::max();
return keys.size() * approximate_node_size;
}
size_t Map::external_memory_size() const
{
auto size = Object::external_memory_size();
size = saturating_add_external_memory_size(size, map_key_tree_external_memory_size(m_keys));
size = saturating_add_external_memory_size(size, hash_map_external_memory_size(m_entries));
return size;
}
void Map::account_external_memory_change(size_t old_external_memory_size)
{
auto new_external_memory_size = external_memory_size();
if (new_external_memory_size > old_external_memory_size)
heap().did_allocate_external_memory(new_external_memory_size - old_external_memory_size);
else if (old_external_memory_size > new_external_memory_size)
heap().did_free_external_memory(old_external_memory_size - new_external_memory_size);
}
void Map::visit_edges(Cell::Visitor& visitor)
{
Base::visit_edges(visitor);

View file

@ -34,6 +34,8 @@ public:
void map_set(Value const&, Value);
size_t map_size() const;
virtual size_t external_memory_size() const override;
struct EndIterator {
};
@ -118,6 +120,8 @@ private:
explicit Map(Object& prototype);
virtual void visit_edges(Visitor& visitor) override;
void account_external_memory_change(size_t old_external_memory_size);
size_t m_next_insertion_id { 0 };
RedBlackTree<size_t, Value> m_keys;
HashMap<Value, Value, ValueTraits> m_entries;

View file

@ -4,6 +4,7 @@
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibJS/Runtime/ExternalMemory.h>
#include <LibJS/Runtime/WeakMap.h>
namespace JS {
@ -21,6 +22,34 @@ WeakMap::WeakMap(Object& prototype)
{
}
Optional<Value> WeakMap::weak_map_get(GC::Ptr<Cell> key) const
{
if (auto it = m_values.find(key); it != m_values.end())
return it->value;
return {};
}
bool WeakMap::weak_map_has(GC::Ptr<Cell> key) const
{
return m_values.contains(key);
}
void WeakMap::weak_map_set(GC::Ptr<Cell> key, Value value)
{
auto old_external_memory_size = external_memory_size();
m_values.set(key, value);
account_external_memory_change(old_external_memory_size);
}
bool WeakMap::weak_map_remove(GC::Ptr<Cell> key)
{
auto old_external_memory_size = external_memory_size();
auto did_remove = m_values.remove(key);
if (did_remove)
account_external_memory_change(old_external_memory_size);
return did_remove;
}
void WeakMap::remove_dead_cells(Badge<GC::Heap>)
{
m_values.remove_all_matching([](Cell* key, Value) {
@ -28,6 +57,20 @@ void WeakMap::remove_dead_cells(Badge<GC::Heap>)
});
}
size_t WeakMap::external_memory_size() const
{
return saturating_add_external_memory_size(Object::external_memory_size(), hash_map_external_memory_size(m_values));
}
void WeakMap::account_external_memory_change(size_t old_external_memory_size)
{
auto new_external_memory_size = external_memory_size();
if (new_external_memory_size > old_external_memory_size)
heap().did_allocate_external_memory(new_external_memory_size - old_external_memory_size);
else if (old_external_memory_size > new_external_memory_size)
heap().did_free_external_memory(old_external_memory_size - new_external_memory_size);
}
void WeakMap::visit_edges(Visitor& visitor)
{
Base::visit_edges(visitor);

View file

@ -25,10 +25,14 @@ public:
virtual ~WeakMap() override = default;
HashMap<GC::Ptr<Cell>, Value> const& values() const { return m_values; }
HashMap<GC::Ptr<Cell>, Value>& values() { return m_values; }
Optional<Value> weak_map_get(GC::Ptr<Cell>) const;
bool weak_map_has(GC::Ptr<Cell>) const;
void weak_map_set(GC::Ptr<Cell>, Value);
bool weak_map_remove(GC::Ptr<Cell>);
size_t weak_map_size() const { return m_values.size(); }
virtual void remove_dead_cells(Badge<GC::Heap>) override;
virtual size_t external_memory_size() const override;
private:
explicit WeakMap(Object& prototype);
@ -36,6 +40,8 @@ private:
virtual bool is_weak_map() const final { return true; }
void visit_edges(Visitor&) override;
void account_external_memory_change(size_t old_external_memory_size);
HashMap<GC::Ptr<Cell>, Value> m_values; // This stores Cell pointers instead of Object pointers to aide with sweeping
};

View file

@ -4,7 +4,6 @@
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/HashTable.h>
#include <AK/TypeCasts.h>
#include <LibJS/Runtime/AbstractOperations.h>
#include <LibJS/Runtime/WeakMapPrototype.h>
@ -54,7 +53,7 @@ JS_DEFINE_NATIVE_FUNCTION(WeakMapPrototype::delete_)
// ii. Set p.[[Value]] to empty.
// iii. Return true.
// 5. Return false.
return Value(weak_map->values().remove(&key.as_cell()));
return Value(weak_map->weak_map_remove(&key.as_cell()));
}
// 24.3.3.3 WeakMap.prototype.get ( key ), https://tc39.es/ecma262/#sec-weakmap.prototype.get
@ -72,10 +71,8 @@ JS_DEFINE_NATIVE_FUNCTION(WeakMapPrototype::get)
// 4. For each Record { [[Key]], [[Value]] } p of M.[[WeakMapData]], do
// a. If p.[[Key]] is not empty and SameValue(p.[[Key]], key) is true, return p.[[Value]].
auto& values = weak_map->values();
auto result = values.find(&key.as_cell());
if (result != values.end())
return result->value;
if (auto result = weak_map->weak_map_get(&key.as_cell()); result.has_value())
return *result;
// 5. Return undefined.
return js_undefined();
@ -95,17 +92,15 @@ JS_DEFINE_NATIVE_FUNCTION(WeakMapPrototype::get_or_insert)
if (!can_be_held_weakly(key))
return vm.throw_completion<TypeError>(ErrorType::CannotBeHeldWeakly, key);
auto& values = weak_map->values();
// 4. For each Record { [[Key]], [[Value]] } p of M.[[WeakMapData]], do
if (auto result = values.find(&key.as_cell()); result != values.end()) {
if (auto result = weak_map->weak_map_get(&key.as_cell()); result.has_value()) {
// a. If p.[[Key]] is not empty and SameValue(p.[[Key]], key) is true, return p.[[Value]].
return result->value;
return *result;
}
// 5. Let p be the Record { [[Key]]: key, [[Value]]: value }.
// 6. Append p to M.[[WeakMapData]].
values.set(&key.as_cell(), value);
weak_map->weak_map_set(&key.as_cell(), value);
// 7. Return value.
return value;
@ -129,12 +124,10 @@ JS_DEFINE_NATIVE_FUNCTION(WeakMapPrototype::get_or_insert_computed)
if (!callback.is_function())
return vm.throw_completion<TypeError>(ErrorType::NotAFunction, callback);
auto& values = weak_map->values();
// 5. For each Record { [[Key]], [[Value]] } p of M.[[WeakMapData]], do
if (auto result = values.find(&key.as_cell()); result != values.end()) {
if (auto result = weak_map->weak_map_get(&key.as_cell()); result.has_value()) {
// a. If p.[[Key]] is not empty and SameValue(p.[[Key]], key) is true, return p.[[Value]].
return result->value;
return *result;
}
// 6. Let value be ? Call(callback, undefined, « key »).
@ -148,7 +141,7 @@ JS_DEFINE_NATIVE_FUNCTION(WeakMapPrototype::get_or_insert_computed)
// ii. Return value.
// 9. Let p be the Record { [[Key]]: key, [[Value]]: value }.
// 10. Append p to M.[[WeakMapData]].
values.set(&key.as_cell(), value);
weak_map->weak_map_set(&key.as_cell(), value);
// 11. Return value.
return value;
@ -169,9 +162,7 @@ JS_DEFINE_NATIVE_FUNCTION(WeakMapPrototype::has)
// 4. For each Record { [[Key]], [[Value]] } p of M.[[WeakMapData]], do
// a. If p.[[Key]] is not empty and SameValue(p.[[Key]], key) is true, return true.
auto& values = weak_map->values();
auto result = values.find(&key.as_cell());
if (result != values.end())
if (weak_map->weak_map_has(&key.as_cell()))
return Value(true);
// 5. Return false.
@ -198,7 +189,7 @@ JS_DEFINE_NATIVE_FUNCTION(WeakMapPrototype::set)
// ii. Return M.
// 5. Let p be the Record { [[Key]]: key, [[Value]]: value }.
// 6. Append p to M.[[WeakMapData]].
weak_map->values().set(&key.as_cell(), value);
weak_map->weak_map_set(&key.as_cell(), value);
// 7. Return M.
return weak_map;

View file

@ -4,6 +4,7 @@
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibJS/Runtime/ExternalMemory.h>
#include <LibJS/Runtime/WeakSet.h>
namespace JS {
@ -21,6 +22,27 @@ WeakSet::WeakSet(Object& prototype)
{
}
bool WeakSet::weak_set_has(GC::Ptr<Cell> value) const
{
return m_values.contains(value);
}
void WeakSet::weak_set_add(GC::Ptr<Cell> value)
{
auto old_external_memory_size = external_memory_size();
m_values.set(value, AK::HashSetExistingEntryBehavior::Keep);
account_external_memory_change(old_external_memory_size);
}
bool WeakSet::weak_set_remove(GC::Ptr<Cell> value)
{
auto old_external_memory_size = external_memory_size();
auto did_remove = m_values.remove(value);
if (did_remove)
account_external_memory_change(old_external_memory_size);
return did_remove;
}
void WeakSet::remove_dead_cells(Badge<GC::Heap>)
{
m_values.remove_all_matching([](Cell* cell) {
@ -28,4 +50,18 @@ void WeakSet::remove_dead_cells(Badge<GC::Heap>)
});
}
size_t WeakSet::external_memory_size() const
{
return saturating_add_external_memory_size(Object::external_memory_size(), hash_table_external_memory_size(m_values));
}
void WeakSet::account_external_memory_change(size_t old_external_memory_size)
{
auto new_external_memory_size = external_memory_size();
if (new_external_memory_size > old_external_memory_size)
heap().did_allocate_external_memory(new_external_memory_size - old_external_memory_size);
else if (old_external_memory_size > new_external_memory_size)
heap().did_free_external_memory(old_external_memory_size - new_external_memory_size);
}
}

View file

@ -25,14 +25,19 @@ public:
virtual ~WeakSet() override = default;
HashTable<GC::Ptr<Cell>> const& values() const { return m_values; }
HashTable<GC::Ptr<Cell>>& values() { return m_values; }
bool weak_set_has(GC::Ptr<Cell>) const;
void weak_set_add(GC::Ptr<Cell>);
bool weak_set_remove(GC::Ptr<Cell>);
size_t weak_set_size() const { return m_values.size(); }
virtual void remove_dead_cells(Badge<GC::Heap>) override;
virtual size_t external_memory_size() const override;
private:
explicit WeakSet(Object& prototype);
void account_external_memory_change(size_t old_external_memory_size);
HashTable<GC::RawPtr<Cell>> m_values; // This stores Cell pointers instead of Object pointers to aide with sweeping
};

View file

@ -4,7 +4,6 @@
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/HashTable.h>
#include <AK/TypeCasts.h>
#include <LibJS/Runtime/AbstractOperations.h>
#include <LibJS/Runtime/WeakSetPrototype.h>
@ -49,7 +48,7 @@ JS_DEFINE_NATIVE_FUNCTION(WeakSetPrototype::add)
// a. If e is not empty and SameValue(e, value) is true, then
// i. Return S.
// 5. Append value to S.[[WeakSetData]].
weak_set->values().set(&value.as_cell(), AK::HashSetExistingEntryBehavior::Keep);
weak_set->weak_set_add(&value.as_cell());
// 6. Return S.
return weak_set;
@ -73,7 +72,7 @@ JS_DEFINE_NATIVE_FUNCTION(WeakSetPrototype::delete_)
// i. Replace the element of S.[[WeakSetData]] whose value is e with an element whose value is empty.
// ii. Return true.
// 5. Return false.
return Value(weak_set->values().remove(&value.as_cell()));
return Value(weak_set->weak_set_remove(&value.as_cell()));
}
// 24.4.3.4 WeakSet.prototype.has ( value ), https://tc39.es/ecma262/#sec-weakset.prototype.has
@ -91,9 +90,7 @@ JS_DEFINE_NATIVE_FUNCTION(WeakSetPrototype::has)
// 4. For each element e of S.[[WeakSetData]], do
// a. If e is not empty and SameValue(e, value) is true, return true.
auto& values = weak_set->values();
auto result = values.find(&value.as_cell());
if (result != values.end())
if (weak_set->weak_set_has(&value.as_cell()))
return Value(true);
// 5. Return false.

View file

@ -58,7 +58,7 @@ TESTJS_GLOBAL_FUNCTION(get_weak_set_size, getWeakSetSize)
if (!is<JS::WeakSet>(*object))
return vm.throw_completion<JS::TypeError>(JS::ErrorType::NotAnObjectOfType, "WeakSet");
auto& weak_set = static_cast<JS::WeakSet&>(*object);
return JS::Value(weak_set.values().size());
return JS::Value(weak_set.weak_set_size());
}
TESTJS_GLOBAL_FUNCTION(get_weak_map_size, getWeakMapSize)
@ -67,7 +67,7 @@ TESTJS_GLOBAL_FUNCTION(get_weak_map_size, getWeakMapSize)
if (!is<JS::WeakMap>(*object))
return vm.throw_completion<JS::TypeError>(JS::ErrorType::NotAnObjectOfType, "WeakMap");
auto& weak_map = static_cast<JS::WeakMap&>(*object);
return JS::Value(weak_map.values().size());
return JS::Value(weak_map.weak_map_size());
}
TESTJS_GLOBAL_FUNCTION(mark_as_garbage, markAsGarbage)