Skip to content

Reuse class member definitions across class re-evaluations#2615

Merged
lahma merged 1 commit into
sebastienros:mainfrom
lahma:perf-class-definition-reuse
Jul 10, 2026
Merged

Reuse class member definitions across class re-evaluations#2615
lahma merged 1 commit into
sebastienros:mainfrom
lahma:perf-class-definition-reuse

Conversation

@lahma

@lahma lahma commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

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 class declaration over and over — every evaluation rebuilt a fresh JintFunctionDefinition (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, ...> to Dictionary<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 accessors
  • class field initializers and auto-accessor backing initializers
  • static initialization blocks
  • AstExtensions.DefineMethod — object-literal and class method definition

Per-evaluation semantics are untouched: every evaluation still produces fresh ScriptFunction closures, 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.ReEvaluateClassDeclarations row (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:

Method main PR Δ time main alloc PR alloc Δ alloc
ReEvaluateClassDeclarations 524.5 ms 404.7 ms −22.8% 967.07 MB 641.05 MB −33.7%
ConstructSimple 157.5 ms 157.4 ms −0.1% 366.15 MB 366.15 MB =
ConstructDeepInheritance 154.6 ms 154.5 ms −0.1% 278.88 MB 278.88 MB =
GetSet 190.4 ms 152.8 ms −19.7%¹ 556.89 MB 556.89 MB =

¹ GetSet doesn't touch class evaluation and its allocation is byte-identical — the time swing is code layout/iteration noise on an InvocationCount=1 row, not a claimed win.

PreparedAnomalyBenchmarks guard (the #2613 gate — the hoisted-declaration path shares the touched cache):

Method main PR Δ time Allocated
SourcePerOp 13.90 ms 13.79 ms −0.8% 45.24 KB (=)
PreparedShared 14.12 ms 13.89 ms −1.6% 23.68 KB (=)
PreparedPerOp 14.11 ms 13.87 ms −1.7% 46.8 KB (=)
PreparedReusedEngine 16.55 ms 15.57 ms −5.9% 9.99 KB (=)

All guard allocations byte-identical; times flat-to-slightly-better.

Tests

New ClassReEvaluationTests pin 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.csproj all 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

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
lahma enabled auto-merge (squash) July 10, 2026 05:37
@lahma
lahma merged commit 77b1563 into sebastienros:main Jul 10, 2026
4 checks passed
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>
@lahma
lahma deleted the perf-class-definition-reuse branch July 13, 2026 06:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant