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
1 change: 1 addition & 0 deletions changelog.d/6795-runtime-state-phase-a.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
perf(runtime): #6759 Phase A — one heap-allocated per-thread `RuntimeState` behind a single const-init TLS pointer replaces 12 hot `thread_local!` object-op side tables (descriptor/accessor tables + gates, overflow fields + last-cache, keys-index sidecar, shape inline/overflow caches, transition cache, field cache, wide-key index). One TLS fetch per operation instead of one per table; isolation, lifetime, and borrow discipline unchanged; arm64_32 transition-cache boxing preserved.
1 change: 1 addition & 0 deletions crates/perry-runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ pub mod regex;
pub mod safe_area;
pub mod set;
pub mod shared_sab;
pub(crate) mod state;
pub mod string;
pub mod symbol;
/// TC39 Temporal API (#4686): `Temporal.Duration`, `Temporal.Instant`,
Expand Down
16 changes: 10 additions & 6 deletions crates/perry-runtime/src/object/array_object_ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -545,9 +545,11 @@ pub(crate) unsafe fn define_array_property(

// Redefining an index that was previously an accessor back to a data
// property: drop the stale accessor entry.
ACCESSOR_DESCRIPTORS.with(|m| {
m.borrow_mut().remove(&(obj as usize, key_name.to_string()));
});
crate::state::state()
.descriptors
.accessor_descriptors
.borrow_mut()
.remove(&(obj as usize, key_name.to_string()));
// [[DefineOwnProperty]] writes the slot directly — clear any stale
// attrs first so the extend helper's [[Set]]-side writability check
// (added for ordinary `arr[i] = v` writes) can't reject this store.
Expand Down Expand Up @@ -726,9 +728,11 @@ pub(crate) unsafe fn define_array_property(
// Redefining a former accessor back to a data property drops the stale
// accessor entry (the non-configurable case already threw above).
if cur_accessor.is_some() {
ACCESSOR_DESCRIPTORS.with(|m| {
m.borrow_mut().remove(&(obj as usize, key_name.to_string()));
});
crate::state::state()
.descriptors
.accessor_descriptors
.borrow_mut()
.remove(&(obj as usize, key_name.to_string()));
}

// Write the value: an explicit `value` wins; a NEW property with no value
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ unsafe fn inherited_proto_accessor_value(
key: *const crate::StringHeader,
receiver: f64,
) -> Option<JSValue> {
if key.is_null() || !ACCESSORS_IN_USE.with(|c| c.get()) {
if key.is_null() || !crate::state::state().descriptors.accessors_in_use.get() {
return None;
}
let key_ptr = (key as *const u8).add(std::mem::size_of::<crate::StringHeader>());
Expand Down
8 changes: 5 additions & 3 deletions crates/perry-runtime/src/object/delete_rest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -380,9 +380,11 @@ pub extern "C" fn js_object_delete_field(
// slot map is now stale (entries past `i` have shifted).
// The next lookup at threshold will rebuild from current
// keys_array.
KEYS_INDEX.with(|m| {
m.borrow_mut().remove(&(obj as usize));
});
crate::state::state()
.object_hot
.keys_index
.borrow_mut()
.remove(&(obj as usize));

1
}
Expand Down
195 changes: 120 additions & 75 deletions crates/perry-runtime/src/object/descriptor_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
use super::*;

use crate::fast_hash::{new_fast_key_hash_map, FastKeyHashMap};
use crate::state::state;
use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
Expand Down Expand Up @@ -47,12 +48,46 @@ impl PropertyAttrs {
}
}

thread_local! {
// Hasher: `FastKeyHasher` (FNV-1a) rather than std's SipHash `RandomState`.
// The key is `(owner_addr, key_string)` — a runtime heap pointer plus a
// program-supplied property name, so no external input reaches it and
// DoS-resistant hashing buys nothing on this hot property-access path.
pub(crate) static PROPERTY_DESCRIPTORS: RefCell<FastKeyHashMap<(usize, String), PropertyAttrs>> = RefCell::new(new_fast_key_hash_map());
/// #6759 Phase A: the descriptor side tables and their per-thread fast-path
/// gates, grouped as the `descriptors` field of
/// [`crate::state::RuntimeState`]. Previously four separate `thread_local!`s;
/// reach them via `crate::state::state().descriptors` (one TLS fetch for the
/// whole group).
pub(crate) struct DescriptorTables {
/// Per-property attribute flags set by `Object.defineProperty` /
/// `Object.freeze` / `Object.seal`, keyed `(owner_addr, key_string)`.
///
/// Hasher: `FastKeyHasher` (FNV-1a) rather than std's SipHash
/// `RandomState`. The key is a runtime heap pointer plus a
/// program-supplied property name, so no external input reaches it and
/// DoS-resistant hashing buys nothing on this hot property-access path.
pub(crate) property_descriptors: RefCell<FastKeyHashMap<(usize, String), PropertyAttrs>>,
/// Accessor descriptor storage: maps `(owner_addr, key_string)` to the
/// getter/setter closure bits. Same hasher rationale as
/// `property_descriptors`.
pub(crate) accessor_descriptors: RefCell<FastKeyHashMap<(usize, String), AccessorDescriptor>>,
/// Fast-path gate: `false` when no accessor descriptors have ever been
/// installed on this thread, so hot `js_object_get_field_by_name` /
/// `set_field_by_name` can skip the `accessor_descriptors` HashMap
/// lookup entirely.
pub(crate) accessors_in_use: Cell<bool>,
/// Fast-path gate for `property_descriptors` — flipped the first time
/// `Object.defineProperty` (or freeze/seal via `set_property_attrs`)
/// installs a per-property descriptor. Lets the hot object-write path
/// skip the `.to_string()` allocation required to look up a descriptor
/// that almost never exists.
pub(crate) property_attrs_in_use: Cell<bool>,
}

impl DescriptorTables {
pub(crate) fn new() -> Self {
DescriptorTables {
property_descriptors: RefCell::new(new_fast_key_hash_map()),
accessor_descriptors: RefCell::new(new_fast_key_hash_map()),
accessors_in_use: Cell::new(false),
property_attrs_in_use: Cell::new(false),
}
}
}

/// Accessor descriptor storage: maps (obj_ptr, key) -> (get_closure_bits, set_closure_bits).
Expand All @@ -66,22 +101,6 @@ pub(crate) struct AccessorDescriptor {
pub set: u64, // NaN-boxed closure f64 bits, 0 = absent
}

thread_local! {
// Hasher: `FastKeyHasher` (FNV-1a); see `PROPERTY_DESCRIPTORS` above for the
// same-shape `(owner_addr, key_string)` key and rationale.
pub(crate) static ACCESSOR_DESCRIPTORS: RefCell<FastKeyHashMap<(usize, String), AccessorDescriptor>> = RefCell::new(new_fast_key_hash_map());
/// Fast-path gate: `false` when no accessor descriptors have ever been installed
/// on this thread, so hot `js_object_get_field_by_name` / `set_field_by_name`
/// can skip the `ACCESSOR_DESCRIPTORS` HashMap lookup entirely.
pub(crate) static ACCESSORS_IN_USE: Cell<bool> = const { Cell::new(false) };
/// Fast-path gate for `PROPERTY_DESCRIPTORS` — flipped the first time
/// `Object.defineProperty` (or freeze/seal via `set_property_attrs`)
/// installs a per-property descriptor. Lets the hot object-write path
/// skip the `.to_string()` allocation required to look up a descriptor
/// that almost never exists.
pub(crate) static PROPERTY_ATTRS_IN_USE: Cell<bool> = const { Cell::new(false) };
}

/// Global monotonic flag: set once any accessor or property descriptor is
/// installed. Checked on every dynamic property write via a single
/// `Relaxed` load (no TLS overhead, no fence on aarch64/x86).
Expand Down Expand Up @@ -319,7 +338,12 @@ pub(crate) fn note_descriptor_target(obj: usize) {
/// Look up the property descriptor for (obj, key). Returns None if no entry exists,
/// in which case the JS default `{ writable: true, enumerable: true, configurable: true }` applies.
pub(crate) fn get_property_attrs(obj: usize, key: &str) -> Option<PropertyAttrs> {
PROPERTY_DESCRIPTORS.with(|m| m.borrow().get(&(obj, key.to_string())).copied())
state()
.descriptors
.property_descriptors
.borrow()
.get(&(obj, key.to_string()))
.copied()
}

/// Whether this specific object has ever had a property descriptor installed on
Expand Down Expand Up @@ -416,38 +440,47 @@ pub(crate) unsafe fn plain_data_write_may_intercept(addr: usize, class_id: u32,
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));
let st = state();
st.descriptors.property_attrs_in_use.set(true);
GLOBAL_DESCRIPTORS_IN_USE.store(true, Ordering::Relaxed);
disable_class_field_inline_guard_for_target(obj);
PROPERTY_DESCRIPTORS.with(|m| {
m.borrow_mut().insert((obj, key), attrs);
});
st.descriptors
.property_descriptors
.borrow_mut()
.insert((obj, key), attrs);
}

/// 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()));
});
state()
.descriptors
.property_descriptors
.borrow_mut()
.remove(&(obj, key.to_string()));
}

