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
90 changes: 90 additions & 0 deletions Jint.Tests/Runtime/SlotLocationCacheTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
namespace Jint.Tests.Runtime;

/// <summary>
/// Pins SlotLocationCache behavior for handler trees shared across function instances
/// (per-engine definition reuse): a node whose cached environment is unreachable from the
/// current chain must re-resolve against it — declining forever would silently disable every
/// slot-backed fast lane from the second re-evaluation of a prepared script onward.
/// </summary>
public class SlotLocationCacheTests
{
[Fact]
public void RepeatedPreparedEvaluationsKeepProducingCorrectResults()
{
var engine = new Engine();
var prepared = Engine.PrepareScript("""
function f() {
var n = 0;
for (var i = 0; i < 1000; i++) { n += i; }
return n;
}
f();
""");

// the cached-definition handler tree serves a fresh function instance (fresh env) per
// evaluation; each must resolve slots against its own chain
for (var e = 0; e < 5; e++)
{
Assert.Equal(499500, engine.Evaluate(prepared).AsNumber());
}
}

[Fact]
public void AlternatingFunctionInstancesSharingOneTreeStayCorrect()
{
var engine = new Engine();
// two instances of the same nested declaration alternate calls: the shared tree's
// slot caches flip between the instances' environments and must stay correct
var result = engine.Evaluate("""
(function () {
function make(base) {
function inner() {
var acc = 0;
for (var i = 0; i < 100; i++) { acc += base; }
return acc;
}
return inner;
}
var a = make(1), b = make(1000);
var sum = 0;
for (var r = 0; r < 4; r++) { sum += a() + b(); }
return sum;
})()
""").AsNumber();

Assert.Equal(4 * (100 + 100_000), result);
}

#if NET
[Fact]
public void SlotLanesSurviveRepeatedPreparedEvaluations()
{
var engine = new Engine();
var prepared = Engine.PrepareScript("""
function f() {
var n = 0;
for (var i = 0; i < 20000; i++) { n += 1; }
return n;
}
f();
""");

// evaluation 1 uses the first-build tree, 2 the freshly cached tree; from 3 on the
// cached tree runs under a different environment than the one that populated its
// slot caches — the lanes must re-resolve and stay unboxed instead of falling to
// the materializing generic path (which allocates a JsNumber per iteration here)
for (var e = 0; e < 3; e++)
{
engine.Evaluate(prepared);
}

var before = GC.GetAllocatedBytesForCurrentThread();
engine.Evaluate(prepared);
var perEvaluation = GC.GetAllocatedBytesForCurrentThread() - before;

Assert.True(
perEvaluation < 150_000,
$"expected slot lanes to stay live across re-evaluations; allocated {perEvaluation} bytes in one evaluation");
}
#endif
}
14 changes: 11 additions & 3 deletions Jint/Runtime/Environments/SlotLocationCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ namespace Jint.Runtime.Environments;
/// Validity reasoning: closure-captured environments cannot be pooled (escape detection
/// prevents it), so a cached env reference is stable for the lifetime of the function
/// instance that captured it, and slot indices are immutable for the lifetime of the env.
/// Handler trees are shared across function INSTANCES though (per-engine definition reuse
/// for re-evaluated scripts, class members and nested declarations), so a node outlives any
/// one instance's environment: an unreachable cached env means the node moved to another
/// instance and must re-resolve against the current chain, not decline.
/// Slot layout is deterministic per AST node, so a node shared across engines via
/// <c>Prepared&lt;Script&gt;</c> resolves to the same index everywhere; the engine-identity
/// gate only avoids wasted walks against another engine's environments.
Expand Down Expand Up @@ -80,9 +84,13 @@ public bool TryResolve(
search = search._outerEnv;
}

slotEnv = null!;
slotIndex = -1;
return false;
// The cached location is not reachable from this chain. Handler trees are shared
// across function instances (per-engine definition reuse for re-evaluated scripts,
// class members, nested declarations), so the same node runs under a fresh
// environment per instance — re-resolve against the current chain exactly as a
// fresh node would (mirroring the cross-engine Prepared<Script> fall-through
// below) instead of declining forever. One bounded walk per instance switch;
// subsequent reads hit the re-populated cache.
}

if (_disabled)
Expand Down
Loading