Fix prepared script performance regression from shared identifier cache thrashing#2324
Merged
Conversation
…ifierExpression cache thrashing When using Engine.PrepareScript(), the AstAnalyzer stored JintIdentifierExpression instances in AST node UserData. Since JintExpression.Build() returns these cached instances directly, ALL references to the same identifier across all scopes and all engine instances shared a single JintIdentifierExpression with its mutable _cachedEnvironment field. Different scopes continuously overwrote each other's cached environment, causing the fast-path lookup to always miss. The fix stores only the immutable BindingName in UserData instead. Each engine execution now creates fresh JintIdentifierExpression instances (with independent environment caches) while still reusing the pre-computed BindingName to avoid repeated JsString/BindingName allocation. This also fixes a minor bug in the single-parameter JintIdentifierExpression constructor that created two redundant BindingName instances (one in the this() chain, one in the body). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…pattern Use the exact Handlebars template-rendering workload from JsExecutionHeavyBenchmark: 4 engine instances each loading the library and calling renderTemplate() with different content items (hello-world, contacts, js-engines, web-browser-family-tree). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This was referenced Mar 12, 2026
This was referenced Jun 8, 2026
This was referenced Jun 29, 2026
This was referenced Jul 7, 2026
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.
Summary
Engine.PrepareScript()+Execute(Prepared<Script>)was slower than executing from a raw stringJintIdentifierExpressioninstances caused environment cache thrashing across scopes and engine instancesPreparedScriptBenchmarkthat replicates the exact JavaScriptEngineSwitcherJsExecutionHeavyBenchmarkpattern: Handlebars template rendering on 4 separate engine instancesProblem
During
PrepareScript(), theAstAnalyzerstored fully-constructedJintIdentifierExpressioninstances in eachIdentifiernode'sUserData. TheJintExpression.Build()fast path returned these cached instances directly, meaning all references to the same identifier — across different scopes and different engine instances — shared a singleJintIdentifierExpressionwith its mutable_cachedEnvironmentfield.When the same identifier appeared in multiple scopes (e.g.
xinfunctionAandxinfunctionB), each scope's execution overwrote_cachedEnvironment, causing the fast-path lookup (ReferenceEquals(env, _cachedEnvironment)) to always miss. This turned the O(1) cached lookup into an O(n) scope chain walk on every access.The regression was visible in the JavaScriptEngineSwitcher benchmark where prepared Jint was 45% slower than unprepared (251ms vs 173ms), while every other engine showed preparation as faster:
Fix
Store only the immutable
BindingNamein UserData instead of the fullJintIdentifierExpression. SinceBindingNameis not aJintExpression, theBuild()shortcut no longer matches — each engine creates freshJintIdentifierExpressioninstances with independent environment caches. The pre-computedBindingName(containing the cachedJsStringandKey) is still reused fromUserData, preserving the allocation benefits of preparation.Also fixes a minor existing bug where the single-parameter
JintIdentifierExpressionconstructor created two redundantBindingNameinstances (one in thethis(...)chain, one in the body).PreparedScriptBenchmark (Handlebars template rendering, 4 engines)
This benchmark replicates the exact JavaScriptEngineSwitcher pattern: load
handlebars.js, then callrenderTemplate()across 4 engine instances with different content items.Before fix (baseline):
After fix:
The prepared path improved from 62.92 ms → 51.48 ms (18% faster) due to the elimination of environment cache thrashing. The prepared-to-string ratio improved from 0.69x to 0.52x.
Verification
Test plan
dotnet test --configuration Release Jint.Tests/Jint.Tests.csproj— all passdotnet test --configuration Release Jint.Tests.Test262/Jint.Tests.Test262.csproj— all passPreparedScriptBenchmark— prepared path 18% faster after fixDromaeoBenchmark— no regressions in any configuration🤖 Generated with Claude Code