From 9f8fd458684d0b5c709433a0390ab8d04cd4451c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 29 Aug 2026 06:59:36 +0000 Subject: [PATCH] perf(runtime): decide builtin iterator .next() first in the handle ladder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `for…of` calls `.next()` once PER ELEMENT, and each of those calls walked the whole receiver-classification ladder in `dispatch_handle` — several hundred lines of small-handle, buffer, node-module, class-id and prototype checks — before reaching the `class_id` compare that actually answers it. On a 4k-entity ECS archetype-migration frame the Map and Set iterator dispatchers were 8.6% of the row, nearly all of it spent getting to them. Hoist the three builtin-iterator `class_id` tests to the top of the ladder. The tests are byte-identical to the ones they shadow further down; only their position changes. These ids are dedicated to the builtin array/Map/Set iterators, so no earlier arm can legitimately claim such a receiver, and the gate reads the GC header first so a small handle or a non-object pointer never reaches the `ObjectHeader` field read. A user override of `.next()` keeps winning: the override lookup lives INSIDE each dispatcher (`call_overridden_iterator_next`), not in the ladder that precedes it, so moving the entry point does not move the override check. Claude-Session: https://claude.ai/code/session_01FUvFrRNZyc5qknBiJbYbby --- .../native_call_method/handle_methods.rs | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/crates/perry-runtime/src/object/native_call_method/handle_methods.rs b/crates/perry-runtime/src/object/native_call_method/handle_methods.rs index e1c6293e29..e5a717abb3 100644 --- a/crates/perry-runtime/src/object/native_call_method/handle_methods.rs +++ b/crates/perry-runtime/src/object/native_call_method/handle_methods.rs @@ -67,6 +67,51 @@ pub(super) unsafe fn dispatch_handle( let refreshed_args = || crate::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(arg_handles); let _ = (root_scope, object_handle, &refreshed_args, raw_bits, jsval); let _ = (method_name_ptr, method_name_len); + + // Builtin collection / array iterators, decided first. + // + // `for…of` calls `.next()` once PER ELEMENT, and every one of those calls + // used to walk the whole ladder below — hundreds of lines of receiver + // classification — before reaching the `class_id` compare that actually + // answers it. On a 4k-entity ECS archetype-migration frame the Map and Set + // iterator dispatchers were 8.6% of the row, nearly all of it spent + // getting to them. + // + // The test hoisted here is byte-identical to the one further down; only its + // position changes. These class ids are dedicated to the builtin iterators, + // so no earlier arm of the ladder can legitimately claim such a receiver. + // A user override of `.next()` is still honoured: the override lookup lives + // INSIDE each dispatcher (`call_overridden_iterator_next`), not in the + // ladder that precedes it. + if jsval.is_pointer() { + let raw = jsval.as_pointer::() as usize; + if !crate::value::addr_class::is_small_handle(raw) && raw != 0 { + if let Some(header) = crate::value::addr_class::try_read_gc_header(raw) { + if header.obj_type == crate::gc::GC_TYPE_OBJECT { + let obj = raw as *mut ObjectHeader; + let class_id = (*obj).class_id; + if class_id == crate::collection_iter_object::MAP_ITERATOR_CLASS_ID { + return Some(crate::collection_iter_object::dispatch_map_iterator_method( + obj, + method_name, + )); + } + if class_id == crate::collection_iter_object::SET_ITERATOR_CLASS_ID { + return Some(crate::collection_iter_object::dispatch_set_iterator_method( + obj, + method_name, + )); + } + if class_id == crate::array::ARRAY_ITERATOR_CLASS_ID { + return Some(crate::array::dispatch_array_iterator_method( + obj, + method_name, + )); + } + } + } + } + } // Check if this is a handle-based object (small integer, not a real heap pointer) // Handles are used by Fastify, ioredis, and other native modules that store // objects in a registry and use integer IDs to reference them.