/// Look up the accessor descriptor (get/set) for (obj, key).
pub(crate) fn get_accessor_descriptor(obj: usize, key: &str) -> Option<AccessorDescriptor> {
ACCESSOR_DESCRIPTORS.with(|m| m.borrow().get(&(obj, key.to_string())).copied())
state()
.descriptors
.accessor_descriptors
.borrow()
.get(&(obj, key.to_string()))
.copied()
}

pub(crate) fn accessor_descriptor_keys_for_obj(obj: usize) -> Vec<String> {
ACCESSOR_DESCRIPTORS.with(|m| {
let mut keys = m
.borrow()
.keys()
.filter_map(|(owner, key)| (*owner == obj).then(|| key.clone()))
.collect::<Vec<_>>();
keys.sort();
keys
})
let mut keys = state()
.descriptors
.accessor_descriptors
.borrow()
.keys()
.filter_map(|(owner, key)| (*owner == obj).then(|| key.clone()))
.collect::<Vec<_>>();
keys.sort();
keys
}

/// #2766: resolve an accessor *getter* closure for `(value, key)` if one is
Expand All @@ -458,7 +491,7 @@ pub(crate) fn accessor_descriptor_keys_for_obj(obj: usize) -> Vec<String> {
/// invoking it. Returns `None` (rather than reading the field) when there is no
/// accessor at all, so the caller falls back to an ordinary field read.
pub(crate) fn reflect_getter_closure_bits(value: f64, key: f64) -> Option<u64> {
if !ACCESSORS_IN_USE.with(|c| c.get()) {
if !state().descriptors.accessors_in_use.get() {
return None;
}
let key_str = crate::builtins::js_string_coerce(key);
Expand Down Expand Up @@ -567,22 +600,26 @@ fn note_accessor_descriptor_key(key: &str) {
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));
let st = state();
st.descriptors.accessors_in_use.set(true);
GLOBAL_DESCRIPTORS_IN_USE.store(true, Ordering::Relaxed);
disable_class_field_inline_guard_for_target(obj);
note_accessor_descriptor_key(&key);
ACCESSOR_DESCRIPTORS.with(|m| {
m.borrow_mut().insert((obj, key), acc);
});
st.descriptors
.accessor_descriptors
.borrow_mut()
.insert((obj, key), acc);
}

/// 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()));
});
state()
.descriptors
.accessor_descriptors
.borrow_mut()
.remove(&(obj, key.to_string()));
}

