Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions crates/perry-runtime/src/gc/dead_owner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,9 @@ fn fan_out(
is_dead_closure: &dyn Fn(usize) -> bool,
is_dead_symbol: &dyn Fn(usize) -> bool,
) {
// Interned key pointers cached in the store-plan cache may die in this
// collection — flush every cached verdict.
crate::object::prop_plan::prop_plan_epoch_bump();
crate::array::prune_dead_array_named_property_owners(is_dead_owner);
crate::map::prune_dead_map_iterator_array_owners(is_dead_owner);
crate::set::prune_dead_set_iterator_array_owners(is_dead_owner);
Expand Down
7 changes: 7 additions & 0 deletions crates/perry-runtime/src/gc/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -907,6 +907,13 @@ pub const OBJ_FLAG_ARRAY_DESCRIPTORS: u16 = 0x400;
// `GC_TYPE_OBJECT`. Set-only (clearing a descriptor leaves it set; the slow
// path is always correct).
pub const OBJ_FLAG_HAS_DESCRIPTORS: u16 = 0x800;
// This specific object's [[Prototype]] was overridden per-instance
// (`Object.setPrototypeOf` / `__proto__` recording via
// `object_set_static_prototype`). Class-keyed interception caches
// (`object::prop_plan`) must not apply a class-chain verdict to an object
// whose own chain diverges. Bit 12; only meaningful for `GC_TYPE_OBJECT`.
// Set-only, travels with the object across evacuation.
pub const OBJ_FLAG_PROTO_OVERRIDE: u16 = 0x1000;
// #2145: this object is a per-kind `<TypedArrayCtor>.prototype` whose
// `[[Prototype]]` is the shared `%TypedArray%.prototype` intrinsic.
// `Object.getPrototypeOf(Int8Array.prototype)` returns the cached
Expand Down
5 changes: 4 additions & 1 deletion crates/perry-runtime/src/object/class_registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,11 @@ pub use registration::{
};

// ── dispatch.rs ─────────────────────────────────────────────────────────────
#[cfg(test)]
pub(crate) use dispatch::test_bump_vtable_generation;
pub(crate) use dispatch::{
call_vtable_method, fetch_parent_kind_in_chain, vtable_ic_insert, vtable_ic_lookup, VTABLE_GEN,
call_vtable_method, fetch_parent_kind_in_chain, vtable_generation, vtable_ic_insert,
vtable_ic_lookup, VTABLE_GEN,
};

// ── parent_static.rs ────────────────────────────────────────────────────────
Expand Down
4 changes: 2 additions & 2 deletions crates/perry-runtime/src/object/class_registry/construct.rs
Original file line number Diff line number Diff line change
Expand Up @@ -949,7 +949,7 @@ pub unsafe extern "C" fn js_new_function_construct(
)
});
if has_user_proto {
super::super::prototype_chain::object_set_static_prototype(
super::super::prototype_chain::object_link_class_default_prototype(
obj_ptr as usize,
dyn_proto.to_bits(),
);
Expand All @@ -961,7 +961,7 @@ pub unsafe extern "C" fn js_new_function_construct(
if !linked_user_proto {
let proto = ensure_function_prototype_object(func_value, cid);
if !proto.is_null() {
super::super::prototype_chain::object_set_static_prototype(
super::super::prototype_chain::object_link_class_default_prototype(
obj_ptr as usize,
crate::value::js_nanbox_pointer(proto as i64).to_bits(),
);
Expand Down
13 changes: 13 additions & 0 deletions crates/perry-runtime/src/object/class_registry/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,19 @@ use std::sync::RwLock;

pub(crate) static VTABLE_GEN: AtomicU64 = AtomicU64::new(1);

/// Current vtable generation — consumed by caches (method IC below, the
/// store-plan cache in `object::prop_plan`) that must invalidate on any
/// class registration/mutation.
#[inline]
pub(crate) fn vtable_generation() -> u64 {
VTABLE_GEN.load(Ordering::Relaxed)
}

#[cfg(test)]
pub(crate) fn test_bump_vtable_generation() {
VTABLE_GEN.fetch_add(1, Ordering::Release);
}

const VTABLE_IC_SIZE: usize = 4096;
const VTABLE_IC_MASK: usize = VTABLE_IC_SIZE - 1;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ use std::sync::RwLock;

/// Register a class with its parent class ID in the global registry
pub(crate) fn register_class(class_id: u32, parent_class_id: u32) {
// Parent linking changes what a class chain can intercept — flush cached
// store plans (`object::prop_plan`).
crate::object::prop_plan::prop_plan_epoch_bump();
let mut registry = CLASS_REGISTRY.write().unwrap();
if registry.is_none() {
*registry = Some(HashMap::new());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,9 @@ pub static NEXT_SYNTHETIC_CLASS_ID: std::sync::atomic::AtomicU32 =
/// when a class extends `func` via the #711 dynamic-parent path.
#[no_mangle]
pub extern "C" fn js_set_function_prototype(func: f64, proto: f64) -> u32 {
// `F.prototype = obj` re-shapes the chain of every future instance —
// flush cached store plans (`object::prop_plan`).
crate::object::prop_plan::prop_plan_epoch_bump();
let func_bits = func.to_bits();
let func_tag = func_bits & 0xFFFF_0000_0000_0000;
let proto_bits = proto.to_bits();
Expand Down
6 changes: 6 additions & 0 deletions crates/perry-runtime/src/object/descriptor_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,7 @@ pub(crate) unsafe fn plain_data_write_may_intercept(addr: usize, class_id: u32,

/// Store a property descriptor for (obj, key).
pub(crate) fn set_property_attrs(obj: usize, key: String, attrs: PropertyAttrs) {
super::prop_plan::prop_plan_epoch_bump();
note_descriptor_target(obj);
PROPERTY_ATTRS_IN_USE.with(|c| c.set(true));
GLOBAL_DESCRIPTORS_IN_USE.store(true, Ordering::Relaxed);
Expand All @@ -414,6 +415,7 @@ pub(crate) fn set_property_attrs(obj: usize, key: String, attrs: PropertyAttrs)
/// Remove a customized property descriptor for (obj, key), restoring default
/// data-property attributes for subsequent writes and reflection.
pub(crate) fn clear_property_attrs(obj: usize, key: &str) {
super::prop_plan::prop_plan_epoch_bump();
PROPERTY_DESCRIPTORS.with(|m| {
m.borrow_mut().remove(&(obj, key.to_string()));
});
Expand Down Expand Up @@ -532,6 +534,7 @@ pub(crate) unsafe fn json_object_getter_value(

/// Store an accessor descriptor for (obj, key).
pub(crate) fn set_accessor_descriptor(obj: usize, key: String, acc: AccessorDescriptor) {
super::prop_plan::prop_plan_epoch_bump();
note_descriptor_target(obj);
ACCESSORS_IN_USE.with(|c| c.set(true));
GLOBAL_DESCRIPTORS_IN_USE.store(true, Ordering::Relaxed);
Expand All @@ -544,6 +547,7 @@ pub(crate) fn set_accessor_descriptor(obj: usize, key: String, acc: AccessorDesc
/// Remove an accessor descriptor for (obj, key), letting ordinary data-property
/// reads and writes use the object's stored field again.
pub(crate) fn clear_accessor_descriptor(obj: usize, key: &str) {
super::prop_plan::prop_plan_epoch_bump();
ACCESSOR_DESCRIPTORS.with(|m| {
m.borrow_mut().remove(&(obj, key.to_string()));
});
Expand All @@ -569,6 +573,7 @@ pub(crate) fn set_builtin_accessor_descriptor(
acc: AccessorDescriptor,
attrs: PropertyAttrs,
) {
super::prop_plan::prop_plan_epoch_bump();
ACCESSOR_DESCRIPTORS.with(|m| {
m.borrow_mut().insert((obj, key.clone()), acc);
});
Expand All @@ -593,6 +598,7 @@ pub(crate) fn set_builtin_accessor_descriptor(
/// `PROPERTY_DESCRIPTORS` per-object and unconditionally. The gate stays
/// down, so the object get/set hot path is unaffected for every program.
pub(crate) fn set_builtin_property_attrs(obj: usize, key: String, attrs: PropertyAttrs) {
super::prop_plan::prop_plan_epoch_bump();
note_descriptor_target(obj);
PROPERTY_DESCRIPTORS.with(|m| {
m.borrow_mut().insert((obj, key), attrs);
Expand Down
107 changes: 80 additions & 27 deletions crates/perry-runtime/src/object/field_set_by_name.rs
Original file line number Diff line number Diff line change
Expand Up @@ -799,6 +799,51 @@ pub extern "C" fn js_object_set_field_by_name(
}
}

// Resolve the interned key EARLY (hoisted from below the interception
// vet): the store-plan cache and the shape-transition cache both key
// on interned pointer identity. If the key is already interned
// (GC_FLAG_INTERNED set — e.g. from js_string_concat intern hit), skip
// the FNV-1a hash entirely. No allocation happens here, so the raw
// `obj`/`key` pointers stay valid.
let mut interned_key = if !key.is_null() && (key as usize) > 0x10000 {
let gc_hdr =
(key as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader;
if (*gc_hdr).gc_flags & crate::gc::GC_FLAG_INTERNED != 0 {
key // already interned
} else {
let kh = key_content_hash(key);
crate::string::js_string_intern(key, kh)
}
} else {
key
};
let interned_key_handle = scope.root_string_ptr(interned_key);
interned_key = interned_key_handle.get_raw_const_ptr::<crate::StringHeader>();

// Store-plan fast gate (`object::prop_plan`): a recorded verdict means
// the full interception vet below (class vtable setter walk, URL-shape
// probe, `plain_data_write_may_intercept`) proved a store of this key
// to this class cannot be intercepted, and no invalidation (vtable /
// descriptor / prototype mutation, GC) happened since. Per-OBJECT
// conditions stay outside the verdict: frozen/sealed/own-descriptor
// flags are checked below as always, and an instance whose chain
// diverges from its class chain (per-instance `setPrototypeOf`
// override, null-proto) never records or honors a plan.
// Flags that make an object ineligible for class-keyed plans: a
// diverging chain (per-instance proto override / null proto) or own
// descriptors (an own accessor must dispatch through the short-circuit
// below, which a plan hit skips).
const PLAN_BLOCKING_FLAGS: u16 = crate::gc::OBJ_FLAG_PROTO_OVERRIDE
| crate::gc::OBJ_FLAG_NULL_PROTO
| crate::gc::OBJ_FLAG_HAS_DESCRIPTORS;
let obj_class_id = (*obj).class_id;
let plan_eligible = !key.is_null()
&& obj_class_id != 0
&& obj_class_id != NATIVE_MODULE_CLASS_ID
&& (*gc_header)._reserved & PLAN_BLOCKING_FLAGS == 0;
let plan_fast = plan_eligible
&& super::prop_plan::store_plan_check(obj_class_id, interned_key as usize);

// Refs #486 (hono): class setter dispatch. JS spec: a `set X(...)`
// accessor on the prototype intercepts `obj.X = value` writes
// before they hit the instance's data slots. Hono's `set res(_res)
Expand All @@ -809,7 +854,7 @@ pub extern "C" fn js_object_set_field_by_name(
// hono-base's `if (!context.finalized) throw` fired on every
// request. Walk the class -> parent chain mirroring the getter
// dispatch in `js_object_get_field_by_name`.
if !key.is_null() && (key as usize) > 0x10000 {
if !plan_fast && !key.is_null() && (key as usize) > 0x10000 {
let class_id = (*obj).class_id;
if class_id != 0 {
if let Ok(registry) = CLASS_VTABLE_REGISTRY.read() {
Expand Down Expand Up @@ -854,7 +899,11 @@ pub extern "C" fn js_object_set_field_by_name(
}
}

if !key.is_null() && (key as usize) > 0x10000 && crate::url::is_url_object_shape(obj) {
if !plan_fast
&& !key.is_null()
&& (key as usize) > 0x10000
&& crate::url::is_url_object_shape(obj)
{
let key_str = key_to_str_for_diag(key);
let obj = obj_handle.get_raw_mut_ptr::<ObjectHeader>();
let value = value_handle.get_nanbox_f64();
Expand Down Expand Up @@ -917,23 +966,6 @@ pub extern "C" fn js_object_set_field_by_name(

let mut prev_keys_usize = keys as usize;

// Resolve to interned pointer for transition cache (pointer identity).
// If the key is already interned (GC_FLAG_INTERNED set — e.g. from
// js_string_concat intern hit), skip the FNV-1a hash entirely.
let mut interned_key = if !key.is_null() && (key as usize) > 0x10000 {
let gc_hdr =
(key as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader;
if (*gc_hdr).gc_flags & crate::gc::GC_FLAG_INTERNED != 0 {
key // already interned
} else {
let kh = key_content_hash(key);
crate::string::js_string_intern(key, kh)
}
} else {
key
};
let interned_key_handle = scope.root_string_ptr(interned_key);
interned_key = interned_key_handle.get_raw_const_ptr::<crate::StringHeader>();
macro_rules! refresh_roots_after_alloc {
() => {{
obj = obj_handle.get_raw_mut_ptr::<ObjectHeader>();
Expand All @@ -957,12 +989,26 @@ pub extern "C" fn js_object_set_field_by_name(
&& !is_frozen
&& !is_sealed_or_no_extend
&& !has_own_descriptors
&& !super::plain_data_write_may_intercept(
obj as usize,
(*obj).class_id,
f64::from_bits(JSValue::string_ptr(key as *mut _).bits()),
)
&& (plan_fast
|| !super::plain_data_write_may_intercept(
obj as usize,
(*obj).class_id,
f64::from_bits(JSValue::string_ptr(key as *mut _).bits()),
))
{
// The full interception vet just returned negative for this
// (class, key) — the vtable setter walk above found nothing, the
// URL-shape probe fell through, and `plain_data_write_may_intercept`
// cleared the chain. Record the verdict so the next store skips
// the vet (`plan_fast` above). Eligibility is re-derived from the
// freshly read `obj_flags`, not the pre-vet read.
if !plan_fast
&& obj_class_id != 0
&& obj_class_id != NATIVE_MODULE_CLASS_ID
&& obj_flags & PLAN_BLOCKING_FLAGS == 0
{
super::prop_plan::store_plan_record(obj_class_id, interned_key as usize);
}
if let Some((next_keys, slot_idx)) =
transition_cache_lookup(prev_keys_usize, interned_key)
{
Expand Down Expand Up @@ -1058,8 +1104,12 @@ pub extern "C" fn js_object_set_field_by_name(
// 200k of those allocations per query; with this guard the
// count drops to zero unless userland actually defined a
// descriptor.
let needs_descriptor_key =
ACCESSORS_IN_USE.with(|c| c.get()) || PROPERTY_ATTRS_IN_USE.with(|c| c.get());
// On a store-plan hit the object provably has no own descriptors
// (OBJ_FLAG_HAS_DESCRIPTORS is clear — vetted below before the plan is
// honored), so the descriptor key string can never be consulted: skip
// the per-store String allocation entirely.
let needs_descriptor_key = !plan_fast
&& (ACCESSORS_IN_USE.with(|c| c.get()) || PROPERTY_ATTRS_IN_USE.with(|c| c.get()));
let incoming_key_str: Option<String> = if needs_descriptor_key && !key.is_null() {
let name_ptr = (key as *const u8).add(std::mem::size_of::<crate::StringHeader>());
let name_len = (*key).byte_len as usize;
Expand All @@ -1086,7 +1136,10 @@ pub extern "C" fn js_object_set_field_by_name(
// throw "Cannot assign to read only property" on a plain `{}` (Next.js
// app-page-turbo runtime's `exports.Fragment = …`). A fresh allocation
// has the flag clear, so it skips the stale lookup entirely.
if ACCESSORS_IN_USE.with(|c| c.get()) && super::object_has_descriptors(obj as usize) {
if !plan_fast
&& ACCESSORS_IN_USE.with(|c| c.get())
&& super::object_has_descriptors(obj as usize)
{
if let Some(ref k) = incoming_key_str {
if let Some(acc) = get_accessor_descriptor(obj as usize, k) {
if acc.set != 0 {
Expand Down
1 change: 1 addition & 0 deletions crates/perry-runtime/src/object/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ mod field_set_by_name;
mod global_fetch;
mod global_this;
pub mod handle_expando;
pub(crate) mod prop_plan;
pub(crate) use global_this::{default_prepare_stack_trace_func_ptr, ERROR_CONSTRUCTOR_PTR};
mod global_this_tables;
mod groupby;
Expand Down
Loading
Loading