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
5 changes: 5 additions & 0 deletions changelog.d/9899-module-namespace-descriptors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
### Fixed

- Dynamic-import namespace exports now report standard data descriptors with
their current live-binding values instead of exposing the runtime's internal
accessor representation.
4 changes: 4 additions & 0 deletions changelog.d/9900-dead-string-wrapper-diag.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
### Fixed

- GC primitive-dispatch diagnostics no longer expose a string-wrapper counter
that lost its only writer when boxed string indices became virtual.
18 changes: 4 additions & 14 deletions crates/perry-runtime/src/gc/diag_sites.rs
Original file line number Diff line number Diff line change
Expand Up @@ -369,22 +369,18 @@ pub(super) fn report_charges(label: &str) {
// whose method the native dispatch tower does not recognise falls through to
// `native_call_method::call_primitive_builtin_prototype_method`: it resolves
// `globalThis.<Builtin>.prototype[<method>]` and, for a SLOPPY callee, boxes
// the receiver with `ToObject`. For a string that wrapper materialises one own
// property per UTF-16 code unit. So a single unrecognised method name on a hot
// render path turns into O(length) allocations per call, and the only way to
// tell WHICH names those are is to count them at the fork.
// the receiver with `ToObject`. A string wrapper exposes one virtual own index
// per UTF-16 code unit, so tracking receiver length shows the scale of the
// boxed surface alongside the method names that reach this fork.

crate::perry_thread_local! {
/// `"<Builtin>.prototype.<method>" -> (calls, receiver_utf16_chars)`.
static PRIMITIVE_DISPATCH: RefCell<HashMap<String, (u64, u64)>> =
RefCell::new(HashMap::new());
/// String wrappers actually materialised: (wrappers, index properties).
static STRING_WRAPPERS: Cell<(u64, u64)> = const { Cell::new((0, 0)) };
}

/// Record one trip through the primitive-method fallback. `recv_chars` is the
/// receiver's UTF-16 length (0 when the receiver is not a string) — the number
/// of own index properties a sloppy callee's `ToObject` wrapper costs.
/// receiver's UTF-16 length (0 when the receiver is not a string).
pub(crate) fn primitive_dispatch(builtin: &[u8], method: &str, recv_chars: u64) {
if !gc_diag_enabled() {
return;
Expand All @@ -403,12 +399,6 @@ pub(super) fn report_primitive_dispatch(label: &str) {
if !gc_diag_enabled() {
return;
}
let (wrappers, indices) = STRING_WRAPPERS.with(Cell::get);
if wrappers > 0 {
eprintln!(
"[gc-primitive-dispatch] {label}: string_wrappers={wrappers} index_properties={indices}"
);
}
let rows: Vec<(String, (u64, u64))> =
PRIMITIVE_DISPATCH.with(|m| m.borrow().iter().map(|(k, v)| (k.clone(), *v)).collect());
if rows.is_empty() {
Expand Down
6 changes: 6 additions & 0 deletions crates/perry-runtime/src/object/descriptors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -964,6 +964,12 @@ pub extern "C" fn js_object_get_own_property_descriptor(obj_value: f64, key_valu
return f64::from_bits(crate::value::TAG_UNDEFINED);
}

// #9889: Hide the live binding's internal accessor behind the module
// namespace exotic object's required data-descriptor shape.
if (*obj).class_id == MODULE_NAMESPACE_CLASS_ID {
return module_namespace_own_property_descriptor(obj, key_str);
}

// Look up descriptor flags (default: all true).
let attrs = key_rust
.as_ref()
Expand Down
20 changes: 20 additions & 0 deletions crates/perry-runtime/src/object/namespace_create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,26 @@ use super::*;

pub(crate) const MODULE_NAMESPACE_CLASS_ID: u32 = 0xFFFF_4E53;

/// #9889: Implement the module namespace exotic object's [[GetOwnProperty]]
/// result while retaining accessors as the internal representation of live
/// bindings. The getter can allocate and evacuate both the namespace and key,
/// so keep them rooted until it returns. `build_data_descriptor` roots the
/// resulting value before allocating the descriptor object.
pub(crate) unsafe fn module_namespace_own_property_descriptor(
obj: *mut ObjectHeader,
key: *const crate::StringHeader,
) -> f64 {
let scope = crate::gc::RuntimeHandleScope::new();
let obj_handle = scope.root_raw_mut_ptr(obj);
let key_handle = scope.root_string_ptr(key);
let value = obj_handle.with_mut_ptr::<ObjectHeader, _>(|current_obj| {
key_handle.with_const_ptr::<crate::StringHeader, _>(|current_key| {
js_object_get_field_by_name(current_obj, current_key)
})
});
super::descriptors::build_data_descriptor(f64::from_bits(value.bits()), true, true, false)
}

/// Apply the host-defined invariants shared by static and dynamic module
/// namespace objects.
#[no_mangle]
Expand Down
6 changes: 0 additions & 6 deletions scripts/gc_runtime_root_holders.json
Original file line number Diff line number Diff line change
Expand Up @@ -341,12 +341,6 @@
"verdict": "not_a_gc_pointer",
"why": "#9794: per-method primitive-dispatch counts, `HashMap<String, (u64, u64)>`. Rust-owned method-name strings and counters."
},
{
"file": "crates/perry-runtime/src/gc/diag_sites.rs",
"name": "STRING_WRAPPERS",
"verdict": "not_a_gc_pointer",
"why": "#9794: materialized/elided string-wrapper counts as a `(u64, u64)` pair."
},
{
"file": "crates/perry-runtime/src/gc/oldgen_defrag.rs",
"name": "LAST_IDLE_PREDICTED_RELEASE",
Expand Down
21 changes: 21 additions & 0 deletions test-files/test_gap_dynamic_import_alias_binding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,24 @@
async function main(): Promise<void> {
const ns = await import("./dynamic_import_alias_binding.ts");

function logDescriptor(
name: "VAR_SET" | "directConst",
expected: string,
): void {
const descriptor = Object.getOwnPropertyDescriptor(ns, name);
console.log(
name,
descriptor !== undefined,
descriptor !== undefined && "value" in descriptor,
descriptor?.writable,
descriptor?.enumerable,
descriptor?.configurable,
descriptor?.get === undefined,
descriptor?.set === undefined,
descriptor !== undefined && ns.checkAlias(descriptor.value, expected),
);
}

console.log(
typeof ns.VAR_SET,
typeof ns.LET_SET,
Expand All @@ -16,6 +34,8 @@ async function main(): Promise<void> {
ns.checkAlias(ns.CONST_SET, "const"),
ns.checkAlias(ns.directConst, "direct"),
);
logDescriptor("VAR_SET", "var-before");
logDescriptor("directConst", "direct");

ns.reassign();
console.log(
Expand All @@ -24,6 +44,7 @@ async function main(): Promise<void> {
ns.checkAlias(ns.VAR_SET, "var-before"),
ns.checkAlias(ns.LET_SET, "let-before"),
);
logDescriptor("VAR_SET", "var-after");
}

main();
22 changes: 22 additions & 0 deletions test-files/test_issue_9869_for_in_visited_levels_gc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// parity-env: PERRY_GC_SCHEDULE_SEED=9869 PERRY_GC_SCHEDULE_RATE=1 PERRY_GC_SCHEDULE_ALLOC_KB=0 PERRY_GC_FORCE_EVACUATE=1 PERRY_GC_VERIFY_EVACUATION=1 PERRY_GC_PROTECT_FROMSPACE=1

// #9869: `for-in` defers building its shadow set until a prototype level has
// an enumerable key to filter. The retained earlier levels must remain rooted
// while key-array construction allocates and triggers a moving collection.
const ancestor = {
own: "shadowed",
ancestor: "visible",
};
const emptyMiddle = Object.create(ancestor);
const receiver = Object.create(emptyMiddle);
receiver.own = "receiver";

// Level zero records `receiver`; the empty middle level allocates a key array
// but keeps the shadow set deferred; the ancestor's keys then trigger a rebuild
// that reads both retained heap objects after evacuation.
const keys: string[] = [];
for (const key in receiver) {
keys.push(key);
}

console.log(keys.join(","));
Loading