Reuse hoisted function definitions across re-evaluations on an engine#2613
Open
lahma wants to merge 1 commit into
Open
Reuse hoisted function definitions across re-evaluations on an engine#2613lahma wants to merge 1 commit into
lahma wants to merge 1 commit into
Conversation
Declaration instantiation constructed a fresh JintFunctionDefinition for every hoisted function on every evaluation - and the definition owns the lazily-built body handler tree, so re-running a prepared script rebuilt each called function's interpreter subtree and reset every per-node inline cache. Function-level and block-level DI paid the same cost per call for inner declarations. Definitions are now cached per engine, keyed on the declaration AST node: - a strong engine-owned Dictionary, NOT the AST's shared UserData State (handler trees accumulate engine-affine cache entries; sharing them across engines retains dead engines - the SharedPreparedScriptDoesNotRetainEngines tripwire) and NOT a ConditionalWeakTable (a dependent handle keeps its value alive on KEY reachability alone, so with the key rooted by a live Prepared<Script> the value->inline caches->engine chain pins the dropped engine) - the global-DI site engages the cache only on RE-evaluation of a script this engine has seen (one HashSet probe), so fresh-engine-per-run hosts execute the exact uncached path; block/function-level DI sites cache unconditionally since repeated calls within a single evaluation already amortize them - a 2048-entry reset backstops hosts streaming endless distinct sources through one engine Measurements (default job; the whole window drifted uniformly +5-8%, so in-window ratios and the deterministic allocation columns carry the signal): - new PreparedAnomalyBenchmarks.PreparedReusedEngine (the cached- Prepared embedding pattern): allocation 14.61 -> 9.99 KB/op (-32%), in-window time ratio 1.10x -> 1.08x vs SourcePerOp - diagnostic probe (small function-declaring script, engine reuse): 3,288 -> 1,740 B/op (-47%) - every fresh-engine row allocation-byte-identical (SourcePerOp / PreparedShared / PreparedPerOp / LinqJs / EvalExecution) Full Jint.Tests green including all GarbageCollectionTests retention pins on both TFMs. 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.
What
Declaration instantiation constructed a fresh
JintFunctionDefinitionfor every hoisted function onevery evaluation — and the definition owns the lazily-built body handler tree, so re-running a
prepared script rebuilt each called function's interpreter subtree and reset every per-node inline
cache. Function-level and block-level DI paid the same cost per call for inner declarations.
Definitions are now cached per engine, keyed on the declaration AST node.
Why this exact shape (two rejected designs)
State: handler trees accumulate engine-affine cache entries,and sharing them across engines retains dead engines —
GarbageCollectionTests.SharedPreparedScriptDoesNotRetainEnginesfailed immediately.ConditionalWeakTable: a dependent handle keeps its value alive on key reachabilityalone, so with the key rooted by a live
Prepared<Script>, the value → inline caches → enginechain pins the dropped engine — the same test failed again, in the opposite direction of the
naive mental model.
Dictionary(dies with the engine; retention scopedto engine lifetime, mirroring
Realm._templateMap), with the global-DI site engaging the cacheonly on re-evaluation of a script this engine has seen (one
HashSetprobe) — sofresh-engine-per-run hosts execute the exact uncached path. Block/function-level DI sites cache
unconditionally since repeated calls within one evaluation already amortize them. A 2048-entry
reset backstops hosts streaming endless distinct sources through one engine.
Results
Default job; the measurement window drifted uniformly +5–8% across all rows (including untouched
guards), so in-window ratios and the deterministic allocation columns carry the signal:
Prepared<Script>)Gating
All-TFM build ✅ · Jint.Tests ×2 ✅ (including all GC retention pins) · PublicInterface ×2 ✅ ·
Test262 99,426/0 ✅
🤖 Generated with Claude Code