/// Install a built-in *reflection-only* accessor descriptor for (obj, key)
Expand All @@ -607,12 +644,15 @@ pub(crate) fn set_builtin_accessor_descriptor(
) {
super::prop_plan::prop_plan_epoch_bump();
note_accessor_descriptor_key(&key);
ACCESSOR_DESCRIPTORS.with(|m| {
m.borrow_mut().insert((obj, key.clone()), acc);
});
PROPERTY_DESCRIPTORS.with(|m| {
m.borrow_mut().insert((obj, key), attrs);
});
let st = state();
st.descriptors
.accessor_descriptors
.borrow_mut()
.insert((obj, key.clone()), acc);
st.descriptors
.property_descriptors
.borrow_mut()
.insert((obj, key), attrs);
}

/// Install a built-in *reflection-only* data-property descriptor for (obj, key)
Expand All @@ -633,9 +673,11 @@ pub(crate) fn set_builtin_accessor_descriptor(
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);
});
state()
.descriptors
.property_descriptors
.borrow_mut()
.insert((obj, key), attrs);
}

/// Walk the keys array of `obj` and apply the given attribute mask AND filter to every existing key.
Expand Down Expand Up @@ -703,18 +745,19 @@ pub(crate) fn prune_dead_descriptor_owner_entries(is_dead_owner: &dyn Fn(usize)
.entry(owner)
.or_insert_with(|| is_dead_owner(owner))
};
PROPERTY_DESCRIPTORS.with(|m| {
let mut m = m.borrow_mut();
let st = state();
{
let mut m = st.descriptors.property_descriptors.borrow_mut();
if !m.is_empty() {
m.retain(|(owner, _), _| !is_dead(*owner));
}
});
ACCESSOR_DESCRIPTORS.with(|m| {
let mut m = m.borrow_mut();
}
{
let mut m = st.descriptors.accessor_descriptors.borrow_mut();
if !m.is_empty() {
m.retain(|(owner, _), _| !is_dead(*owner));
}
});
}
}

