mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2026-06-25 02:40:25 +00:00
Replace the lazy per-shape OrderedHashMap cache for non-dictionary shapes with a GC-allocated descriptor array. Store descriptors in hash order for lookup while keeping an enum index so callers can still walk properties in insertion order. Keep dictionary shapes on the mutable OrderedHashMap path, and migrate callers that enumerated Shape::property_table() to the new insertion order iterator. Cap descriptor arrays to their compact u16 index range and keep larger dictionary shapes on the mutable table path across prototype transitions and prototype clones. Add coverage for setting the prototype of a dictionary object with more than 65536 named properties.
487 lines
18 KiB
C++
487 lines
18 KiB
C++
/*
|
|
* Copyright (c) 2020-2024, Andreas Kling <andreas@ladybird.org>
|
|
* Copyright (c) 2025, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
|
|
*
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*/
|
|
|
|
#include <LibGC/DeferGC.h>
|
|
#include <LibJS/Runtime/DescriptorArray.h>
|
|
#include <LibJS/Runtime/ExternalMemory.h>
|
|
#include <LibJS/Runtime/Realm.h>
|
|
#include <LibJS/Runtime/Shape.h>
|
|
#include <LibJS/Runtime/VM.h>
|
|
|
|
namespace JS {
|
|
|
|
GC_DEFINE_ALLOCATOR(Shape);
|
|
GC_DEFINE_ALLOCATOR(PrototypeChainValidity);
|
|
|
|
Shape::~Shape() = default;
|
|
|
|
size_t Shape::external_memory_size() const
|
|
{
|
|
size_t size = 0;
|
|
if (m_dictionary && m_property_table)
|
|
size += hash_map_external_memory_size(*m_property_table);
|
|
if (m_forward_transitions)
|
|
size += hash_map_external_memory_size(*m_forward_transitions);
|
|
if (m_prototype_transitions)
|
|
size += hash_map_external_memory_size(*m_prototype_transitions);
|
|
if (m_delete_transitions)
|
|
size += hash_map_external_memory_size(*m_delete_transitions);
|
|
if (m_child_prototype_shapes)
|
|
size += vector_external_memory_size(*m_child_prototype_shapes);
|
|
return size;
|
|
}
|
|
|
|
GC::Ref<Shape> Shape::create_dictionary_transition()
|
|
{
|
|
auto new_shape = heap().allocate<Shape>(m_realm);
|
|
new_shape->m_dictionary = true;
|
|
new_shape->m_has_parameter_map = m_has_parameter_map;
|
|
new_shape->m_prototype = m_prototype;
|
|
invalidate_prototype_if_needed_for_new_prototype(new_shape);
|
|
copy_properties_to_dictionary_shape(*new_shape);
|
|
return new_shape;
|
|
}
|
|
|
|
GC::Ptr<Shape> Shape::get_or_prune_cached_forward_transition(TransitionKey const& key)
|
|
{
|
|
if (m_is_prototype_shape)
|
|
return nullptr;
|
|
if (!m_forward_transitions)
|
|
return nullptr;
|
|
auto it = m_forward_transitions->find(key);
|
|
if (it == m_forward_transitions->end())
|
|
return nullptr;
|
|
if (!it->value) {
|
|
// The cached forward transition has gone stale (from garbage collection). Prune it.
|
|
m_forward_transitions->remove(it);
|
|
return nullptr;
|
|
}
|
|
return it->value.ptr();
|
|
}
|
|
|
|
GC::Ptr<Shape> Shape::get_or_prune_cached_delete_transition(PropertyKey const& key)
|
|
{
|
|
if (m_is_prototype_shape)
|
|
return nullptr;
|
|
if (!m_delete_transitions)
|
|
return nullptr;
|
|
auto it = m_delete_transitions->find(key);
|
|
if (it == m_delete_transitions->end())
|
|
return nullptr;
|
|
if (!it->value) {
|
|
// The cached delete transition has gone stale (from garbage collection). Prune it.
|
|
m_delete_transitions->remove(it);
|
|
return nullptr;
|
|
}
|
|
return it->value.ptr();
|
|
}
|
|
|
|
GC::Ptr<Shape> Shape::get_or_prune_cached_prototype_transition(Object* prototype)
|
|
{
|
|
if (m_is_prototype_shape)
|
|
return nullptr;
|
|
if (!m_prototype_transitions)
|
|
return nullptr;
|
|
auto it = m_prototype_transitions->find(prototype);
|
|
if (it == m_prototype_transitions->end())
|
|
return nullptr;
|
|
if (!it->value) {
|
|
// The cached prototype transition has gone stale (from garbage collection). Prune it.
|
|
m_prototype_transitions->remove(it);
|
|
return nullptr;
|
|
}
|
|
return it->value.ptr();
|
|
}
|
|
|
|
GC::Ref<Shape> Shape::create_put_transition(PropertyKey const& property_key, PropertyAttributes attributes)
|
|
{
|
|
TransitionKey key { property_key, attributes };
|
|
if (auto existing_shape = get_or_prune_cached_forward_transition(key))
|
|
return *existing_shape;
|
|
auto new_shape = heap().allocate<Shape>(*this, property_key, attributes, TransitionType::Put);
|
|
new_shape->m_descriptors = copy_descriptors();
|
|
new_shape->m_descriptors->set(property_key, { m_property_count, attributes }, m_property_count);
|
|
new_shape->m_own_descriptor_count = new_shape->m_property_count;
|
|
invalidate_prototype_if_needed_for_new_prototype(new_shape);
|
|
if (!m_is_prototype_shape) {
|
|
if (!m_forward_transitions)
|
|
m_forward_transitions = make<HashMap<TransitionKey, GC::Weak<Shape>>>();
|
|
m_forward_transitions->set(key, new_shape);
|
|
}
|
|
return new_shape;
|
|
}
|
|
|
|
GC::Ref<Shape> Shape::create_configure_transition(PropertyKey const& property_key, PropertyAttributes attributes)
|
|
{
|
|
TransitionKey key { property_key, attributes };
|
|
if (auto existing_shape = get_or_prune_cached_forward_transition(key))
|
|
return *existing_shape;
|
|
auto new_shape = heap().allocate<Shape>(*this, property_key, attributes, TransitionType::Configure);
|
|
new_shape->m_descriptors = copy_descriptors();
|
|
new_shape->m_descriptors->set_attributes(property_key, attributes, m_own_descriptor_count);
|
|
new_shape->m_own_descriptor_count = new_shape->m_property_count;
|
|
invalidate_prototype_if_needed_for_new_prototype(new_shape);
|
|
if (!m_is_prototype_shape) {
|
|
if (!m_forward_transitions)
|
|
m_forward_transitions = make<HashMap<TransitionKey, GC::Weak<Shape>>>();
|
|
m_forward_transitions->set(key, new_shape.ptr());
|
|
}
|
|
return new_shape;
|
|
}
|
|
|
|
GC::Ref<Shape> Shape::create_prototype_transition(Object* new_prototype)
|
|
{
|
|
if (new_prototype)
|
|
new_prototype->convert_to_prototype_if_needed();
|
|
if (auto existing_shape = get_or_prune_cached_prototype_transition(new_prototype))
|
|
return *existing_shape;
|
|
auto new_shape = heap().allocate<Shape>(*this, new_prototype);
|
|
if (m_dictionary && m_property_count > DescriptorArray::max_descriptor_count) {
|
|
new_shape->m_dictionary = true;
|
|
copy_properties_to_dictionary_shape(*new_shape);
|
|
} else {
|
|
new_shape->m_descriptors = copy_descriptors();
|
|
new_shape->m_own_descriptor_count = new_shape->m_property_count;
|
|
}
|
|
invalidate_prototype_if_needed_for_new_prototype(new_shape);
|
|
if (!m_is_prototype_shape) {
|
|
if (!m_prototype_transitions)
|
|
m_prototype_transitions = make<HashMap<GC::Ptr<Object>, GC::Weak<Shape>>>();
|
|
m_prototype_transitions->set(new_prototype, new_shape.ptr());
|
|
}
|
|
return new_shape;
|
|
}
|
|
|
|
Shape::Shape(Realm& realm)
|
|
: m_realm(realm)
|
|
{
|
|
}
|
|
|
|
Shape::Shape(Shape& previous_shape, PropertyKey const& property_key, PropertyAttributes attributes, TransitionType transition_type)
|
|
: m_attributes(attributes)
|
|
, m_transition_type(transition_type)
|
|
, m_has_parameter_map(previous_shape.m_has_parameter_map)
|
|
, m_realm(previous_shape.m_realm)
|
|
, m_previous(&previous_shape)
|
|
, m_property_key(property_key)
|
|
, m_prototype(previous_shape.m_prototype)
|
|
, m_property_count(transition_type == TransitionType::Put ? previous_shape.m_property_count + 1 : previous_shape.m_property_count)
|
|
, m_own_descriptor_count(m_property_count)
|
|
{
|
|
}
|
|
|
|
Shape::Shape(Shape& previous_shape, PropertyKey const& property_key, TransitionType transition_type)
|
|
: m_transition_type(transition_type)
|
|
, m_has_parameter_map(previous_shape.m_has_parameter_map)
|
|
, m_realm(previous_shape.m_realm)
|
|
, m_previous(&previous_shape)
|
|
, m_property_key(property_key)
|
|
, m_prototype(previous_shape.m_prototype)
|
|
, m_property_count(previous_shape.m_property_count - 1)
|
|
, m_own_descriptor_count(m_property_count)
|
|
{
|
|
VERIFY(transition_type == TransitionType::Delete);
|
|
}
|
|
|
|
Shape::Shape(Shape& previous_shape, Object* new_prototype)
|
|
: m_transition_type(TransitionType::Prototype)
|
|
, m_has_parameter_map(previous_shape.m_has_parameter_map)
|
|
, m_realm(previous_shape.m_realm)
|
|
, m_previous(&previous_shape)
|
|
, m_prototype(new_prototype)
|
|
, m_property_count(previous_shape.m_property_count)
|
|
, m_own_descriptor_count(m_property_count)
|
|
{
|
|
}
|
|
|
|
void Shape::visit_edges(Cell::Visitor& visitor)
|
|
{
|
|
Base::visit_edges(visitor);
|
|
visitor.visit(m_realm);
|
|
visitor.visit(m_descriptors);
|
|
visitor.visit(m_prototype);
|
|
visitor.visit(m_previous);
|
|
if (m_property_key.has_value())
|
|
m_property_key->visit_edges(visitor);
|
|
|
|
visitor.ignore(m_prototype_transitions);
|
|
|
|
// Child prototype-shape weak refs need no marking; pruning is lazy.
|
|
visitor.ignore(m_child_prototype_shapes);
|
|
|
|
// FIXME: The forward transition keys should be weak, but we have to mark them for now in case they go stale.
|
|
if (m_forward_transitions) {
|
|
for (auto& it : *m_forward_transitions)
|
|
it.key.property_key.visit_edges(visitor);
|
|
}
|
|
|
|
// FIXME: The delete transition keys should be weak, but we have to mark them for now in case they go stale.
|
|
if (m_delete_transitions) {
|
|
for (auto& it : *m_delete_transitions)
|
|
it.key.visit_edges(visitor);
|
|
}
|
|
|
|
visitor.visit(m_prototype_chain_validity);
|
|
|
|
// Only dictionary shapes actually need us to mark the keys in m_property_table.
|
|
//
|
|
// For non-dictionary shapes, m_property_table is a lazily-built cache of the
|
|
// transition chain: every key it contains was originally inserted into some
|
|
// ancestor's m_property_key, and that ancestor is kept alive by m_previous
|
|
// (which we already visit above). So those keys are guaranteed to be marked
|
|
// transitively via the chain, and re-marking them here is pure overhead.
|
|
//
|
|
// The exception used to be the handful of intrinsic shapes populated via
|
|
// add_property_without_transition() in Intrinsics.cpp (iterator-result,
|
|
// function, arguments, regexp-exec-array, ...). Those shapes are not
|
|
// dictionaries and have no m_previous to reach their property keys through.
|
|
// However, every key they hold is a vm.names.* string or a well-known
|
|
// symbol, both of which are strongly rooted by the VM for its entire
|
|
// lifetime, so skipping them here is safe.
|
|
if (m_dictionary && m_property_table) {
|
|
for (auto& it : *m_property_table)
|
|
it.key.visit_edges(visitor);
|
|
}
|
|
}
|
|
|
|
Optional<PropertyMetadata> Shape::lookup(PropertyKey const& property_key) const
|
|
{
|
|
if (m_property_count == 0)
|
|
return {};
|
|
if (m_dictionary) {
|
|
ensure_property_table();
|
|
auto property = m_property_table->get(property_key);
|
|
if (!property.has_value())
|
|
return {};
|
|
return property;
|
|
}
|
|
if (!m_descriptors)
|
|
return {};
|
|
return m_descriptors->lookup(property_key, m_own_descriptor_count);
|
|
}
|
|
|
|
void Shape::for_each_property_in_insertion_order(Function<void(PropertyKey const&, PropertyMetadata const&)> const& callback) const
|
|
{
|
|
if (m_dictionary) {
|
|
ensure_property_table();
|
|
for (auto const& [property_key, metadata] : *m_property_table)
|
|
callback(property_key, metadata);
|
|
return;
|
|
}
|
|
if (!m_descriptors)
|
|
return;
|
|
m_descriptors->for_each_in_insertion_order(callback, m_own_descriptor_count);
|
|
}
|
|
|
|
void Shape::ensure_property_table() const
|
|
{
|
|
VERIFY(m_dictionary);
|
|
if (m_property_table)
|
|
return;
|
|
m_property_table = make<OrderedHashMap<PropertyKey, PropertyMetadata>>();
|
|
}
|
|
|
|
void Shape::ensure_descriptor_array()
|
|
{
|
|
VERIFY(!m_dictionary);
|
|
if (m_descriptors)
|
|
return;
|
|
m_descriptors = heap().allocate<DescriptorArray>();
|
|
}
|
|
|
|
GC::Ref<DescriptorArray> Shape::copy_descriptors() const
|
|
{
|
|
VERIFY(m_property_count <= DescriptorArray::max_descriptor_count);
|
|
if (!m_dictionary && m_descriptors)
|
|
return heap().allocate<DescriptorArray>(*m_descriptors, m_own_descriptor_count);
|
|
|
|
auto descriptors = heap().allocate<DescriptorArray>();
|
|
for_each_property_in_insertion_order([&](auto const& property_key, auto const& metadata) {
|
|
descriptors->set(property_key, metadata, descriptors->size());
|
|
});
|
|
return descriptors;
|
|
}
|
|
|
|
void Shape::copy_properties_to_dictionary_shape(Shape& shape) const
|
|
{
|
|
VERIFY(shape.m_dictionary);
|
|
shape.ensure_property_table();
|
|
for_each_property_in_insertion_order([&](auto const& property_key, auto const& metadata) {
|
|
shape.m_property_table->set(property_key, metadata);
|
|
});
|
|
shape.m_property_count = shape.m_property_table->size();
|
|
}
|
|
|
|
GC::Ref<Shape> Shape::create_delete_transition(PropertyKey const& property_key)
|
|
{
|
|
if (auto existing_shape = get_or_prune_cached_delete_transition(property_key))
|
|
return *existing_shape;
|
|
auto new_shape = heap().allocate<Shape>(*this, property_key, TransitionType::Delete);
|
|
new_shape->m_descriptors = copy_descriptors();
|
|
new_shape->m_descriptors->remove(property_key, m_own_descriptor_count);
|
|
new_shape->m_own_descriptor_count = new_shape->m_property_count;
|
|
invalidate_prototype_if_needed_for_new_prototype(new_shape);
|
|
if (!m_delete_transitions)
|
|
m_delete_transitions = make<HashMap<PropertyKey, GC::Weak<Shape>>>();
|
|
m_delete_transitions->set(property_key, new_shape.ptr());
|
|
return new_shape;
|
|
}
|
|
|
|
void Shape::add_property_without_transition(PropertyKey const& property_key, PropertyAttributes attributes)
|
|
{
|
|
invalidate_prototype_if_needed_for_change_without_transition();
|
|
if (m_dictionary) {
|
|
ensure_property_table();
|
|
if (m_property_table->set(property_key, { m_property_count, attributes }) == AK::HashSetResult::InsertedNewEntry) {
|
|
VERIFY(m_property_count < NumericLimits<u32>::max());
|
|
++m_property_count;
|
|
++m_dictionary_generation;
|
|
}
|
|
return;
|
|
}
|
|
|
|
ensure_descriptor_array();
|
|
if (!m_descriptors->lookup(property_key, m_own_descriptor_count).has_value()) {
|
|
VERIFY(m_property_count < NumericLimits<u32>::max());
|
|
m_descriptors->set(property_key, { m_property_count, attributes }, m_own_descriptor_count);
|
|
++m_property_count;
|
|
++m_own_descriptor_count;
|
|
++m_dictionary_generation;
|
|
return;
|
|
}
|
|
m_descriptors->set(property_key, { m_property_count, attributes }, m_own_descriptor_count);
|
|
}
|
|
|
|
void Shape::set_property_attributes_without_transition(PropertyKey const& property_key, PropertyAttributes attributes)
|
|
{
|
|
invalidate_prototype_if_needed_for_change_without_transition();
|
|
VERIFY(is_dictionary());
|
|
VERIFY(m_property_table);
|
|
auto it = m_property_table->find(property_key);
|
|
VERIFY(it != m_property_table->end());
|
|
it->value.attributes = attributes;
|
|
m_property_table->set(property_key, it->value);
|
|
++m_dictionary_generation;
|
|
}
|
|
|
|
void Shape::remove_property_without_transition(PropertyKey const& property_key, u32 offset)
|
|
{
|
|
invalidate_prototype_if_needed_for_change_without_transition();
|
|
VERIFY(is_dictionary());
|
|
VERIFY(m_property_table);
|
|
if (m_property_table->remove(property_key))
|
|
--m_property_count;
|
|
for (auto& it : *m_property_table) {
|
|
VERIFY(it.value.offset != offset);
|
|
if (it.value.offset > offset)
|
|
--it.value.offset;
|
|
}
|
|
++m_dictionary_generation;
|
|
}
|
|
|
|
GC::Ref<Shape> Shape::clone_for_prototype()
|
|
{
|
|
VERIFY(!m_is_prototype_shape);
|
|
VERIFY(!m_prototype_chain_validity);
|
|
auto new_shape = heap().allocate<Shape>(m_realm);
|
|
new_shape->m_is_prototype_shape = true;
|
|
new_shape->m_has_parameter_map = m_has_parameter_map;
|
|
new_shape->m_prototype = m_prototype;
|
|
if (m_dictionary && m_property_count > DescriptorArray::max_descriptor_count) {
|
|
new_shape->m_dictionary = true;
|
|
copy_properties_to_dictionary_shape(*new_shape);
|
|
} else {
|
|
new_shape->m_descriptors = copy_descriptors();
|
|
new_shape->m_property_count = m_property_count;
|
|
new_shape->m_own_descriptor_count = m_property_count;
|
|
}
|
|
new_shape->m_prototype_chain_validity = heap().allocate<PrototypeChainValidity>();
|
|
if (new_shape->m_prototype)
|
|
new_shape->m_prototype->shape().add_child_prototype_shape(*new_shape);
|
|
return new_shape;
|
|
}
|
|
|
|
void Shape::set_prototype_without_transition(Object* new_prototype)
|
|
{
|
|
VERIFY(new_prototype);
|
|
new_prototype->convert_to_prototype_if_needed();
|
|
m_prototype = new_prototype;
|
|
}
|
|
|
|
void Shape::set_prototype_shape()
|
|
{
|
|
VERIFY(!m_is_prototype_shape);
|
|
m_is_prototype_shape = true;
|
|
m_prototype_chain_validity = heap().allocate<PrototypeChainValidity>();
|
|
if (m_prototype)
|
|
m_prototype->shape().add_child_prototype_shape(*this);
|
|
}
|
|
|
|
void Shape::add_child_prototype_shape(GC::Ref<Shape> child)
|
|
{
|
|
VERIFY(m_is_prototype_shape);
|
|
VERIFY(child->m_is_prototype_shape);
|
|
if (!m_child_prototype_shapes)
|
|
m_child_prototype_shapes = make<Vector<GC::Weak<Shape>>>();
|
|
m_child_prototype_shapes->append(GC::Weak<Shape> { *child });
|
|
}
|
|
|
|
void Shape::invalidate_prototype_if_needed_for_new_prototype(GC::Ref<Shape> new_prototype_shape)
|
|
{
|
|
if (!m_is_prototype_shape)
|
|
return;
|
|
new_prototype_shape->set_prototype_shape();
|
|
m_prototype_chain_validity->set_valid(false);
|
|
|
|
invalidate_all_prototype_chains_leading_to_this();
|
|
|
|
// The owning object is keeping the same [[Prototype]], so its existing
|
|
// children descend from new_prototype_shape going forward.
|
|
new_prototype_shape->m_child_prototype_shapes = move(m_child_prototype_shapes);
|
|
}
|
|
|
|
void Shape::invalidate_prototype_if_needed_for_change_without_transition()
|
|
{
|
|
if (!m_is_prototype_shape)
|
|
return;
|
|
m_prototype_chain_validity->set_valid(false);
|
|
m_prototype_chain_validity = heap().allocate<PrototypeChainValidity>();
|
|
|
|
invalidate_all_prototype_chains_leading_to_this();
|
|
}
|
|
|
|
void Shape::invalidate_all_prototype_chains_leading_to_this()
|
|
{
|
|
if (!m_child_prototype_shapes || m_child_prototype_shapes->is_empty())
|
|
return;
|
|
|
|
HashTable<Shape*> shapes_to_invalidate;
|
|
Vector<Shape*> worklist;
|
|
auto enqueue_children_of = [&](Shape& shape) {
|
|
if (!shape.m_child_prototype_shapes)
|
|
return;
|
|
// Prune dead weak refs and enqueue the live ones in one pass.
|
|
shape.m_child_prototype_shapes->remove_all_matching([&](GC::Weak<Shape> const& weak) {
|
|
auto child = weak.ptr();
|
|
if (!child)
|
|
return true;
|
|
if (shapes_to_invalidate.set(child.ptr()) == HashSetResult::InsertedNewEntry)
|
|
worklist.append(child.ptr());
|
|
return false;
|
|
});
|
|
};
|
|
enqueue_children_of(*this);
|
|
while (!worklist.is_empty())
|
|
enqueue_children_of(*worklist.take_last());
|
|
|
|
for (auto* shape : shapes_to_invalidate) {
|
|
shape->m_prototype_chain_validity->set_valid(false);
|
|
shape->m_prototype_chain_validity = heap().allocate<PrototypeChainValidity>();
|
|
}
|
|
}
|
|
|
|
}
|