Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions Jint.Benchmark/ClassBenchmark.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,38 @@ public void GetSet()
_engine.Evaluate(script);
}
}

[Benchmark]
public void ReEvaluateClassDeclarations()
{
// the dom.js / class-factory shape: one engine re-evaluates the same prepared script that
// declares classes; member definitions come from the per-engine cache while class
// identities, prototypes, private state and static-block effects stay per-evaluation
var script = Engine.PrepareScript("""
(function () {
class Node {
#tag = 'node';
static VERSION = 1;
static { this.registry = []; }
constructor(name) { this.name = name; this.children = []; }
appendChild(child) { this.children.push(child); return child; }
get childCount() { return this.children.length; }
set alias(value) { this.name = value; }
describe() { return this.#tag + ':' + this.name; }
static create(name) { return new Node(name); }
}
class Element extends Node {
describe() { return 'element:' + super.describe(); }
}
const e = new Element('div');
e.appendChild(Node.create('span'));
e.alias = 'main';
return e.describe() + '/' + e.childCount + '/' + Element.VERSION;
})();
""");
for (var i = 0; i < 40_000; ++i)
{
_engine.Evaluate(script);
}
}
}
80 changes: 80 additions & 0 deletions Jint.Tests/Runtime/ClassReEvaluationTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
namespace Jint.Tests.Runtime;

/// <summary>
/// Pins the semantics of the per-engine function-definition cache applied to class evaluation:
/// re-evaluating the same class AST (factory functions, re-run prepared scripts) reuses the
/// members' interpreter definitions, while closures, home objects, environments and private
/// state stay strictly per evaluation.
/// </summary>
public class ClassReEvaluationTests
{
[Fact]
public void ClassFactoriesProduceIndependentClasses()
{
var engine = new Engine();
var result = engine.Evaluate("""
(function () {
function make(tag) {
return class {
static origin = tag;
#secret = tag + '!';
constructor() { this.made = tag; }
get secret() { return this.#secret; }
describe() { return tag + ':' + this.made; }
static { this.stamped = tag.toUpperCase(); }
};
}
const A = make('a'), B = make('b');
const a = new A(), b = new B();
return [
A !== B, A.prototype !== B.prototype,
A.prototype.describe !== B.prototype.describe,
a.describe(), b.describe(),
a.secret, b.secret,
A.origin, B.origin, A.stamped, B.stamped,
a instanceof A, a instanceof B
].join(',');
})()
""").AsString();

Assert.Equal("true,true,true,a:a,b:b,a!,b!,a,b,A,B,true,false", result);
}

[Fact]
public void SuperAndHomeObjectStayPerEvaluation()
{
var engine = new Engine();
var result = engine.Evaluate("""
(function () {
function make(base) {
return class extends base { hello() { return 'sub-' + super.hello(); } };
}
class B1 { hello() { return 'one'; } }
class B2 { hello() { return 'two'; } }
const S1 = make(B1), S2 = make(B2);
return new S1().hello() + '|' + new S2().hello();
})()
""").AsString();

Assert.Equal("sub-one|sub-two", result);
}

[Fact]
public void RepeatedPreparedScriptEvaluationStaysCorrect()
{
var engine = new Engine();
// top-level `class` is a lexical declaration and correctly throws on re-declaration, so the
// re-evaluation pattern wraps it — the class AST (and its cached member definitions) still
// repeats across evaluations
var prepared = Engine.PrepareScript("""
(function () {
class Point { #x; constructor(x) { this.#x = x; } get x() { return this.#x; } double() { return this.x * 2; } }
return new Point(21).double();
})();
""");

Assert.Equal(42, engine.Evaluate(prepared).AsNumber());
Assert.Equal(42, engine.Evaluate(prepared).AsNumber());
Assert.Equal(42, engine.Evaluate(prepared).AsNumber());
}
}
9 changes: 8 additions & 1 deletion Jint/AstExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,14 @@ internal static Record DefineMethod<T>(this T m, ObjectInstance obj, ObjectInsta
Throw.SyntaxError(engine.Realm);
}

