ReviewOS

zig-utils/zig-js

Objects: amortize uncontended named-property synchronization

#532
Open chris opened this 8 days ago ·
8 days ago

Summary

Split from #531 after first-class Shape attribution proved that partitioned transition publication has zero per-Shape writer yields. The end-to-end shared-realm row remains limited by repeated ordinary-property synchronization around fresh, lane-local receivers.

Related: #478, #480, #471, #493. Parent benchmark/Shape work: #531.

Measured diagnosis

For 1,000 unique add/read/delete-to-base operations per lane, the 8-lane invocation records 8,016 Shape transition requests, 8,008 misses, and 0 transition writer yields. After private-activation Environment read elision and combined named-descriptor probes, the same 8,000 scored edges still record approximately:

  • 72,531 Environment binding-lock acquisitions;
  • 96,403 Object property-lock acquisitions;
  • 33,048 Object backing-lock acquisitions;
  • 24,073 Object element-lock acquisitions.

The combined descriptor snapshot lowers property-lock acquisitions from about 22.1 to 12.1 per edge, but quick wall samples remain noisy and aggregate 1/2/4/8 throughput is not monotonic. Lower counters alone are not acceptance evidence.

Requirements

  • Preserve exact OrdinaryGet/OrdinarySet/Delete semantics for accessors, attributes, prototype setters, Proxies, module namespaces, integer-indexed exotics, lazy function prototypes, host hooks, and JIT invalidation ordering.
  • Preserve engine-state race freedom under the no-GIL default and concurrent marking; ordinary JavaScript program races remain permitted as documented.
  • Do not use predictable hashing, disable locks globally, add benchmark-only fast paths, weaken validation, or hide work outside the claimed boundary.
  • Do not modify the user-owned stacks/queues/jobs work.

Acceptance

  • Committed base/variant/contended benchmark rows retain frozen checksums and equal timed work.
  • Partitioned total throughput is non-decreasing across 1/2/4/8 lanes on stable repeated samples; same-parent contention remains bounded and visible.
  • Exact attribution explains remaining Environment/property/backing/element acquisitions, contentions, worker CPU, allocation, GC, and RSS.
  • Shared/captured/root Environments and genuinely shared Objects keep synchronization; any private fast path has a written publication/revocation proof.
  • OOM rollback, bounded heap, moving/concurrent GC, same-name convergence, no-GIL TSan, threadfuzz profiles, and full units pass.
  • Object + Array-prototype + Proxy test262 show no regressions against the exact parent.
  • Exact-parent raw evidence and report are checked in before performance claims are published.
8 days ago

The separately reviewable Environment work discovered during #532 is complete in #533 / 6a6b508a. #532 remains open and scoped to ordinary named-property synchronization, JIT-safe mutation/publication ordering, and its own acceptance gates; no object fast path is being claimed from the Environment result.

8 days ago

Landed perf(object): snapshot named descriptors once (#532) on main. Ordinary named accessor/data/attribute observations now use one property-lock snapshot across [[Get]], prototype [[Set]], and receiver validation; indexed/lazy/Proxy/module/TypedArray and virtual Array/String descriptors retain explicit semantic paths. Exact 1,000-edge attribution preserved checksums while property-lock invocation deltas fell 104,376 -> 64,230 at 4 lanes and 208,688 -> 128,422 at 8 lanes (about 38.5%); Environment/backing counts were unchanged. Validation: full units 1,589/1 skipped/0 failed/0 leaked; two focused shared-property/JIT race TSan tests clean; threadfuzz 400/0; exact Object/Reflect/Proxy/Array/TypedArray/String test262 9,625 -> 9,625, 0 failures/flips. #532 remains open: monotonic end-to-end scaling and the remaining mutation/publication design are not yet proven.

8 days ago

Landed perf(object): fuse ordinary named mutations (#532) on main.

The same-receiver ordinary data-write path now revalidates accessor/data attributes and extensibility, then publishes the write under one property_lock when no native artifact is published. Ordinary named deletion similarly removes accessor/data representations in one transaction. Published-JIT mutation keeps the pre-mutation invalidation path; Proxy/module/host, canonical-index, TypedArray/String, lazy-function-prototype, and real-Array length behavior stays on the dedicated paths.

Exact-parent (2d895edb) 8-lane attribution for 8,000 scored representative_shape_transition_fanout edges, frozen checksum 18,026,000:

  • Object property-lock acquisitions: 128,422 -> 80,384 (-37.4%)
  • Environment locks: 16,340 -> 16,340
  • Object backing locks: 33,048 -> 33,048
  • Object element locks: 24,073 -> 24,073
  • Shape requests/hits/misses/writer yields: 8,016 / 8 / 8,008 / 0
  • Final variant allocation: 41,503 backing allocations, 41,402 GC cells; retained RSS 45,088,768 bytes

Final-code seven-sample medians and aggregate throughput:

lanesmedian nsedges/schecksum
13,584,292278,995.1501,500
26,225,166321,276.61,503,500
414,681,083272,459.55,009,000
830,657,959260,943.718,026,000

Compared with the exact-parent medians recorded in #531, elapsed time changes by +1.35% / -2.74% / -19.75% / -23.88% at 1/2/4/8 lanes. This is a material high-lane improvement, but total throughput still declines after two lanes. The monotonic-scaling acceptance item therefore remains unchecked and #532 stays open.

Validation on the final code:

  • Full units: 1,593 passed, 1 skipped, 0 failed, 0 leaked.
  • Focused no-GIL TSan: fused descriptors + transparent Proxy/Array boundary; shared named deletion/compaction; optimized reads racing delete/redefine/prototype replacement — clean.
  • threadfuzz: default 400/0 and verify 300/0.
  • Exact-parent Object + Reflect + Proxy + Array.prototype + TypedArray + String test262: 9,354 -> 9,354, 0 failures/flips.
  • OOM deletion rollback, descriptor/creation-order conversion, shared deep-shape convergence, targeted JIT retirement, and moving-GC named sidecars pass.

The exact-parent comparison caught and prevented an intermediate regression where transparent Proxy forwarding exposed real Array length to the ordinary delete path; the landed code has a dedicated regression and retains the Array exotic boundary.

8 days ago

The amplified 8-lane representative_shape_transition_fanout profile separated another shared bottleneck from named-property synchronization: repeated serviceDebugStatement source-location cache misses contend on Context.debug_registry_lock, even with no debugger hook installed. I split that independent, bounded cache/registry problem into #541. The existing attribution rows still show fixed named-property-lock acquisitions per edge, so #532 remains open and scoped to object synchronization; no stacks, queues, jobs, Promise, or Worker lifecycle code is part of this child.

8 days ago

#541 is complete in 59b34db6. On the 8-lane base invocation, debugger-registry acquisitions fell from 440,593 to 814 while the #532 counts remained exactly 80,384 named-property, 33,048 backing, 24,073 element, and 16,340 environment lock acquisitions. The checksum and 8,016 shape requests were unchanged.

This removes the dominant uncounted shared lock from the profile and sharpens #532: named-property acquisition is still fixed per published edge, but no longer hidden behind hundreds of thousands of debugger-registry entries. I’m continuing here without touching the separately owned stacks, queues, jobs, Promise, or Worker lifecycle areas.

8 days ago

Instrumentation landed on main in 8df448eb (bench(object): classify property lock acquisitions (#532)). Every opt-in property-lock acquisition is now exactly one of named descriptor snapshot, ordinary receiver set, ordinary named delete, or residual “other”; the attribution validator rejects rows whose categories do not sum to the aggregate.

ReleaseFast 1,000-job invocation counts (identical for base, variant, and contended controls):

lanestotalnamed snapshotsreceiver setsnamed deletesother
110,0766,0331,0051,0002,038
220,12012,0552,0072,0004,058
440,20824,0994,0114,0008,098
880,38448,1878,0198,00016,178

The distribution is deterministic and scales with equal work, while contention counts remain scheduling-dependent. Named snapshots are 59.9% of acquisitions; residual metadata locks are another 20.1%. The next change will take only atomic-null fast paths for absent accessor/attribute sidecars, retaining locks for every non-null mutable map. This is an engine-state-safe optimization, not a global lock disable or benchmark recognition.

8 days ago

Landed on main in 65df8bc8 (perf(object): elide absent sidecar locks (#532)).

The ordinary descriptor helpers now consume only an atomically published null sidecar without locking: null attributes mean the complete default descriptor, and a null accessor map means no accessor exists. Every non-null map lookup remains serialized against grow/mutation. A dedicated four-reader TSan witness covers concurrent first publication and permits only the pre-publication null/default state or the fully published state.

Exact invocation attribution:

lanestotal beforetotal afterother beforeother after
110,0769,0742,0381,036
220,12018,1164,0582,054
440,20836,2008,0984,090
880,38472,36816,1788,162

This removes exactly one lock per scored edge (−9.97% at eight lanes). Named snapshot, receiver-set, and named-delete counts and all frozen checksums are unchanged across base, variant, and contended controls.

Seven order-balanced exact-parent pairs per lane (diagnostic, not publication evidence):

lanesparent mediancandidate medianratioinstruction ratiopeak-RSS ratio
13.396 ms3.413 ms1.005×1.0001×0.9984×
24.635 ms4.656 ms1.005×1.0079×0.9979×
47.123 ms7.240 ms1.017×1.0007×0.9979×
835.505 ms29.192 ms0.822×0.9780×0.9996×

Low-lane changes are within noise; the eight-lane wall/cycle improvement is also noisy. Final candidate throughput is approximately 293k / 430k / 552k / 274k edges/s at 1/2/4/8 lanes, so the required monotonic scaling is still not achieved and #532 remains open.

Validation:

  • full units: 1,603 passed, 1 skipped, 0 failed, 0 leaked
  • null fast-path/populated fallback unit and concurrent first-publication TSan witness
  • exact-parent test262: Object 3,411 → 3,411; Array 3,081 → 3,081; Proxy 311 → 311; zero failures/flips
  • exact-parent benchmark identities, checksums, dependency revisions, and nominal thermal states matched

No stacks, queues, jobs, Promise, microtask, or Worker-lifecycle code changed.

8 days ago

Landed perf(object): publish immutable shape misses (#532) on main.

Ordinary named descriptor probes can now return an absent result without property_lock when both facts are complete: the accessor sidecar is atomically null and an acquire-loaded, immutable Shape ancestry has no matching name. Shape-changing writers release-publish only after slot initialization. Present descriptors still enter property_lock, recheck accessors, and reuse the optimistic slot only when the locked Shape pointer is unchanged; every race falls back to the full locked lookup.

The evidence rejected two broader versions before landing this one: a mirrored Shape pointer widened every GC Object allocation from 128 to 136 bytes, and making every Shape read atomic plus repeating present lookups increased instructions by 2-4%. The final version keeps Object at 96 bytes in the 128-byte slab and limits atomic operations to Shape publication and the absent probe.

Exact invocation attribution (1,000 jobs per lane):

lanestotal beforetotal afternamed snapshots beforenamed snapshots after
19,0747,0696,0334,028
218,11614,10612,0558,045
436,20028,18024,09916,079
872,36856,32848,18732,147

This removes approximately two absent snapshot locks per scored edge. Receiver sets (8,019 at eight lanes), named deletes (8,000), residual property locks (8,162), backing/element/environment acquisitions, 8,016 Shape requests, and frozen checksums are unchanged across base, variant, and contended controls.

Seven order-balanced exact-parent pairs per lane are diagnostic only. Final instruction ratios are 0.9979x / 1.0056x / 0.9906x / 1.0019x at 1/2/4/8 lanes; peak-RSS ratios are 0.9913x / 0.9986x / 0.9916x / 1.0110x. Wall rows remain noisy and do not prove monotonic throughput, so #532 stays open and no public performance claim is made.

Final validation:

  • full units: 1,605 passed, 1 skipped, 0 failed, 0 leaked
  • focused four-reader mutation/publication TSan witness: clean
  • threadfuzz default 400/0 and verifier 300/0
  • exact-parent test262: Object 3,411 -> 3,411; Array 3,081 -> 3,081; Proxy 311 -> 311; zero failures/flips
  • Object remains in the 128-byte GC slab; bounded heap, moving/concurrent GC, C-API protection, and no-GIL lifecycle tests pass in the full suite

No stacks, queues, jobs, Promise, microtask, Worker-lifecycle, or user-owned documentation paths changed.

8 days ago

Landed perf(object): publish exact accessor snapshots (#532) on main.

A post-178c50f1 amplified sample initially appeared to implicate concurrent GC holding property_lock; I implemented and TSan-tested that bounded inline-edge snapshot as #542, then rejected and reverted it when the worker-yield stack remained. The actual receivers were shared prototype objects with non-null accessor maps: unrelated-name probes still serialized before immutable Shape absence could decide the descriptor. #542 records that rejected path and is closed with no code landed.

The landed design adds a single-word reader/writer gate to the existing cold accessor sidecar. Exact-name readers copy Accessor values concurrently; property_lock continues to serialize writers, while the gate makes grow/update/delete and descriptor-cell publication wait for current readers and prevents new readers from entering. Ordinary descriptor misses can now combine an exact accessor miss with the acquire-published immutable Shape. No global lock was disabled and no benchmark-specific path was added.

Frozen 8-lane 1,000-job invocation (checksum = 18,026,000):

  • broad property-lock acquisitions: 56,328 -> 48,312
  • named descriptor snapshots: 32,147 -> 24,131
  • receiver sets / named deletes / residual other: 8,019 / 8,000 / 8,162, unchanged
  • one diagnostic incremental pair: contention/spins 347 / 1,496 -> 2 / 60 (scheduling-dependent, not a stable claim)
  • amplified CPU sample: the previous 1,617 scheduler-yield leaves disappeared; the remaining top leaves are Shape lookup, allocation ownership, debugger location lookup, and ordinary interpreter work

Seven order-balanced 8-lane diagnostic pairs were wall-neutral: parent median 19.111 ms, candidate 19.277 ms (1.009x, noisy). Median retired instructions improved from about 496.5M to 490.6M (0.988x). No public throughput claim is made, and #532 stays open because monotonic 1/2/4/8 scaling is still unproven.

Validation:

  • full units: 1,606 passed, 1 skipped, 0 failed, 0 leaked
  • focused four-reader accessor grow/update/delete TSan witness: clean
  • focused accessor/OOM/GC group: 19/19
  • threadfuzz: default 400/0 and verifier 300/0
  • exact-parent test262: Object 3,411 -> 3,411; Array 3,081 -> 3,081; Proxy 311 -> 311; zero failures/flips
  • Object remains in the 128-byte slab; ObjectColdState remains within 256 bytes

No stacks, queues, jobs, Promise, microtask, or Worker-lifecycle code changed.

8 days ago

The final accessor-gate implementation in d437b5fc removed the measured scheduler-yield property-lock hotspot: the partitioned 8-lane exact invocation now records 48,312 property-lock acquisitions with only 2 contentions / 2 spins, and the post-change collapsed sample contains no swtch_pri leaves.

However, seven-sample base/variant/contended scaling still falls at eight lanes and is noisy at 4/8 lanes. The base scales 1.000x / 1.400x / 1.709x / 1.186x; the anti-specialization variant 1.000x / 1.465x / 1.842x / 1.211x; the same-parent control 1.000x / 1.404x / 1.641x / 1.042x. All frozen checksums match. These are diagnostics, not publishable performance evidence.

#543 tested the next plausible duplicate Shape-state lookup with an exact-parent A/B. It produced no consistent wall/counter gain and regressed the 8-lane contended median 4.5%, so it was fully reverted. This leaves #532's synchronization implementation causally successful but its inherited monotonic-throughput acceptance unsatisfied; I am keeping the issue open and will not claim completion from lower lock counts alone.

8 days ago

Current-main sampling confirms the remaining eight-lane ceiling is outside the property synchronization work completed here. In an amplified exact Shape-publication invocation, GcCellBacking.allocFn was the top collapsed leaf (2,649 samples), with 1,085 scheduler-yield leaves and 511 frees; prior property contention remains near zero. The distinct bounded allocator problem is now #559. #532 remains open for its inherited monotonic gate, but I will not force another object-lock change from this profile.

8 days ago

Fresh current-main (e96196c8) seven-sample diagnostics after #559 still do not satisfy the inherited monotonic-throughput gate. Frozen checksums remain exact.

row1 lane2 lanes4 lanes8 lanes
base edges/s458,234768,246995,118939,073
variant edges/s461,983813,7661,186,005907,574
contended edges/s452,455743,748887,262644,034

Median base elapsed is 2.182 / 2.603 / 4.020 / 8.519 ms; variant 2.165 / 2.458 / 3.373 / 8.815 ms; contended 2.210 / 2.689 / 4.508 / 12.422 ms. The property-lock contention work remains causally complete, but eight-lane total throughput still declines. I am keeping #532 open rather than forcing another object synchronization change. The #531 borrowed Shape-key candidate did restore monotonic scaling, but remains correctly blocked on the deterministic #482 lifecycle failure owned by the separate jobs/Worker workstream.

8 days ago

Related cold-start child #663 owns the separate uncontended initialization case: publish a low-level data value plus final attributes under one property_lock transaction with exact OOM rollback. It does not alter ordinary shared-object synchronization or #532's monotonic scaling gate.

8 days ago

Acceptance metadata reconciliation only: the committed benchmark boundary (d9d54b5b, d30790f2, 6236f4be), exact attribution (8df448eb plus the linked 1/2/4/8 counts), synchronization/publication proofs (65df8bc8, 178c50f1, d437b5fc), full OOM/GC/no-GIL/TSan/threadfuzz/unit gates, and exact Object/Array/Proxy Test262 comparisons already prove five boxes. I checked those without changing the implementation. The issue stays open: stable monotonic 1/2/4/8 throughput remains unproven, and no checked exact-parent raw artifact/report exists. The latest profile also ties the inherited eight-lane ceiling to #531/#482, so no further object-lock change is justified from current evidence.

8 days ago

The inherited monotonic-throughput gate advanced on main in 88cde689 (perf(object): borrow immutable shape order keys (#531, #532)). Ordinary data creation-order entries now borrow immutable Context-arena Shape names; accessor/native entries retain owned copies, and all teardown paths distinguish ownership.

This does not weaken or bypass the named-property synchronization design already completed here. Exact eight-lane attribution preserves 6,400,191 property-lock acquisitions with negligible scheduling-dependent contention and zero scored collections, while backing allocation/release calls fall from 1,725,044 / 1,725,008 to 125,036 / 125,000. The independent base and anti-specialization rows are now monotonic across 1/2/4/8 lanes; the deliberately serialized control remains bounded and visibly non-scaling.

The full semantic/concurrency gates and exact Object/Array/Proxy Test262 comparisons are recorded on #531. These measurements remain diagnostic only. #532 stays open until the required exact-parent raw/report artifacts are checked in; no public performance claim is being made yet.

8 days ago

Shape transition counters are no longer dropped by the representative evidence pipeline: #844 landed as a6feac57, backed by #845/#846. The V33 quick artifact retained exact request/hit/miss/writer-yield maps across all 510 snapshots and generated the shared-realm Shape report while keeping all prior gates strict. Publication-grade quiet-host evidence remains pending under #531.

Sign in to comment on this issue.