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
69 changes: 69 additions & 0 deletions crates/perry-runtime/src/object/descriptor_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,75 @@ pub(crate) fn object_has_descriptors(obj: usize) -> bool {
false
}

/// #6084 (item 6): can anything intercept a plain-data write of `key` to the
/// `GC_TYPE_OBJECT` at `addr` (own accessor / non-writable descriptor, or an
/// inherited setter / non-writable data property), so the dynamic-write
/// transition-cache fast path must be skipped for THIS write?
///
/// Replaces the process-global `GLOBAL_DESCRIPTORS_IN_USE` latch that used to
/// gate both dynamic-write fast paths. That latch flips on *any* descriptor
/// install anywhere — so a single `Object.freeze` on a completely unrelated
/// object (or any library that freezes one config object at import time)
/// permanently pushed EVERY dynamic property write in the process onto the
/// O(own-key-count) slow walk. Measured: 1M objects × 3 new props = 5281 ms;
/// the identical loop after one unrelated `Object.freeze` = 6807 ms (+29%,
/// and it never recovers).
///
/// The vetting here is the same predicate `ordinary_set`'s #5054 fast path
/// (`proxy.rs`) already applies per receiver, and the same receiver-level /
/// prototype-level split as the #5654 read-side guard:
/// - own descriptors are visible per-object in `OBJ_FLAG_HAS_DESCRIPTORS`
/// (set by [`note_descriptor_target`], travels with the object on
/// evacuation, and is clear on every fresh allocation);
/// - only *prototype*-level installs can intercept a write to an object whose
/// own flag is clear, and those are checked against the actual prototype
/// chain — `Object.prototype` per-key via [`object_proto_may_intercept_key`]
/// (a blanket check made wide dynamic builds O(n²), see #5054), a recorded
/// `setPrototypeOf` target, or the class chain via
/// [`class_instance_set_may_intercept`].
///
/// Conservative in every uncertain case (returns `true` = take the slow path).
/// `caller` must have already established that `addr` is a `GC_TYPE_OBJECT`
/// whose frozen/sealed/non-extensible flags are clear.
pub(crate) unsafe fn plain_data_write_may_intercept(addr: usize, class_id: u32, key: f64) -> bool {
// Nothing has ever installed a descriptor or accessor: no per-object work at
// all, just the one relaxed load the old gate did.
if !descriptors_in_use() {
return false;
}

// A descriptor exists SOMEWHERE. Vet this receiver and its prototype chain
// instead of latching the whole process onto the slow path.

// Own accessor / non-writable descriptor on this exact object.
if object_has_descriptors(addr) {
return true;
}

// `note_descriptor_target` cannot record the per-object flag for typed
// arrays (small ones are plain-alloc'd without a GcHeader) or for exotic
// expando hosts, so their descriptors are invisible to the flag check
// above — never fast-path them once any descriptor exists.
if crate::typedarray::lookup_typed_array_kind(addr).is_some() {
return true;
}
let value = crate::value::js_nanbox_pointer(addr as i64);
if super::exotic_expando::exotic_expando_kind_of_value(value).is_some() {
return true;
}

if class_id == 0 {
// Plain object. Its prototype is exactly `Object.prototype` unless a
// `setPrototypeOf` target was recorded for it.
super::prototype_chain::object_static_prototype(addr).is_some()
|| object_proto_may_intercept_key(key)
} else {
// Class instance: an inherited accessor / non-writable data property
// anywhere in the chain intercepts the write.
class_instance_set_may_intercept(addr, class_id, key)
}
}

