Re-resolve slot locations when a shared handler tree changes function instance#2616
Merged
Merged
Conversation
lahma
enabled auto-merge (squash)
July 10, 2026 06:30
… instance Per-engine definition reuse (sebastienros#2613) shares one handler tree across the fresh function instances a re-evaluated prepared script creates. SlotLocationCache cached the first instance's environment and returned a permanent miss once that environment became unreachable, silently disabling every slot-backed fast lane - unboxed loop counters, comparison/equality lanes, sum-of-products - from the second re-evaluation onward (a JsNumber materialization per identifier read in tight loops). On an unreachable cached environment, fall through and re-resolve against the current chain exactly like the cross-engine Prepared<Script> path; one bounded walk per instance switch, subsequent reads hit the re-populated cache. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BY8HoKam5H8vTPn1bMY2Hv
lahma
force-pushed
the
fix-slot-cache-shared-trees
branch
from
July 10, 2026 06:55
4869229 to
4f23f0f
Compare
lahma
added a commit
that referenced
this pull request
Jul 10, 2026
Full re-run after the 2026-07-10 batch (#2613, #2615-#2620). Highlights vs the 2026-07-09 refresh: dromaeo-string-base64 allocation -30% (2.30 -> 1.62 MB/op) and -modern -4.3% time, the classic row now ranked first outright (previously tied with NiL.JS) - loop tests of the form i < s.length read the length unboxed through the comparison lane (#2617). The batch's engine-reuse wins (#2613/#2615 definition reuse, #2616 slot-cache lane restoration) target long-lived-engine embeddings and are not visible in this fresh-engine-per-op table. Everything else moved within documented noise bands; competitor rows served as thermal canaries (times within +/-3%, allocations byte-identical). Claude-Session: https://claude.ai/code/session_01BY8HoKam5H8vTPn1bMY2Hv Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This was referenced Jul 10, 2026
lahma
added a commit
that referenced
this pull request
Jul 10, 2026
… epoch (#2625) Global reads from nested lexical scopes (loop bodies, closures) validate through TryGetValidatedGlobalDescriptorNested, which re-walks up to four environments with per-hop shadow probes on EVERY read - the dominant cost difference between the stopwatch-modern and classic variants, whose most-referenced binding (sw) is a global reached from two levels of loop scope, ~3 reads x 391k iterations per op. The walk's outcome is now memoized per identifier node: the start environment and every intermediate link are pinned by identity, and a repeat read from the identical chain revalidates with reference compares instead of HasBinding scans. Chain-link identity (not just the start) is required because pooled loop environments are re-attached under different outers across entries. The one mutation identity cannot see - a binding injected into a pre-existing pinned environment - is covered by a new Engine._envBindingInjectionEpoch, bumped only at the injection sites: sloppy direct eval instantiation and the AnnexB block-function var-scope copies. Creates into fresh environments never bump (a fresh environment cannot appear in a previously pinned chain). The existing global shape/lexical version checks run before the memo and cover all global-side mutations; a miss falls through to the walk and re-publishes, never permanently declining (the #2616 pattern). Serves reads, writes and the unboxed numeric lanes through the shared validator. Claude-Session: https://claude.ai/code/session_01BY8HoKam5H8vTPn1bMY2Hv Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
#2613 shares one handler tree across the fresh function instances that a re-evaluated prepared script creates — but
SlotLocationCachestill assumed a node lives and dies with a single function instance. The cache stored the first instance's environment, and once that environment was no longer reachable from the current chain,TryResolvereturned a permanent miss: it never fell through to re-resolution.Net effect on main today: from the second re-evaluation of a prepared script onward (i.e. the second use of the #2613-cached tree), every slot-backed fast lane in that script is silently dead — unboxed loop counters, the comparison/equality lanes, fused modulo, sum-of-products — and every identifier read in a tight loop materializes a
JsNumberthrough the generic path. The engine-reuse embedding scenario #2613 targeted is exactly the scenario that hits it.How it surfaced: an A/B for a new lane showed its target row allocation-unchanged on both sides. A per-evaluation allocation probe with lane fire counters showed fires = 100k on evaluations 1–2, then 0 forever, with the allocation cliff at evaluation 3; decline-site counters pointed at the left slot resolve; the permanent-miss return in
TryResolvewas the mechanism.The fix
When the hit-walk exhausts without reaching the cached environment (and without meeting a shadowing binding or a with-object — those still decline this read), fall through to
ResolveAndPopulateand re-cache against the current chain — exactly what the existing cross-enginePrepared<Script>path already does on an engine-identity mismatch. Cost: one bounded, pure walk per instance switch; subsequent reads hit the re-populated cache. The fall-through is also cheaper than the old behavior on the miss path itself, which re-walked and declined on every read.Benchmarks
LoopDispatchBenchmarks(one engine re-evaluating each prepared loop; every measured iteration past the first is a re-evaluation, so main's numbers are the degraded-lane floor):EmptyLoop returns to ~11 ns/iteration — the floor these lanes were built to deliver; on main every row was paying a JsNumber materialization per identifier read from the second evaluation on.
¹ StringAppend's remaining allocation is the rope builder for the 100k-char result string, not lane churn.
PreparedAnomalyBenchmarksguard (the #2613 gate):² Within this row's run-to-run band; allocations byte-identical across all four rows.
Tests
New
SlotLocationCacheTests: correctness pins for repeated prepared evaluations and for two function instances alternating over one shared tree, plus an allocation pin (net10.0) that fails on main (626 KB/evaluation for a 20k-iteration counter loop) and passes with the fix.Full gate:
Jint/Jint.csprojall TFMs, Jint.Tests (net10.0 + net472), Jint.Tests.PublicInterface (net10.0 + net472), full Test262 (single failure =Atomics/waitAsync/bigint/no-spurious-wakeup-no-operation.js, the documented wall-clock flake family — 8/8 green re-run in isolation, 88 tests per run).🤖 Generated with Claude Code
https://claude.ai/code/session_01BY8HoKam5H8vTPn1bMY2Hv