/// #6710: drop every property-attr + accessor descriptor owned by `obj`.
Expand All @@ -731,18 +774,19 @@ pub(crate) fn clear_object_descriptors(obj: usize) {
if !HANDLE_HAS_DESCRIPTORS.load(Ordering::Relaxed) {
return;
}
PROPERTY_DESCRIPTORS.with(|m| {
let mut m = m.borrow_mut();
let st = state();
{
let mut m = st.descriptors.property_descriptors.borrow_mut();
if !m.is_empty() {
m.retain(|(owner, _), _| *owner != obj);
}
});
ACCESSOR_DESCRIPTORS.with(|m| {
let mut m = m.borrow_mut();
}
{
let mut m = st.descriptors.accessor_descriptors.borrow_mut();
if !m.is_empty() {
m.retain(|(owner, _), _| *owner != obj);
}
});
}
}

/// Rewrite a descriptor table's owner ADDRESS during the GC metadata-rewrite
Expand Down Expand Up @@ -771,8 +815,9 @@ fn rewrite_descriptor_owner(
/// attrs and accessors don't silently detach (or fire on a new tenant at a
/// reused address).
pub(crate) fn scan_descriptor_roots_mut(visitor: &mut crate::gc::RuntimeRootVisitor<'_>) {
PROPERTY_DESCRIPTORS.with(|descriptors| {
let mut descriptors = descriptors.borrow_mut();
let st = state();
{
let mut descriptors = st.descriptors.property_descriptors.borrow_mut();
let needs_rebuild = descriptors
.keys()
.any(|(owner, _)| rewrite_descriptor_owner(visitor, *owner) != *owner);
Expand All @@ -783,10 +828,10 @@ pub(crate) fn scan_descriptor_roots_mut(visitor: &mut crate::gc::RuntimeRootVisi
descriptors.insert((owner, key), attrs);
}
}
});
}

ACCESSOR_DESCRIPTORS.with(|descriptors| {
let mut descriptors = descriptors.borrow_mut();
{
let mut descriptors = st.descriptors.accessor_descriptors.borrow_mut();
let needs_rebuild = descriptors
.keys()
.any(|(owner, _)| rewrite_descriptor_owner(visitor, *owner) != *owner);
Expand All @@ -812,5 +857,5 @@ pub(crate) fn scan_descriptor_roots_mut(visitor: &mut crate::gc::RuntimeRootVisi
}
}
}
});
}
}
Loading
Loading