Reuse class member definitions across class re-evaluations#2615
Merged
Conversation
Every evaluation of a class - a factory function returning one, or a re-run prepared script - rebuilt a JintFunctionDefinition per method, accessor, field initializer and static block, and with it each member's body handler tree and inline caches. The per-engine definition cache introduced for hoisted declarations now also serves DefineMethod, MethodDefinitionEvaluation, class field initializers (including auto-accessor backing fields) and static blocks, keyed on their stable AST nodes; the dictionary key widened from FunctionDeclaration to Node. Closures, home objects, environments and private state stay strictly per evaluation: only the immutable definition (analysis + body handler tree) is shared, the ScriptFunction instances are created fresh with their own [[HomeObject]]/[[Environment]] as before. Tests pin factory-produced classes staying independent (identities, prototypes, method identities, per-class captured tags in field initializers and static blocks, private accessors), super binding per evaluation, and repeated prepared-script evaluation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
lahma
enabled auto-merge (squash)
July 10, 2026 05:37
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 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.
Follow-up to #2613, extending the per-engine function-definition cache from hoisted declarations to class evaluation. Real-world motivation: the dom.js / SimpleCrawler pattern from #2569, where a factory function (or a re-run prepared script) evaluates the same
classdeclaration over and over — every evaluation rebuilt a freshJintFunctionDefinition(and, lazily, a fresh body handler tree plus all its inline caches) for every method, accessor, field initializer and static block.What changed
The #2613 cache widens from
Dictionary<FunctionDeclaration, ...>toDictionary<Node, ...>behind two new internal accessors (TryGetFunctionDefinition/CacheFunctionDefinition, same 2048-entry backstop), and class evaluation now reuses cached definitions at all per-member sites:ClassDefinition.MethodDefinitionEvaluation— methods and accessorsAstExtensions.DefineMethod— object-literal and class method definitionPer-evaluation semantics are untouched: every evaluation still produces fresh
ScriptFunctionclosures, fresh class identity, fresh prototypes, per-evaluation private state and re-run static blocks — only the immutable interpreter definition (and its warm handler tree) is shared, the same object each call of one class's method already shared.Benchmarks
New
ClassBenchmark.ReEvaluateClassDeclarationsrow (one engine re-evaluating a prepared script declaring a 2-class hierarchy with methods, accessors, private field, static property and static block, 40k evaluations per op) — A/B against main in one window, base side = engine sources from main with the same benchmark row:¹ GetSet doesn't touch class evaluation and its allocation is byte-identical — the time swing is code layout/iteration noise on an
InvocationCount=1row, not a claimed win.PreparedAnomalyBenchmarksguard (the #2613 gate — the hoisted-declaration path shares the touched cache):All guard allocations byte-identical; times flat-to-slightly-better.
Tests
New
ClassReEvaluationTestspin the per-evaluation semantics (independent class identities/prototypes/private fields/static-block effects across evaluations of one AST, super/home-object correctness, repeated prepared-script evaluation).Full gate green:
Jint/Jint.csprojall TFMs, Jint.Tests (net10.0: 3266, net472: 3204), Jint.Tests.PublicInterface (82×2), full Test262 clean.🤖 Generated with Claude Code
https://claude.ai/code/session_01BY8HoKam5H8vTPn1bMY2Hv