Skip to content

[perf] Quadratic async/promise/timer structures + object-keyed Map O(n) + permanent TIMER_REF_STATES leak #6084

Description

@proggeramlug

Severity: performance (super-linear blowups on common shapes)
Found by: audit fable-audit-perry-2.md (several measured).

A cluster of runtime data structures that go quadratic or leak on idiomatic workloads:

  1. process.nextTick queue drains with Vec::remove(0) — O(n²). crates/perry-runtime/src/builtins/globals.rs:980,1008-1014. A burst of n ticks memmoves ~n²/2 entries. Fix: VecDeque::pop_front (the promise TASK_QUEUE two files over already does this). Good first issue.
  2. Promise.all keeps per-pending-element state in a global Vec scanned in full on every settle. promise/combinators.rs:22,41-59,858-861; PROMISE_OVERFLOW_REACTIONS and PROMISE_SETTLE_LISTENERS (then.rs:255-389) have the same shape — any non-empty table taxes every unrelated settle. Promise.all of 10k pending promises ≈ 100M comparisons. Fix: key by promise (HashMap<usize, SmallVec<…>>) or hang an intrusive reaction list off the Promise.
  3. Timers: three unsorted Mutex<Vec>s, full-scanned several times per loop turn, and TIMER_REF_STATES grows forever. timer.rs:331,504-521 — one i64→bool entry per timer ever created, never removed (self-documented). A server arming a per-request timeout leaks unboundedly and locks the map per liveness probe. Fix: BinaryHeap per queue + id-tombstone map + cached next-deadline atomic + fold ref-state into the timer record.
  4. PROMISE_CONTEXTS (AsyncLocalStorage bookkeeping) churns a HashMap insert per .then + O(K) key scan per settle even when ALS is never used. promise/mod.rs:570-687. Fix: inline the snapshot into Promise, or gate on an "ALS ever instantiated" flag.
  5. Object/BigInt-keyed Map/Set operations are O(n) each → O(n²) workloads. map.rs:751-830 restricts the hash index to non-pointer keys; object keys take a linear scan. Measured 1,793× slower than string keys (20k inserts+gets: 7,173 ms vs 4 ms). Object-keyed Maps are the default cache/registry idiom. Fix: a movable-safe identity hash (lazily assign a 32-bit id in the GcHeader spare bits, index by id).
  6. One user Object.freeze/defineProperty permanently disables the dynamic-write fast path process-wide. GLOBAL_DESCRIPTORS_IN_USE (descriptor_state.rs:86,338,468) gates transition_fast + the in-function transition-cache path (field_set_by_name.rs:54,916); perf(runtime,codegen): stop unrelated descriptor installs disabling the class-field inline fast path (#5654) #6057 fixed the read side only. Fix: gate on the receiver's OBJ_FLAG_HAS_DESCRIPTORS + the existing object_proto_may_intercept_key check instead of the global.

Confidence: high (source; items 1/2/5 also probe/measured).

Metadata

Metadata

Assignees

No one assigned

    Labels

    performanceRuntime, compile-time, build-size, or memory performancetriagedMaintainer reviewed; type, scope, and next step are clear

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions