ReviewOS

zig-utils/zig-js

Debugger: amortize shared statement-location registry reads

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

Parent: #531 Discovered while attributing: #532 Related: #501, #496, #493, #461

Problem

The shared-realm transition-publication benchmark still loses throughput beyond two lanes after Shape and ordinary-property lock work. A three-second sample of an amplified eight-lane run shows the dominant scheduled-yield stack in Interpreter.debugStatementLocation: the per-Interpreter 64-entry direct-mapped cache repeatedly aliases hot AST statement nodes, falls back to the Context-wide mutable location registry, and contends on DebugRegistryLock even though no debugger, host statement hook, or profiler hook is installed.

This is engine debugger/source-location state, not a JavaScript program race and not ordinary Object synchronization. Concurrent dynamic source/eval registration still mutates the Context registry, so removing its lock is not valid.

Scope

  • Add opt-in attribution for statement-location cache hits/misses plus debug-registry lock acquisitions, contention, and spins; normal execution must not pay atomic-counter overhead.
  • Freeze the existing #531 base/variant/contended source, jobs, timing boundary, and checksums as the end-to-end witness.
  • Replace pathological direct mapping with a bounded collision-resistant or set-associative Interpreter-local lookup that preserves exact node/location identity.
  • Keep registry mutation synchronized and reentrant for concurrent dynamic evaluation and inspector callbacks.
  • Preserve statement hooks, debugger statements, script IDs/URLs/start lines, stack locations, inspector resolution, safepoints, Context teardown, and OOM atomicity.

No-workaround rules

  • Do not omit statement-location metadata, skip serviceDebugStatement, disable hooks/safepoints, precompute benchmark nodes, recognize source/workload names, globally intern attacker-controlled source, or make the registry append without a safe publication design.
  • Do not modify stacks, queues, jobs, Worker lifecycle, or the user-owned #474/#482/#502 work.

Acceptance

  • Attribution lands before the cache change and exactly reports configuration/warmup/invocation deltas.
  • Base and anti-specialization rows show materially fewer registry acquisitions/contentions at 1/2/4/8 lanes; contended Shape behavior remains visible and unchanged.
  • Cache storage is fixed and bounded per Interpreter, with adversarial collision/eviction coverage and no attacker-controlled unbounded retention.
  • Concurrent dynamic eval/source publication, nested inspector reentrancy, exact statement hooks/locations, stack traces, and allocation failure pass.
  • Suppression-free TSan, no-GIL focused witnesses, threadfuzz, full units, and affected debugger/source-location test262 pass.
  • Exact-parent evidence reports wall/CPU/instructions, debug-lock work, allocation, GC, and RSS without treating profiler timings as scored evidence.
9 days ago

Initial diagnostic (not publication evidence): an amplified 8-lane / 100,000-jobs profile recorded 1,159 sampled leaves in swtch_pri/cthread_yield; most sampled yield stacks entered Interpreter.debugStatementLocation → lockDebugRegistry. The current cache is a bounded 64-entry direct-mapped table indexed from the AST-node address, so regular arena layouts can alias. Next step is instrumentation-only: opt-in cache hit/miss and registry lock contention counters owned by the Context, with no atomics in normal execution.

9 days ago

Instrumentation landed directly on main in 76591cad (bench(debugger): attribute registry contention (#541)). Counters are realm-owned and opt-in; ordinary execution retains a null sink.

ReleaseFast invocation deltas, 1,000 jobs per lane (diagnostic, not publication evidence):

workloadlanescache hitscache missesregistry acquirescontentionsfailed attempts
base16,99355,10955,11000
base213,999110,178110,1793,6085,048
base428,011220,316220,31721,58572,474
base856,035440,592440,59320,62376,675
variant16,99355,10955,11000
variant213,999110,178110,1792,2822,718
variant428,011220,316220,31714,72455,519
variant856,035440,592440,59359,2721,326,987
contended16,99355,10955,11000
contended213,999110,178110,1797741,187
contended428,011220,316220,31728,888111,975
contended856,035440,592440,59361,263441,486

The miss and acquisition counts are deterministic across the three workload variants and scale almost exactly with lanes. This isolates the statement-location lookup from #532’s fixed object-lock counts and #531’s shape-publication yields. Next I’m replacing the 64-entry direct-mapped cache with a bounded collision-resistant design and will compare against this exact parent.

9 days ago

Implemented and pushed to main in 59b34db6 (perf(debugger): cache absent statement locations (#541)).

What changed:

  • Cache immutable negative source-location lookups as well as positive locations. Parsed AST membership is fully published before evaluation, so both results are stable for a node; dynamically parsed scripts use newly published node identities.
  • Replace the direct-mapped cache with a fixed 16-set × 4-way design. Capacity remains 64 entries, allocation-free, with deterministic bounded eviction under adversarial collisions.
  • Keep the new counters inside the existing opt-in runtime attribution profiler. The first inline-counter attempt was rejected by the bounded-heap recovery gate because it enlarged every ordinary Context; ordinary contexts now retain their previous footprint.

ReleaseFast invocation deltas (diagnostic):

lanesbase acquisitions beforebase acquisitions afterafter / beforebase misses after
155,1101560.00283×155
2110,1792500.00227×249
4220,3174380.00199×437
8440,5938140.00185×813

The 8-lane contended control fell from 440,593 to 779 registry acquisitions. Checksums and the exact object-lock and shape-transition counts remained unchanged.

Seven order-balanced exact-parent 8-lane pairs against 76591cad (diagnostic, not publication evidence):

  • wall median: 41.381 → 23.501 ms (0.568×; RSD 10.77% / 13.13%)
  • instructions: 734,968,192 → 492,875,655 (0.6706×)
  • cycles: 349,543,025 → 205,237,035 (0.5872×)
  • energy: 0.21316 → 0.13172 J (0.6179×)
  • peak RSS: 44,695,552 → 44,711,936 bytes (1.0004×)
  • thermal: nominal → nominal; all checksums matched

Validation:

  • full unit gate: 1,601 passed, 1 skipped, 0 failed, 0 leaked
  • positive, negative, and four-way collision/eviction cache tests
  • bounded-heap recovery regression
  • concurrent dynamic-script publication normally and under TSan
  • opt-in attribution contract
  • conformance smoke: 33/33

No Promise, microtask, task, queue, stack, Worker, or job-lifecycle code was changed. Closing as complete.

Sign in to comment on this issue.