/// Store a property descriptor for (obj, key).
pub(crate) fn set_property_attrs(obj: usize, key: String, attrs: PropertyAttrs) {
note_descriptor_target(obj);
Expand Down
36 changes: 31 additions & 5 deletions crates/perry-runtime/src/object/field_set_by_name.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,6 @@ pub extern "C" fn js_object_set_field_by_name_transition_fast(
if obj.is_null() || (obj as usize) < crate::gc::GC_HEADER_SIZE + 0x1000 {
return 0;
}
if GLOBAL_DESCRIPTORS_IN_USE.load(Ordering::Relaxed) {
return 0;
}

let scope = crate::gc::RuntimeHandleScope::new();
let obj_handle = scope.root_raw_mut_ptr(obj);
Expand All @@ -80,7 +77,10 @@ pub extern "C" fn js_object_set_field_by_name_transition_fast(
if object_flags
& (crate::gc::OBJ_FLAG_FROZEN
| crate::gc::OBJ_FLAG_SEALED
| crate::gc::OBJ_FLAG_NO_EXTEND)
| crate::gc::OBJ_FLAG_NO_EXTEND
// #6084 item 6: an own descriptor on THIS object (accessor or
// non-writable) must route through the full setter semantics.
| crate::gc::OBJ_FLAG_HAS_DESCRIPTORS)
!= 0
{
return 0;
Expand All @@ -89,6 +89,18 @@ pub extern "C" fn js_object_set_field_by_name_transition_fast(
return 0;
}

// #6084 item 6: this used to be a `GLOBAL_DESCRIPTORS_IN_USE` check at
// the top of the function — one `Object.freeze` anywhere in the process
// (even on an unrelated object) permanently disabled this fast path for
// every object. Vet the receiver's own flag (above) and its prototype
// chain (here) instead. `class_id` is 0 at this point, so the only
// inherited interceptor is `Object.prototype` (or a recorded
// `setPrototypeOf` target).
let key_f64 = f64::from_bits(JSValue::string_ptr(key as *mut _).bits());
if super::plain_data_write_may_intercept(obj as usize, 0, key_f64) {
return 0;
}

let key_gc =
(key as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader;
if (*key_gc).obj_type != crate::gc::GC_TYPE_STRING {
Expand Down Expand Up @@ -910,10 +922,24 @@ pub extern "C" fn js_object_set_field_by_name(
}

// FAST PATH: shape-transition cache with interned string pointer identity.
//
// #6084 item 6: the descriptor gate here used to be the process-global
// `GLOBAL_DESCRIPTORS_IN_USE` latch, so ONE `Object.freeze` anywhere
// (even on an object never written to again) permanently forced every
// dynamic write in the process down the O(own-key-count) slow walk
// below. It is now vetted per receiver: an own descriptor is visible in
// this object's `OBJ_FLAG_HAS_DESCRIPTORS`, and only prototype-level
// interceptors need a chain walk.
let has_own_descriptors = obj_flags & crate::gc::OBJ_FLAG_HAS_DESCRIPTORS != 0;
if !key.is_null()
&& !is_frozen
&& !is_sealed_or_no_extend
&& !GLOBAL_DESCRIPTORS_IN_USE.load(Ordering::Relaxed)
&& !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()),
)
{
if let Some((next_keys, slot_idx)) =
transition_cache_lookup(prev_keys_usize, interned_key)
Expand Down
9 changes: 5 additions & 4 deletions crates/perry-runtime/src/object/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,10 +157,11 @@ pub(crate) use descriptor_state::{
descriptors_in_use, disable_class_field_inline_guard, get_accessor_descriptor,
get_property_attrs, json_object_getter_value, mark_all_keys, note_descriptor_target,
object_has_descriptors, object_proto_descriptors_in_use, object_proto_may_intercept_key,
prune_dead_descriptor_owner_entries, reflect_getter_closure_bits, set_accessor_descriptor,
set_builtin_accessor_descriptor, set_builtin_property_attrs, set_property_attrs,
AccessorDescriptor, PropertyAttrs, ACCESSORS_IN_USE, ACCESSOR_DESCRIPTORS,
GLOBAL_DESCRIPTORS_IN_USE, PROPERTY_ATTRS_IN_USE, PROPERTY_DESCRIPTORS,
plain_data_write_may_intercept, prune_dead_descriptor_owner_entries,
reflect_getter_closure_bits, set_accessor_descriptor, set_builtin_accessor_descriptor,
set_builtin_property_attrs, set_property_attrs, AccessorDescriptor, PropertyAttrs,
ACCESSORS_IN_USE, ACCESSOR_DESCRIPTORS, GLOBAL_DESCRIPTORS_IN_USE, PROPERTY_ATTRS_IN_USE,
PROPERTY_DESCRIPTORS,
};
pub use this_binding::{
js_implicit_this_get, js_implicit_this_get_sloppy, js_implicit_this_set, js_new_target_get,
Expand Down
64 changes: 27 additions & 37 deletions crates/perry-runtime/src/promise/combinators.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
use super::*;
use std::os::raw::c_int;

use super::keyed_table::PromiseKeyedTable;

use super::assimilate::{
assimilate_via_then_property, enqueue_thenable_job, get_then_action,
promise_resolve_assimilating, thenable_job_reject_fn, thenable_job_resolve_fn,
Expand All @@ -19,8 +21,12 @@ pub(super) struct PromiseAllState {
}

thread_local! {
pub(super) static PROMISE_ALL_STATES: RefCell<Vec<(usize, PromiseAllState)>> =
const { RefCell::new(Vec::new()) };
/// Keyed by input-promise address. See `keyed_table.rs`: a dense `Vec` is
/// still the GC scanners' traversal/rewrite surface, with an O(1) key index
/// layered on top (#6084 item 2 — this used to be a raw `Vec` that every
/// settlement scanned end to end).
pub(super) static PROMISE_ALL_STATES: RefCell<PromiseKeyedTable<PromiseAllState>> =
const { RefCell::new(PromiseKeyedTable::new()) };
}

/// Drain ALL `PromiseAllState` entries associated with `promise`.
Expand All @@ -42,20 +48,7 @@ pub(super) fn promise_all_take_all_handlers(promise: *mut Promise) -> Vec<Promis
if promise.is_null() {
return Vec::new();
}
PROMISE_ALL_STATES.with(|states| {
let mut states = states.borrow_mut();
let key = promise as usize;
let mut drained = Vec::new();
let mut i = 0;
while i < states.len() {
if states[i].0 == key {
drained.push(states.swap_remove(i).1);
} else {
i += 1;
}
}
drained
})
PROMISE_ALL_STATES.with(|states| states.borrow_mut().take_all(promise as usize))
}

#[inline]
Expand All @@ -70,11 +63,17 @@ pub(super) fn promise_all_settle(state: PromiseAllState, value: f64, is_fulfille
pub(super) fn scan_promise_all_states_mut(visitor: &mut crate::gc::RuntimeRootVisitor<'_>) {
PROMISE_ALL_STATES.with(|states| {
let mut states = states.borrow_mut();
for (key, state) in states.iter_mut() {
visitor.visit_metadata_usize_slot(key);
visitor.visit_raw_mut_ptr_slot(&mut state.result_promise);
visitor.visit_raw_mut_ptr_slot(&mut state.results_arr);
visitor.visit_raw_mut_ptr_slot(&mut state.state_arr);
let mut rekeyed = false;
for entry in states.iter_mut() {
// Evacuation rewrites the key IN PLACE — the position stays valid,
// but the key → position index no longer does. Rebuild it lazily.
rekeyed |= visitor.visit_metadata_usize_slot(&mut entry.key);
visitor.visit_raw_mut_ptr_slot(&mut entry.value.result_promise);
visitor.visit_raw_mut_ptr_slot(&mut entry.value.results_arr);
visitor.visit_raw_mut_ptr_slot(&mut entry.value.state_arr);
}
if rekeyed {
states.note_key_rewritten();
}
});
}
Expand All @@ -88,24 +87,19 @@ pub(super) fn remove_all_states_for_dead_promise(promise: *mut Promise) {
return;
}
let key = promise as usize;
PROMISE_ALL_STATES.with(|states| {
let mut states = states.borrow_mut();
if !states.is_empty() {
states.retain(|(k, _)| *k != key);
}
});
PROMISE_ALL_STATES.with(|states| states.borrow_mut().remove_key(key));
}

/// Copied-minor from-space cleanup for `PROMISE_ALL_STATES` — see
/// `cleanup_copied_minor_settle_listeners_for_gc` (`reactions.rs`).
pub(super) fn cleanup_copied_minor_all_states_for_gc() {
use super::CopiedMinorPromiseKeyFate::*;
PROMISE_ALL_STATES.with(|states| {
states.borrow_mut().retain_mut(|(key, _)| {
match super::copied_minor_promise_key_fate(*key) {
states.borrow_mut().retain_mut(|entry| {
match super::copied_minor_promise_key_fate(entry.key) {
Keep => true,
Rekey(new_key) => {
*key = new_key;
entry.key = new_key;
true
}
Drop => false,
Expand Down Expand Up @@ -891,7 +885,7 @@ pub extern "C" fn js_promise_all(promises_arr: *const crate::array::ArrayHeader)
}
PromiseState::Pending => {
PROMISE_ALL_STATES.with(|states| {
states.borrow_mut().push((promise_ptr as usize, state));
states.borrow_mut().push(promise_ptr as usize, state);
});
set_promise_callback_context(promise_ptr);
}
Expand Down Expand Up @@ -1721,12 +1715,8 @@ mod tests {
assert_eq!((*all_b).state, PromiseState::Pending);

// PROMISE_ALL_STATES must hold TWO entries keyed on `shared`.
let registered = PROMISE_ALL_STATES.with(|s| {
s.borrow()
.iter()
.filter(|(k, _)| *k == shared as usize)
.count()
});
let registered =
PROMISE_ALL_STATES.with(|s| s.borrow_mut().count_for_key(shared as usize));
assert_eq!(
registered, 2,
"expected two Promise.all states keyed on the shared pending promise"
Expand Down
Loading
Loading