ReviewOS

zig-utils/zig-js

Runtime: release own-key enumeration indexes

#521
Closed chris opened this 9 days ago ·
9 days ago

Parent: #480 Related: #493, #460, #459

Defect

Interpreter.objectOwnKeysList and proxyOwnKeys build non-owning integer/string membership tables from the realm arena for each enumeration. The returned ordered key slices and formatted index strings legitimately retain their arena lifetime, but the hash buckets are temporary. Repeated Reflect.ownKeys, Object.keys/values/entries, JSON object enumeration, or Proxy ownKeys invariant validation therefore retain historical bucket capacity until realm teardown.

This is user-controlled memory growth on a security-sensitive reflection boundary. Proxy trap ordering, duplicate detection, configurability/extensibility invariants, integer-index ordering, Symbols, hidden keys, host hooks, arrays, typed arrays, and function prototype materialization must remain exact.

Required change

  • Add ordinary-object, array/sparse-index, and Proxy growth/plateau witnesses with exact ordered checksums.
  • Route only non-owning membership buckets through Context-owned freeable scratch backing and deinitialize them on success and every abrupt path.
  • Keep returned key bytes/slices and all observable property/trap results arena-owned for their required lifetime.
  • Preserve no-GIL allocator synchronization and add normal/TSan concurrent coverage.
  • Record exact-parent CPU and peak/retained-memory evidence; retain only demonstrated improvements.
  • Run Reflect/Object/Proxy/JSON test262 subtrees with no regressions.

No-workaround rules

No key-count limit, skipped invariant, altered key order, hidden Proxy trap, global mutable cache, system runtime dependency, or benchmark-only path.

9 days ago

Implemented and verified on main.

Commits:

What changed:

  • Context-backed, freeable scratch now owns only transient own-key membership tables. Returned key slices/bytes and observable results retain arena lifetime.
  • Ordinary key-order enumeration builds one live-key state index, walks historical order backward to preserve delete/re-add creation order, and avoids per-entry linear shape lookup.
  • Proxy duplicate detection reuses the same table for configurability/extensibility invariants instead of retaining a second table.
  • Reflection, for-in/enumeration, sparse-array helpers, seal/freeze, Symbol recovery, and inspector property reads all use the same scratch contract.
  • Every temporary table has defer cleanup on success, OOM, trap throw, and invariant throw paths. The Context allocator retains its existing parallel-JS synchronization ruling.

Exact-parent evidence (14ffcdb3 -> e9683798, seven order-balanced pairs, identical checksums):

  • ordered 1024: 2.089 -> 0.194 ms; 45,259,190 -> 2,837,724 instructions
  • ordered 2048: 11.328 -> 0.404 ms; 128,626,972 -> 5,596,123 instructions
  • ordered 4096: 58.745 -> 0.670 ms; 600,484,219 -> 11,161,500 instructions
  • sparse-array control: 0.534 -> 0.528 ms (0.988x)
  • Proxy invariant control: 20.218 -> 20.078 ms (0.993x)
  • 4096-key live snapshot after ten warmups: retained RSS 31,408,128 -> 27,361,280 bytes; peak RSS 31,506,432 -> 27,426,816 bytes

Evidence:

Correctness and concurrency:

  • full sharded unit suite: 1548 passed, 1 skipped, 0 failed, 0 leaked
  • focused normal and TSan semantic/ownership tests: pass, zero leaks
  • TSan parallel-JS synchronized Object backing allocator witness: pass (10.0 s), zero races/leaks
  • test262 exact comparison: Reflect/ownKeys 13 -> 13; Object/keys 59 -> 59; Proxy/ownKeys 27 -> 27; JSON/stringify 66 -> 66; flips 0 in every subtree
  • benchmark-comparison structural/publication harness: pass

The separately discovered named-property deletion reconstruction cost remains tracked in #522 and was not hidden or worked around here.

Sign in to comment on this issue.