mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2026-06-25 02:40:25 +00:00
Descriptor arrays and dictionary tables now carry the complete property metadata for Shape lookup and enumeration, so Shape no longer needs to retain the transition payload that used to rebuild the lazy property table. Remove the previous-shape pointer, transition property key, transition attributes, and transition type. Keep only a constructor-time property count change so put, configure, delete, and prototype transitions still initialize their visible property count explicitly. This reduces Shape from 104 to 88 bytes.
496 lines
17 KiB
C++
496 lines
17 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;
|
|
|
|
void Shape::finalize()
|
|
{
|
|
Base::finalize();
|
|
if (m_dictionary)
|
|
m_property_storage.property_table.~PropertyTablePtr();
|
|
}
|
|
|
|
GC::Ptr<DescriptorArray> Shape::descriptors() const
|
|
{
|
|
VERIFY(!m_dictionary);
|
|
return m_property_storage.descriptors;
|
|
}
|
|
|
|
void Shape::set_descriptors(GC::Ptr<DescriptorArray> descriptors)
|
|
{
|
|
VERIFY(!m_dictionary);
|
|
m_property_storage.descriptors = descriptors;
|
|
}
|
|
|
|
Shape::PropertyTablePtr& Shape::property_table() const
|
|
{
|
|
VERIFY(m_dictionary);
|
|
return m_property_storage.property_table;
|
|
}
|
|
|
|
void Shape::become_dictionary_shape()
|
|
{
|
|
VERIFY(!m_dictionary);
|
|
new (&m_property_storage.property_table) PropertyTablePtr();
|
|
m_dictionary = true;
|
|
}
|
|
|
|
size_t Shape::external_memory_size() const
|
|
{
|
|
size_t size = 0;
|
|
if (m_dictionary && property_table())
|
|
size += hash_map_external_memory_size(*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->become_dictionary_shape();
|
|
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, PropertyCountChange::Increment);
|
|
|
|
if (descriptors() && descriptors()->size() == m_property_count) {
|
|
descriptors()->set(property_key, { m_property_count, attributes }, m_property_count);
|
|
new_shape->set_descriptors(descriptors());
|
|
} else {
|
|
new_shape->set_descriptors(copy_descriptors());
|
|
new_shape->descriptors()->set(property_key, { m_property_count, attributes }, 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, PropertyCountChange::Preserve);
|
|
new_shape->set_descriptors(copy_descriptors());
|
|
new_shape->descriptors()->set_attributes(property_key, attributes, 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->become_dictionary_shape();
|
|
copy_properties_to_dictionary_shape(*new_shape);
|
|
} else if (m_dictionary) {
|
|
new_shape->set_descriptors(copy_descriptors());
|
|
} else {
|
|
new_shape->set_descriptors(descriptors());
|
|
}
|
|
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, PropertyCountChange property_count_change)
|
|
: m_has_parameter_map(previous_shape.m_has_parameter_map)
|
|
, m_realm(previous_shape.m_realm)
|
|
, m_prototype(previous_shape.m_prototype)
|
|
, m_property_count(previous_shape.m_property_count)
|
|
{
|
|
switch (property_count_change) {
|
|
case PropertyCountChange::Preserve:
|
|
break;
|
|
case PropertyCountChange::Increment:
|
|
VERIFY(m_property_count < NumericLimits<u32>::max());
|
|
++m_property_count;
|
|
break;
|
|
case PropertyCountChange::Decrement:
|
|
VERIFY(m_property_count > 0);
|
|
--m_property_count;
|
|
break;
|
|
}
|
|
}
|
|
|
|
Shape::Shape(Shape& previous_shape, Object* new_prototype)
|
|
: m_has_parameter_map(previous_shape.m_has_parameter_map)
|
|
, m_realm(previous_shape.m_realm)
|
|
, m_prototype(new_prototype)
|
|
, m_property_count(previous_shape.m_property_count)
|
|
{
|
|
}
|
|
|
|
void Shape::visit_edges(Cell::Visitor& visitor)
|
|
{
|
|
Base::visit_edges(visitor);
|
|
visitor.visit(m_realm);
|
|
if (!m_dictionary)
|
|
visitor.visit(m_property_storage.descriptors);
|
|
visitor.visit(m_prototype);
|
|
|
|
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);
|
|
|
|
// Descriptor arrays mark their own keys; dictionary tables are not cells, so Shape marks their keys directly.
|
|
if (m_dictionary && property_table()) {
|
|
for (auto& it : *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 = property_table()->get(property_key);
|
|
if (!property.has_value())
|
|
return {};
|
|
return property;
|
|
}
|
|
if (!descriptors())
|
|
return {};
|
|
return descriptors()->lookup(property_key, m_property_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] : *property_table())
|
|
callback(property_key, metadata);
|
|
return;
|
|
}
|
|
if (!descriptors())
|
|
return;
|
|
descriptors()->for_each_in_insertion_order(callback, m_property_count);
|
|
}
|
|
|
|
void Shape::ensure_property_table() const
|
|
{
|
|
VERIFY(m_dictionary);
|
|
if (property_table())
|
|
return;
|
|
property_table() = make<PropertyTable>();
|
|
}
|
|
|
|
void Shape::ensure_descriptor_array()
|
|
{
|
|
VERIFY(!m_dictionary);
|
|
if (descriptors())
|
|
return;
|
|
set_descriptors(heap().allocate<DescriptorArray>());
|
|
}
|
|
|
|
GC::Ref<DescriptorArray> Shape::copy_descriptors() const
|
|
{
|
|
VERIFY(m_property_count <= DescriptorArray::max_descriptor_count);
|
|
if (!m_dictionary && descriptors())
|
|
return heap().allocate<DescriptorArray>(*descriptors(), m_property_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.property_table()->set(property_key, metadata);
|
|
});
|
|
shape.m_property_count = shape.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, PropertyCountChange::Decrement);
|
|
new_shape->set_descriptors(copy_descriptors());
|
|
new_shape->descriptors()->remove(property_key, 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 (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 (!descriptors()->lookup(property_key, m_property_count).has_value()) {
|
|
VERIFY(m_property_count < NumericLimits<u32>::max());
|
|
descriptors()->set(property_key, { m_property_count, attributes }, m_property_count);
|
|
++m_property_count;
|
|
++m_dictionary_generation;
|
|
return;
|
|
}
|
|
descriptors()->set(property_key, { m_property_count, attributes }, m_property_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(property_table());
|
|
auto it = property_table()->find(property_key);
|
|
VERIFY(it != property_table()->end());
|
|
it->value.attributes = attributes;
|
|
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(property_table());
|
|
if (property_table()->remove(property_key))
|
|
--m_property_count;
|
|
for (auto& it : *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->become_dictionary_shape();
|
|
copy_properties_to_dictionary_shape(*new_shape);
|
|
} else {
|
|
new_shape->set_descriptors(copy_descriptors());
|
|
new_shape->m_property_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>();
|
|
}
|
|
}
|
|
|
|
}
|