From bd6750aaa5d7cfed6565ae164dfd65de8bd4b4a0 Mon Sep 17 00:00:00 2001 From: Aliaksandr Kalenik Date: Thu, 22 May 2025 19:46:08 +0300 Subject: [PATCH] LibJS: Skip prototype chain lookup in `internal_set()` for arrays ...when Array.prototype and Object.prototype are intact. If `internal_set()` is called on an array exotic object with a numeric PropertyKey, and: - the prototype chain has not been modified (i.e., there are no getters or setters for indexed properties), and - the array is not the target of a Proxy object, then we can directly store the value in the receiver's indexed properties, without checking whether it already exists somewhere in the prototype chain. 1.7x improvement on the following program: ```js function f() { let a = []; let i = 0; while (i < 10_000_000) { a.push(i); i++; } } f(); ``` --- Libraries/LibJS/Runtime/Array.cpp | 77 ++++++++++++++++++- Libraries/LibJS/Runtime/Array.h | 9 ++- Libraries/LibJS/Runtime/ArrayPrototype.cpp | 2 +- Libraries/LibJS/Runtime/Intrinsics.cpp | 9 +++ Libraries/LibJS/Runtime/Intrinsics.h | 6 ++ Libraries/LibJS/Runtime/ProxyObject.cpp | 4 + .../builtins/Array/array-as-proxy-target.js | 26 +++++++ Libraries/LibWeb/WebIDL/ObservableArray.cpp | 6 +- Libraries/LibWeb/WebIDL/ObservableArray.h | 2 +- 9 files changed, 133 insertions(+), 8 deletions(-) create mode 100644 Libraries/LibJS/Tests/builtins/Array/array-as-proxy-target.js diff --git a/Libraries/LibJS/Runtime/Array.cpp b/Libraries/LibJS/Runtime/Array.cpp index 9fced8923c..4cb1042940 100644 --- a/Libraries/LibJS/Runtime/Array.cpp +++ b/Libraries/LibJS/Runtime/Array.cpp @@ -35,7 +35,7 @@ ThrowCompletionOr> Array::create(Realm& realm, u64 length, Object // 3. Let A be MakeBasicObject(« [[Prototype]], [[Extensible]] »). // 4. Set A.[[Prototype]] to proto. // 5. Set A.[[DefineOwnProperty]] as specified in 10.4.2.1. - auto array = realm.create(*prototype); + auto array = realm.create(realm, *prototype); // 6. Perform ! OrdinaryDefineOwnProperty(A, "length", PropertyDescriptor { [[Value]]: 𝔽(length), [[Writable]]: true, [[Enumerable]]: false, [[Configurable]]: false }). MUST(array->internal_define_own_property(vm.names.length, { .value = Value(length), .writable = true, .enumerable = false, .configurable = false })); @@ -63,12 +63,19 @@ GC::Ref Array::create_from(Realm& realm, ReadonlySpan elements) return array; } -Array::Array(Object& prototype) +Array::Array(Realm& realm, Object& prototype) : Object(ConstructWithPrototypeTag::Tag, prototype) + , m_realm(realm) { m_has_magical_length_property = true; } +void Array::visit_edges(Cell::Visitor& visitor) +{ + Base::visit_edges(visitor); + visitor.visit(m_realm); +} + // 10.4.2.4 ArraySetLength ( A, Desc ), https://tc39.es/ecma262/#sec-arraysetlength ThrowCompletionOr Array::set_length(PropertyDescriptor const& property_descriptor) { @@ -276,6 +283,72 @@ ThrowCompletionOr> Array::internal_get_own_property return Object::internal_get_own_property(property_key); } +ThrowCompletionOr Array::internal_set(PropertyKey const& property_key, Value value, Value receiver, CacheablePropertyMetadata* cacheable_metadata, PropertyLookupPhase phase) +{ + auto& vm = this->vm(); + + auto default_prototype_chain_intact = [&] { + auto const& intrinsics = m_realm->intrinsics(); + auto* array_prototype = shape().prototype(); + if (!array_prototype) + return false; + if (!array_prototype->indexed_properties().is_empty()) + return false; + auto& array_prototype_shape = shape().prototype()->shape(); + if (intrinsics.default_array_prototype_shape().ptr() != &array_prototype_shape) + return false; + + auto* object_prototype = array_prototype_shape.prototype(); + if (!object_prototype) + return false; + if (!object_prototype->indexed_properties().is_empty()) + return false; + auto& object_prototype_shape = array_prototype_shape.prototype()->shape(); + if (intrinsics.default_object_prototype_shape().ptr() != &object_prototype_shape) + return false; + if (object_prototype_shape.prototype()) + return false; + + return true; + }; + + VERIFY(receiver.is_object()); + auto& receiver_object = receiver.as_object(); + + // Fast path for arrays with intact prototype chain + if (&receiver_object == this && !m_is_proxy_target && default_prototype_chain_intact()) { + if (property_key.is_number()) { + auto index = property_key.as_number(); + auto property_descriptor = TRY(internal_get_own_property(property_key)); + if (!property_descriptor.has_value()) { + if (!TRY(is_extensible())) + return false; + PropertyAttributes attributes; + attributes.set_writable(true); + attributes.set_enumerable(true); + attributes.set_configurable(true); + indexed_properties().put(index, value, attributes); + return true; + } + if (property_descriptor->is_data_descriptor()) { + if (property_descriptor->writable.has_value() && !*property_descriptor->writable) + return false; + auto attributes = property_descriptor->attributes(); + indexed_properties().put(index, value, attributes); + return true; + } + } else if (property_key == vm.names.length) { + auto property_descriptor = TRY(internal_get_own_property(property_key)); + if (property_descriptor->writable.has_value() && !*property_descriptor->writable) + return false; + property_descriptor->value = value; + return TRY(set_length(*property_descriptor)); + } + } + + return Object::internal_set(property_key, value, receiver, cacheable_metadata, phase); +} + // 10.4.2.1 [[DefineOwnProperty]] ( P, Desc ), https://tc39.es/ecma262/#sec-array-exotic-objects-defineownproperty-p-desc ThrowCompletionOr Array::internal_define_own_property(PropertyKey const& property_key, PropertyDescriptor const& property_descriptor, Optional* precomputed_get_own_property) { diff --git a/Libraries/LibJS/Runtime/Array.h b/Libraries/LibJS/Runtime/Array.h index e30df2335e..cf93627270 100644 --- a/Libraries/LibJS/Runtime/Array.h +++ b/Libraries/LibJS/Runtime/Array.h @@ -48,21 +48,28 @@ public: virtual ~Array() override = default; virtual ThrowCompletionOr> internal_get_own_property(PropertyKey const&) const override final; + virtual ThrowCompletionOr internal_set(PropertyKey const&, Value value, Value receiver, CacheablePropertyMetadata*, PropertyLookupPhase) override; virtual ThrowCompletionOr internal_define_own_property(PropertyKey const&, PropertyDescriptor const&, Optional* precomputed_get_own_property = nullptr) override final; virtual ThrowCompletionOr internal_delete(PropertyKey const&) override; virtual ThrowCompletionOr> internal_own_property_keys() const override final; [[nodiscard]] bool length_is_writable() const { return m_length_writable; } + void set_is_proxy_target(bool is_proxy_target) { m_is_proxy_target = is_proxy_target; } + + virtual void visit_edges(Cell::Visitor& visitor) override; + protected: - explicit Array(Object& prototype); + explicit Array(Realm& realm, Object& prototype); private: virtual bool is_array_exotic_object() const final { return true; } ThrowCompletionOr set_length(PropertyDescriptor const&); + GC::Ref m_realm; bool m_length_writable { true }; + bool m_is_proxy_target { false }; }; template<> diff --git a/Libraries/LibJS/Runtime/ArrayPrototype.cpp b/Libraries/LibJS/Runtime/ArrayPrototype.cpp index defc5df7f4..f174886d40 100644 --- a/Libraries/LibJS/Runtime/ArrayPrototype.cpp +++ b/Libraries/LibJS/Runtime/ArrayPrototype.cpp @@ -32,7 +32,7 @@ GC_DEFINE_ALLOCATOR(ArrayPrototype); static HashTable> s_array_join_seen_objects; ArrayPrototype::ArrayPrototype(Realm& realm) - : Array(realm.intrinsics().object_prototype()) + : Array(realm, realm.intrinsics().object_prototype()) { } diff --git a/Libraries/LibJS/Runtime/Intrinsics.cpp b/Libraries/LibJS/Runtime/Intrinsics.cpp index 5e3ed95600..885de9e2ae 100644 --- a/Libraries/LibJS/Runtime/Intrinsics.cpp +++ b/Libraries/LibJS/Runtime/Intrinsics.cpp @@ -316,6 +316,13 @@ void Intrinsics::initialize_intrinsics(Realm& realm) m_json_parse_function = &json_object()->get_without_side_effects(vm.names.parse).as_function(); m_json_stringify_function = &json_object()->get_without_side_effects(vm.names.stringify).as_function(); m_object_prototype_to_string_function = &object_prototype()->get_without_side_effects(vm.names.toString).as_function(); + + array_prototype()->convert_to_prototype_if_needed(); + m_default_array_prototype_shape = array_prototype()->shape(); + m_default_object_prototype_shape = object_prototype()->shape(); + + VERIFY(array_prototype()->indexed_properties().is_empty()); + VERIFY(object_prototype()->indexed_properties().is_empty()); } template @@ -416,6 +423,8 @@ void Intrinsics::visit_edges(Visitor& visitor) visitor.visit(m_native_function_shape); visitor.visit(m_unmapped_arguments_object_shape); visitor.visit(m_mapped_arguments_object_shape); + visitor.visit(m_default_array_prototype_shape); + visitor.visit(m_default_object_prototype_shape); visitor.visit(m_proxy_constructor); visitor.visit(m_async_from_sync_iterator_prototype); visitor.visit(m_async_generator_prototype); diff --git a/Libraries/LibJS/Runtime/Intrinsics.h b/Libraries/LibJS/Runtime/Intrinsics.h index f98a269442..3f69abfa3f 100644 --- a/Libraries/LibJS/Runtime/Intrinsics.h +++ b/Libraries/LibJS/Runtime/Intrinsics.h @@ -50,6 +50,9 @@ public: [[nodiscard]] u32 mapped_arguments_object_well_known_symbol_iterator_offset() const { return m_mapped_arguments_object_well_known_symbol_iterator_offset; } [[nodiscard]] u32 mapped_arguments_object_callee_offset() const { return m_mapped_arguments_object_callee_offset; } + [[nodiscard]] GC::Ref default_array_prototype_shape() const { return *m_default_array_prototype_shape; } + [[nodiscard]] GC::Ref default_object_prototype_shape() const { return *m_default_object_prototype_shape; } + [[nodiscard]] GC::Ref throw_type_error_accessor() { return *m_throw_type_error_accessor; } // Not included in JS_ENUMERATE_NATIVE_OBJECTS due to missing distinct prototype @@ -179,6 +182,9 @@ private: u32 m_mapped_arguments_object_well_known_symbol_iterator_offset { 0 }; u32 m_mapped_arguments_object_callee_offset { 0 }; + GC::Ptr m_default_array_prototype_shape; + GC::Ptr m_default_object_prototype_shape; + GC::Ptr m_throw_type_error_accessor; // Not included in JS_ENUMERATE_NATIVE_OBJECTS due to missing distinct prototype diff --git a/Libraries/LibJS/Runtime/ProxyObject.cpp b/Libraries/LibJS/Runtime/ProxyObject.cpp index 5b882904b1..ab8cd25a4d 100644 --- a/Libraries/LibJS/Runtime/ProxyObject.cpp +++ b/Libraries/LibJS/Runtime/ProxyObject.cpp @@ -46,6 +46,10 @@ ProxyObject::ProxyObject(Object& target, Object& handler, Object& prototype) , m_target(target) , m_handler(handler) { + if (target.is_array_exotic_object()) { + auto& array = static_cast(target); + array.set_is_proxy_target(true); + } } static Value property_key_to_value(VM& vm, PropertyKey const& property_key) diff --git a/Libraries/LibJS/Tests/builtins/Array/array-as-proxy-target.js b/Libraries/LibJS/Tests/builtins/Array/array-as-proxy-target.js new file mode 100644 index 0000000000..63eae6951e --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Array/array-as-proxy-target.js @@ -0,0 +1,26 @@ +test("proxy traps should be invoked in the correct order", () => { + var log = []; + var target = []; + var proxy = new Proxy( + target, + new Proxy( + {}, + { + get(t, pk, r) { + log.push(pk); + }, + } + ) + ); + proxy.push(1); + + expect(log.length, 8); + expect(log[0]).toBe("get"); + expect(log[1]).toBe("get"); + expect(log[2]).toBe("set"); + expect(log[3]).toBe("getOwnPropertyDescriptor"); + expect(log[4]).toBe("defineProperty"); + expect(log[5]).toBe("set"); + expect(log[6]).toBe("getOwnPropertyDescriptor"); + expect(log[7]).toBe("defineProperty"); +}); diff --git a/Libraries/LibWeb/WebIDL/ObservableArray.cpp b/Libraries/LibWeb/WebIDL/ObservableArray.cpp index ebd7a26d92..516b5d77ed 100644 --- a/Libraries/LibWeb/WebIDL/ObservableArray.cpp +++ b/Libraries/LibWeb/WebIDL/ObservableArray.cpp @@ -14,11 +14,11 @@ GC_DEFINE_ALLOCATOR(ObservableArray); GC::Ref ObservableArray::create(JS::Realm& realm) { auto prototype = realm.intrinsics().array_prototype(); - return realm.create(prototype); + return realm.create(realm, prototype); } -ObservableArray::ObservableArray(Object& prototype) - : JS::Array(prototype) +ObservableArray::ObservableArray(JS::Realm& realm, Object& prototype) + : JS::Array(realm, prototype) { } diff --git a/Libraries/LibWeb/WebIDL/ObservableArray.h b/Libraries/LibWeb/WebIDL/ObservableArray.h index ccb00c7553..6cea86495e 100644 --- a/Libraries/LibWeb/WebIDL/ObservableArray.h +++ b/Libraries/LibWeb/WebIDL/ObservableArray.h @@ -43,7 +43,7 @@ public: } } - explicit ObservableArray(Object& prototype); + explicit ObservableArray(JS::Realm&, Object& prototype); virtual void visit_edges(JS::Cell::Visitor&) override;