var definition = new JintFunctionDefinition(function, sourceTextNode);
// re-evaluating the same class (factory functions, re-run prepared scripts) reuses the
// method's interpreter definition and its warm body handler tree; the closure, home object
// and environments stay per-evaluation
if (!engine.TryGetFunctionDefinition((Node) function, out var definition))
{
engine.CacheFunctionDefinition((Node) function, definition = new JintFunctionDefinition(function, sourceTextNode));
}

var closure = intrinsics.Function.OrdinaryFunctionCreate(prototype, definition, definition.ThisMode, env, privateEnv);
closure.MakeMethod(obj);

Expand Down
39 changes: 24 additions & 15 deletions Jint/Engine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,11 @@ public sealed partial class Engine : IDisposable
internal EvaluationContext? _activeEvaluationContext;
internal ErrorDispatchInfo? _error;

// Per-engine cache of function declarations' interpreter definitions, keyed on the AST node.
// The definition owns the lazily-built body handler tree — and through it every per-node inline
// cache — so reusing it across evaluations and calls keeps a prepared script's functions warm
// instead of rebuilding the subtree each time declaration instantiation runs.
// Per-engine cache of interpreter function definitions — hoisted declarations, class methods,
// class field initializers and static blocks — keyed on the stable AST node. The definition
// owns the lazily-built body handler tree — and through it every per-node inline cache — so
// reusing it across evaluations and calls keeps a prepared script's functions warm instead of
// rebuilding the subtree each time declaration instantiation or class evaluation runs.
//
// Lifetime notes. Engine-owned rather than shared via the AST's UserData State because handler
// trees accumulate engine-affine cache entries, and sharing them across engines retains dead
Expand All @@ -68,30 +69,38 @@ public sealed partial class Engine : IDisposable
// failure the weak table was meant to avoid. The strong form instead retains definitions for
// scripts this engine evaluated until the engine dies, mirroring Realm._templateMap's existing
// behavior.
private readonly Dictionary<FunctionDeclaration, JintFunctionDefinition> _hoistedFunctionDefinitions = new();
private readonly Dictionary<Node, JintFunctionDefinition> _functionDefinitions = new();

// Scripts this engine has run global declaration instantiation for, so the definition cache
// above only engages on RE-evaluation (see GlobalDeclarationInstantiation).
private readonly HashSet<Script> _evaluatedScripts = new();

internal JintFunctionDefinition GetOrCreateFunctionDefinition(FunctionDeclaration declaration)
{
if (!_hoistedFunctionDefinitions.TryGetValue(declaration, out var definition))
if (!TryGetFunctionDefinition(declaration, out var definition))
{
// backstop for hosts streaming endless distinct sources (unique eval/script texts)
// through one engine: reset rather than grow without bound - steady-state reuse of a
// sane number of scripts never reaches this
if (_hoistedFunctionDefinitions.Count >= 2048)
{
_hoistedFunctionDefinitions.Clear();
}

_hoistedFunctionDefinitions[declaration] = definition = new JintFunctionDefinition(declaration);
CacheFunctionDefinition(declaration, definition = new JintFunctionDefinition(declaration));
}

return definition;
}

internal bool TryGetFunctionDefinition(Node key, [NotNullWhen(true)] out JintFunctionDefinition? definition)
=> _functionDefinitions.TryGetValue(key, out definition);

internal void CacheFunctionDefinition(Node key, JintFunctionDefinition definition)
{
// backstop for hosts streaming endless distinct sources (unique eval/script texts)
// through one engine: reset rather than grow without bound - steady-state reuse of a
// sane number of scripts never reaches this
if (_functionDefinitions.Count >= 2048)
{
_functionDefinitions.Clear();
}

_functionDefinitions[key] = definition;
}

private readonly EventLoop _eventLoop = new();
internal EventLoop EventLoop => _eventLoop;

