ReviewOS

zig-utils/zig-js

Shapes: scale shared transition publication beyond two lanes

#531
Open chris opened this 10 days ago ·
10 days ago

Parent: #478 Split from: #530 Related: #461, #480, #493

Problem

#530 replaces deep linear lookup and predictable transition hashing with collision-free persistent AVL indexes. Its functional four-lane and suppression-free TSan witnesses pass, but a dedicated shared-realm publication diagnostic shows that total transition throughput stops scaling beyond two lanes even when lanes publish beneath independent parent shapes.

The pre-implementation fixture is intentionally split into:

  • partitioned base shapes: each lane publishes unique fanout edges beneath its own prepared parent shape in one shared Context;
  • anti-specialization variant: different base/key layout with identical logical work and checksum;
  • contended control: every lane publishes beneath one parent, preserving the serialized worst case.

Each invocation uses a fresh base-shape generation and reuses one object per lane, deleting back to the base after every unique publication. This removes per-iteration object construction while preserving real transition publication, exact own-key order, deletion, and deterministic checksums.

Scope

  • Attribute 1/2/4/8-lane time to Shape transition locks, Context arena allocation, object property locks, GC, and Thread lifecycle using shared attribution.
  • Remove the causal independent-parent bottleneck with bounded Context-owned storage and an explicit synchronization design.
  • Consider per-thread arena tranches, batched immutable-node allocation, or optimistic persistent-root publication only when exact profiles justify them.
  • Preserve same-name convergence, collision-independent exact keys, stable slots, delete/re-add undo, OOM atomicity, moving GC, and Context teardown.
  • Keep the same-parent contended row visible; do not describe intentional serialization as scalable.

No-workaround rules

  • No GIL, hidden serialization outside the measured lock, prepublishing scored keys, reusing a result cache, reducing logical work at higher lane counts, unbounded per-thread reserves, benchmark-name/source recognition, or excluding the 4/8-lane rows.
  • Do not trade exact same-name convergence or OOM rollback for duplicate visible shapes.

Acceptance

  • Benchmark source, base/variant/contended identities, jobs, checksums, warmup, and timed boundary are committed before the runtime fix.
  • Clean 1/2/4/8-lane order-balanced evidence reports transitions/second, instructions/transition, lock contention/wait, allocation bytes, GC, peak/retained RSS, and dispersion.
  • Partitioned total publication throughput is monotonic across 1/2/4/8 lanes and the anti-specialization variant agrees; every row retains identical per-lane work.
  • The contended row remains bounded and exact, with wait/serialization attributed rather than hidden.
  • Same-name convergence, disjoint fanout, deep delete/re-add, allocation-failure rollback, 32 MiB bounded heap, moving/parallel GC, threadfuzz, and suppression-free TSan pass.
  • Shallow classes, mixed application, dense Array, and #530 deep lookup rows do not regress materially; retained memory remains bounded and published.

Security boundary

Property names, fanout, and thread schedules are attacker-controlled. The fix must improve independent publication without reintroducing predictable collision behavior, races, half-published roots, or unbounded thread-local retention.

10 days ago

Benchmark-first commit landed on main as d9d54b5b: bench(shape): add shared transition publication rows (#531).

Frozen rows:

  • representative_shape_transition_fanout: one fresh prepared base shape per lane;
  • _variant: different base/key layout with identical logical work/checksum;
  • _contended: all lanes deliberately share one parent shape.

Each lane reuses one object, publishes a unique edge, validates exact own-key order/value, deletes back to the prepared base, and checks absence. For 1,000 publications/lane the exact 1/2/4/8 checksums are 503,500 / 1,507,500 / 5,017,000 / 18,042,000. The 18/18 benchmark structural gates pass.

No runtime fix is included in this commit. Quick diagnostics confirm the partitioned row stops scaling beyond two lanes; accepted measurements and causal attribution will follow from this frozen parent.

10 days ago

Benchmark boundary correction is now pushed in d30790f.

The committed row no longer charges Reflect.ownKeys or membership scans to each transition publication. Every operation still publishes a unique computed edge, validates the observed value, and validates deletion back to the prepared base shape; exhaustive key-order and absence semantics remain in the production unit suite.

For jobs=1000, the frozen exact checksums are now:

  • 1 lane: 501500
  • 2 lanes: 1503500
  • 4 lanes: 5009000
  • 8 lanes: 18026000

A quick parent diagnostic still loses aggregate throughput beyond two lanes (about 333k, 303k, 255k, and 137k publications/s at 1/2/4/8 lanes), so this commit is benchmark isolation only, not a runtime fix. I am tracing the remaining shared-realm environment/property ownership and synchronization costs before changing the engine.

10 days ago

A second benchmark isolation correction is pushed in 6236f4b.

Unique property-name strings are now constructed in the existing untimed __benchmarkPrepare phase, one immutable key array per lane. The scored worker boundary remains one unique Shape-edge publication, exact value observation, and delete-to-base validation per operation. This removes integer/string formatting and its GC/backing allocations from the claimed transition-publication work.

The benchmark matrix structural tests pass 18/18, and all 20 quick samples across 1/2/4/8 lanes retain the frozen checksums (501500, 1503500, 5009000, 18026000). No runtime fix is included in this commit.

10 days ago

Shape transition attribution is now first-class in 6ced428. Tier-attribution rows record requests, hits, misses, and writer yields alongside the existing Environment/Object/allocation/worker counters.

On the isolated 1,000-publication-per-lane row, the candidate records:

  • 1 lane: 1,002 requests / 1,001 misses / 0 yields
  • 2 lanes: 2,004 / 2,002 / 0
  • 4 lanes: 4,008 / 4,004 / 0
  • 8 lanes: 8,016 / 8,008 / 0

That evidence rules out contended per-Shape writer locks in the partitioned row. It instead exposed persistent-AVL path-copy amplification and the surrounding ordinary-property synchronization costs. The benchmark comparison self-tests and 18/18 representative matrix structural tests pass.

10 days ago

The Shape publication implementation is pushed in 2708a80.

The persistent AVL remains the deep-shape property lookup index. Transition fanout now uses a separate append-only concurrent skip list:

  • exact byte-key equality;
  • one node per published edge instead of copying an AVL spine;
  • one existing per-Shape writer lock for same-name convergence;
  • lock-free readers over release-published immutable forward links;
  • SipHash-2-4 secret level selection for attacker-controlled property names;
  • secure-entropy failure is fail-closed, matching collection hash seeding.

Validation:

  • Shape-focused units: 27 pass / 0 fail / 0 leak, including allocation-failure rollback, bounded + moving GC, concurrent same-name convergence, and shared-realm publication.
  • focused no-GIL TSan publication: pass (23.56 s), no race/leak report.
  • exact Shape-only test262: Object 3,411 + Array prototype 2,810 + Proxy 311 = 6,532 pass / 0 fail / 0 flips.

The partitioned JS row still does not meet the issue’s monotonic 1/2/4/8 throughput acceptance because surrounding ordinary-property synchronization remains dominant. I am keeping #531 open and continuing that work; the Shape writer-lock diagnosis itself is now closed by measured zero yields.

10 days ago

The measured non-Shape bottleneck is split into #532: ordinary named-property synchronization and its private/publication proof. #531 remains the parent end-to-end scaling issue; #532 owns the lock-amortization work needed to satisfy its remaining monotonic 1/2/4/8 acceptance. This split does not relax or remove any #531 gate.

10 days ago

The Environment portion of the measured synchronization cost is complete in #533 via 6a6b508a: exact invocation Environment locks fell about 90.4% at both 4 and 8 lanes with unchanged checksums, full units/TSan/threadfuzz/test262 gates green. #531 remains open because end-to-end monotonic 1/2/4/8 throughput is still not proven; ordinary named-property synchronization remains tracked in #532.

10 days ago

#532 made another measured step in 58d73f42: exact property-lock counts fell about 38.5% at both 4 and 8 lanes with unchanged workload checksums and full semantic/concurrency gates green. This is attribution evidence only. #531 stays open because its monotonic 1/2/4/8 throughput acceptance has not been satisfied.

10 days ago

Post-58d73f421558d3918de2e37af0e7ebbb6305f4c3 clean-main scaling check (ReleaseFast, representative_shape_transition_fanout, shared realm, 1,000 jobs, 7 samples/lane):

LanesMedian wall timeAggregate throughputChecksum
13,776,792 ns264,775.0 edges/s501500
26,937,250 ns288,298.7 edges/s1503500
426,778,541 ns149,373.3 edges/s5009000
863,276,458 ns126,429.3 edges/s18026000

The checksums are exact and all seven rows were valid at every lane count. The 1→2 lane result improves, but 4 and 8 lanes collapse, so #531 acceptance is not met and the issue remains open.

The same run shows remaining synchronization acquisition counts scale roughly linearly while recorded contention is low: 4 lanes had 64,230 property-lock and 36,299 environment-lock acquisitions; 8 lanes had 128,422 and 72,531 respectively. System CPU also rose from 16.987 ms at 4 lanes to 103.136 ms at 8 lanes. The aggregate environment metric currently mixes root, captured, other reads, writes, and marker synchronization; the next step is to split that attribution before changing another lock path.

These are diagnostic /tmp samples, not publication evidence.

10 days ago

Environment lock ownership attribution landed in 3577d307, and the private-write protocol landed in 2d895edb / #535.

Exact 8-lane ownership before #535 accounted for all 72,531 Environment acquisitions: 16,334 reads + 56,197 writes + 0 traces. Of the writes, 56,191 were uncaptured private activations. After #535, actual Environment acquisitions are 16,340 and those 56,191 private writes are recorded as guarded elisions (-77.47% locks), with checksum 18,026,000 preserved.

Seven-sample ReleaseFast representative_shape_transition_fanout, 1,000 jobs per lane:

lanesraw wall-ns samplesmedian wall-nsedges/schecksum
13,981,042; 3,527,416; 3,471,042; 3,521,209; 3,562,625; 3,894,541; 3,536,5003,536,500282,765.4501,500
26,343,667; 6,126,250; 6,208,042; 6,471,208; 6,400,833; 6,670,667; 6,401,8756,400,833312,459.31,503,500
417,075,167; 16,851,333; 17,122,459; 21,841,958; 18,293,292; 19,370,125; 19,815,12518,293,292218,659.45,009,000
843,629,958; 46,174,834; 30,229,834; 38,798,333; 30,726,416; 40,275,792; 42,883,04240,275,792198,630.518,026,000

Compared with the prior clean-main medians, wall time improved by approximately 6.4% / 7.7% / 31.7% / 36.4% at 1/2/4/8 lanes. The optimization is material, but #531 acceptance is not met: throughput still peaks at two lanes and declines at four and eight. Keeping this issue open for the next measured bottleneck.

10 days ago

#532 advanced the parent end-to-end row in dc919306. Exact 8-lane property-lock acquisitions fell 128,422 -> 80,384 (-37.4%) with checksum 18,026,000; Environment/backing/element counts stayed 16,340 / 33,048 / 24,073.

Final-code seven-sample medians are 3,584,292 / 6,225,166 / 14,681,083 / 30,657,959 ns at 1/2/4/8 lanes, or 278,995.1 / 321,276.6 / 272,459.5 / 260,943.7 aggregate edges/s. Relative to the prior exact parent in this issue, elapsed time changes by +1.35% / -2.74% / -19.75% / -23.88%.

This is a material four/eight-lane improvement, but #531 acceptance remains unmet because aggregate throughput still peaks at two lanes. The exact attribution and semantic/concurrency evidence are in the #532 update. I am keeping both issues open; I am also leaving Thread/Worker/job lifecycle changes to the separately owned #482/#502 work rather than folding them into the Shape/Object change.

10 days ago

Profiling the current representative fanout workload found that a substantial share of scheduler yields is outside shape publication: direct-mapped statement-location cache aliases repeatedly enter the shared debugger registry. That independent bottleneck is now tracked in #541. I’ll add opt-in counters before changing the cache, then re-run the same #531 attribution matrix so subsequent shape conclusions are based on separated costs.

10 days ago

#541 is complete in 59b34db6. The 8-lane base invocation now performs 814 debugger-registry acquisitions instead of 440,593, with identical checksum, object-lock counts, and shape counts. Seven exact-parent diagnostic pairs measured wall 0.568×, instructions 0.6706×, cycles 0.5872×, and peak RSS 1.0004×; variance is explicitly too high for publication evidence.

The shape attribution is now much cleaner: base/variant still publish 8,008 misses with zero shape-lock yields, while the deliberately contended control isolates writer yields. I’m returning to the independent object-synchronization child #532 before drawing another conclusion about the remaining >2-lane scaling ceiling.

10 days ago

Post-d437b5fc diagnostics confirm that #532 removed the measured property-lock contention, but #531's end-to-end monotonic gate is still not met.

Seven clean-main samples per lane, ReleaseFast, 1,000 jobs/lane:

workloadscaling at 1 / 2 / 4 / 8 lanes4-lane RSD8-lane RSD
partitioned base1.000x / 1.400x / 1.709x / 1.186x21.92%19.30%
anti-specialization variant1.000x / 1.465x / 1.842x / 1.211x19.45%19.00%
same-parent control1.000x / 1.404x / 1.641x / 1.042x19.44%24.50%

All checksums remained exact. These are noisy diagnostics, not publication evidence.

Exact invocation attribution on the partitioned 8-lane row records 8,016 transition requests / 8,008 misses / 0 Shape yields, 48,312 property-lock acquisitions with only 2 contentions / 2 spins, and zero backing/element contention. The benchmark's cooperative-GC telemetry records zero scored collections at every lane count, ruling out the sampled post-invocation nursery relocation work as the timed ceiling.

I tested the remaining apparent Shape.lookupState leaf separately in #543 by carrying proven immutable lookup state through OrdinarySet and named Delete. Seven order-balanced exact-parent pairs across all three controls showed no causal improvement and an eight-lane contended regression, so the patch was reverted and #543 closed as not planned. I am keeping #531 open, but will not make another Shape/Object change from this profile alone.

10 days ago

A current-main 1/2/4/8 diagnostic still scales through four lanes and falls at eight (about 282k / 391k / 487k / 377k edges/s). An amplified sample now identifies GcCellBacking single-cell allocation/free as the dominant collapsed path rather than Shape publication or named-property contention. I split that causal allocation problem into #559; #531 remains the end-to-end parent and none of its acceptance gates are relaxed.

10 days ago

Correction to the #559 split above: b3d41abe proved that the apparent GcCellBacking hotspot was generated by fixture preparation still running inside __benchmarkRunShared, plus preparation-triggered GC deferred into the next evaluation. #559 is now closed with that causal correction.

Under the corrected boundary, the 8-lane x 100000-job scored invocation preserves checksum 180002600000, performs zero collections, and records only 82 slab acquisitions across 800000 published edges (41 allocation, 33 publication, 8 ownership, no relocation/unpublication/free). The GC allocator is therefore ruled out for this row.

Corrected three-sample medians are 253,732,709 / 279,234,834 / 319,724,375 / 907,345,417 ns at 1/2/4/8 lanes, or approximately 394k / 716k / 1.25M / 882k aggregate edges/s. Scaling is substantially cleaner and remains strong through four lanes, but the four-to-eight fall still violates #531 acceptance. This issue again owns that residual end-to-end shape/publication analysis; no allocator sharding will be carried forward from the invalid profile.

10 days ago

An unlanded Object key-order candidate establishes a causal path to monotonic 1/2/4/8 partitioned throughput, but it is not ready to land because the required mid-GC lifecycle fuzz gate exposes a differential failure tracked on #482.

Candidate design: creation-order entries borrow immutable Shape operation-name slices owned by the Context arena; accessor/native names retain owned copies. Object and GC teardown free only owned entries. This removes one duplicate/free pair per ordinary add/delete without adding a cache, reserve, or alternate semantic path.

Exact 8034cdfa parent and candidate ReleaseFast binaries were pinned by SHA-256 (0e00bb5f… parent, 162bc678… candidate). Seven alternating fresh-process pairs, 100,000 exact publications per lane:

lanesparent mediancandidate mediancandidate edges/selapsed reductioncandidate scaling
1266,958,917 ns256,039,666 ns390,5644.09%1.00x
2287,952,416 ns265,633,834 ns752,9167.75%1.93x
4386,780,917 ns309,412,833 ns1,292,77120.00%3.31x
8988,117,833 ns523,440,084 ns1,528,35147.03%3.91x

Every sample preserved the frozen checksums 5000150000, 15000350000, 50000900000, and 180002600000. Three-sample candidate diagnostics also remained monotonic for the anti-specialization variant (about 376k / 722k / 1.31M / 1.87M edges/s). The deliberately same-parent control remained exact and visibly serialized (about 387k / 676k / 694k / 504k edges/s).

Exact eight-lane attribution preserved checksum 180002600000, zero scored collections, 82 slab acquisitions, 800,008 transition requests / 800,000 misses / 0 Shape yields, and the same Object lock counts. Delegated Object-backing allocations/releases fell from 1,725,043 / 1,725,016 to 125,035 / 125,008: 1.6 million fewer of each, or about 92.7%.

Green candidate gates:

  • full units: 1,634 pass / 1 skip / 0 fail / 0 leak;
  • suppression-free TSan named deletion, moving relocation, and parallel shared mutation: 5/5;
  • threadfuzz default: 400/400;
  • threadfuzz amplify: 400/400;
  • threadfuzz broad: 400/400.

Blocking differential:

  • clean 8034cdfa: full mid-GC seed-10 replay 45/45; focused midgcmicrotask 1 10 3/3;
  • candidate: focused seed 10 fails 2/2 with TypeError: Thread constructor requires a callable argument; the long mid-GC profile reported the same failure at seeds 10 and 46 before being stopped;
  • exact reproducer and ownership boundary are documented in #482.

The candidate is stashed, not committed or pushed. #531 remains open: the performance result is diagnostic, the full efficiency/RSS/non-regression publication matrix has not been accepted, and the no-workaround lifecycle gate must be green before this optimization can land.

10 days ago

#764 removes deterministic adversarial shard/bucket placement from shared string interning in 288b4893, while preserving the 16-way lock topology. This is a bounded security/contention hardening slice only; it does not satisfy #531's independent shape-transition scaling acceptance.

10 days ago

Acceptance metadata reconciliation only. The benchmark-first boundary is proven by d9d54b5b, d30790f2, and 6236f4be: all three identities, equal work, warmup/preparation boundary, and frozen 1/2/4/8 checksums are committed. The same-parent control is also exact and visibly serialized in every retained diagnostic, including the candidate matrix recorded above, so that acceptance row is proven. I checked only those two boxes. Clean publication-grade 1/2/4/8 evidence, landed monotonic partitioned scaling, the complete current-code lifecycle matrix, and published control/RSS evidence remain open; the causal key-order candidate is still correctly gated by #482.

10 days ago

The previously blocked borrowed-key implementation is now landed on main as 88cde689: perf(object): borrow immutable shape order keys (#531, #532).

Design and safety boundary:

  • ordinary data add/delete creation-order entries borrow the exact immutable Shape operation-name slice owned by the Context arena;
  • accessor/native names remain self-owned;
  • rollback, replacement, explicit teardown, and GC finalization free only owned entries;
  • no cache, prepublication, reserve, global lock change, or benchmark-recognizing path was added.

The historical #482 seed-10 blocker is resolved on the current candidate: full mid-GC is 45/45 normally and under TSan, plus three focused seed-10 reproductions. Full units are 2,236 passed / 1 expected skip / 0 failed / 0 leaked. Default, amplified, broad, verify, and lifecycle threadfuzz profiles pass; the focused named-delete/OOM/moving-GC TSan group is clean. Exact Test262 remains Object 3,411 -> 3,411, Array 3,081 -> 3,081, Proxy 311 -> 311, with zero failures and zero flips.

Current exact-parent diagnostics (100,000 publications/lane, frozen checksums) show the expected causal separation:

rowcandidate aggregate throughput at 1 / 2 / 4 / 8 lanes
partitioned base~619k / 1.105M / 2.158M / 2.998M edges/s
anti-specialization variant~647k / 1.185M / 2.315M / 3.313M edges/s
serialized control~630k / 906k / 897k / 707k edges/s

At eight lanes, exact invocation attribution for both independent fixtures changes backing allocation/release calls from 1,725,044 / 1,725,008 to 125,036 / 125,000 and removes 41,422,408 bytes of matched allocation/free traffic. Net live backing-byte change, transition work, property-lock acquisitions, and scored GC collections remain unchanged; the serialized control is wall-neutral at scale. Shallow classes and the mixed application row are neutral-to-better; the dense-Array control is +0.92% wall with neutral-to-better instructions/cycles/energy/RSS.

These are clean diagnostics, not publication evidence. #531 remains open until the seven-pair quiet-reference 1/2/4/8 base/variant/control artifacts, exact attribution/RSS report, and retained deep-lookup control are checked in. No public performance claim is made from these /tmp results.

10 days ago

The standard publication path can now carry this issue exact Shape evidence: #844 landed as a6feac57, with supporting fail-closed runtime-profiler repairs #845 (928d5797) and #846 (61ec0d95). A complete V33 quick collection accepted 510/510 snapshots and rendered request/hit/miss/writer-yield tables without relaxing allocation, synchronization, GC, RSS, tier, or checksum gates. This issue remains open for the quiet-reference publication artifact and retained deep-lookup row.

Sign in to comment on this issue.