Expand Down
29 changes: 24 additions & 5 deletions Jint/Native/Function/ClassDefinition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -613,7 +613,11 @@ private static AutoAccessorResult AutoAccessorDefinitionEvaluation(
var env = engine.ExecutionContext.LexicalEnvironment;
var privEnv = engine.ExecutionContext.PrivateEnvironment;

var definition = new JintFunctionDefinition(new ClassFieldFunction(accessorProperty.Value));
if (!engine.TryGetFunctionDefinition(accessorProperty.Value, out var definition))
{
engine.CacheFunctionDefinition(accessorProperty.Value, definition = new JintFunctionDefinition(new ClassFieldFunction(accessorProperty.Value)));
}

initializer = intrinsics.Function.OrdinaryFunctionCreate(intrinsics.Function.PrototypeObject, definition, FunctionThisMode.Global, env, privEnv);
initializer.MakeMethod(target);
initializer._classFieldInitializerName = name;
Expand Down Expand Up @@ -658,7 +662,11 @@ private static AutoAccessorResult AutoAccessorDefinitionEvaluation(
var env = engine.ExecutionContext.LexicalEnvironment;
var privEnv = engine.ExecutionContext.PrivateEnvironment;

var definition = new JintFunctionDefinition(new ClassFieldFunction(accessorProperty.Value));
if (!engine.TryGetFunctionDefinition(accessorProperty.Value, out var definition))
{
engine.CacheFunctionDefinition(accessorProperty.Value, definition = new JintFunctionDefinition(new ClassFieldFunction(accessorProperty.Value)));
}

initializer = intrinsics.Function.OrdinaryFunctionCreate(intrinsics.Function.PrototypeObject, definition, FunctionThisMode.Global, env, privEnv);
initializer.MakeMethod(target);
initializer._classFieldInitializerName = name;
Expand Down Expand Up @@ -689,7 +697,11 @@ private static ClassFieldDefinition ClassFieldDefinitionEvaluation(Engine engine
var env = engine.ExecutionContext.LexicalEnvironment;
var privateEnv = engine.ExecutionContext.PrivateEnvironment;

var definition = new JintFunctionDefinition(new ClassFieldFunction(fieldDefinition.Value));
if (!engine.TryGetFunctionDefinition(fieldDefinition.Value, out var definition))
{
engine.CacheFunctionDefinition(fieldDefinition.Value, definition = new JintFunctionDefinition(new ClassFieldFunction(fieldDefinition.Value)));
}

initializer = intrinsics.Function.OrdinaryFunctionCreate(intrinsics.Function.PrototypeObject, definition, FunctionThisMode.Global, env, privateEnv);

initializer.MakeMethod(homeObject);
Expand Down Expand Up @@ -729,7 +741,10 @@ private static ClassStaticBlockDefinition ClassStaticBlockDefinitionEvaluation(E
{
var intrinsics = engine.Realm.Intrinsics;

var definition = new JintFunctionDefinition(new ClassStaticBlockFunction(o));
if (!engine.TryGetFunctionDefinition(o, out var definition))
{
engine.CacheFunctionDefinition(o, definition = new JintFunctionDefinition(new ClassStaticBlockFunction(o)));
}

var lex = engine.ExecutionContext.LexicalEnvironment;
var privateEnv = engine.ExecutionContext.PrivateEnvironment;
Expand Down Expand Up @@ -799,7 +814,11 @@ public ClassStaticBlockFunction(StaticBlock staticBlock) : base(NodeType.StaticB
return DefineMethodProperty(obj, methodDef.Key, methodDef.Closure, enumerable);
}

var definition = new JintFunctionDefinition(function, sourceTextNode: method);
if (!engine.TryGetFunctionDefinition((Node) function, out var definition))
{
engine.CacheFunctionDefinition((Node) function, definition = new JintFunctionDefinition(function, sourceTextNode: method));
}

var intrinsics = engine.Realm.Intrinsics;

var value = method.TryGetKey(engine);
Expand Down
Loading