diff --git a/Jint/Native/Function/ScriptFunction.cs b/Jint/Native/Function/ScriptFunction.cs index 3b3f3dda2d..cd62b07a8f 100644 --- a/Jint/Native/Function/ScriptFunction.cs +++ b/Jint/Native/Function/ScriptFunction.cs @@ -75,9 +75,9 @@ protected internal override JsValue Call(JsValue thisObject, JsCallArguments arg Throw.TypeError(calleeContext.Realm, $"Class constructor {_functionDefinition.Name} cannot be invoked without 'new'"); } - // Check if slot array can be cached after this call + // Capture funcEnv for end-of-call pool return when bindings can't escape state = _functionDefinition.Initialize(); - if (state is { UseFixedSlots: true, EnvironmentMayEscape: false }) + if (state is { EnvironmentMayEscape: false }) { funcEnv = (FunctionEnvironment) calleeContext.LexicalEnvironment; } @@ -120,12 +120,19 @@ protected internal override JsValue Call(JsValue thisObject, JsCallArguments arg } finally { - // Cache the slot array for reuse by next call to the same function (thread-safe) - if (funcEnv?._slots is { } slots) + if (funcEnv is not null) { - System.Array.Clear(slots, 0, slots.Length); - Interlocked.Exchange(ref state!._cachedSlots, slots); - funcEnv._slots = null; + // Cache the slot array for reuse by next call to the same function (thread-safe) + if (funcEnv._slots is { } slots) + { + System.Array.Clear(slots, 0, slots.Length); + Interlocked.Exchange(ref state!._cachedSlots, slots); + funcEnv._slots = null; + } + + // Return the env itself to the per-State pool so the next call to a function + // sharing this State (typically the same Function instance) avoids the allocation. + Interlocked.Exchange(ref state!._cachedEnv, funcEnv); } _engine.LeaveExecutionContext(); } diff --git a/Jint/Runtime/Environments/DeclarativeEnvironment.cs b/Jint/Runtime/Environments/DeclarativeEnvironment.cs index fee93dd062..a613a35432 100644 --- a/Jint/Runtime/Environments/DeclarativeEnvironment.cs +++ b/Jint/Runtime/Environments/DeclarativeEnvironment.cs @@ -353,6 +353,14 @@ public void Clear() _dictionary = null; } + /// + /// Reset for env-pool reuse: drop any using/await-using disposable resources tracked from the previous call. + /// + internal void ClearDisposeCapability() + { + _disposeCapability = null; + } + internal void TransferTo(List names, DeclarativeEnvironment env) { var source = _dictionary!; diff --git a/Jint/Runtime/Environments/FunctionEnvironment.cs b/Jint/Runtime/Environments/FunctionEnvironment.cs index 6a9ff5e03c..2ce173af2f 100644 --- a/Jint/Runtime/Environments/FunctionEnvironment.cs +++ b/Jint/Runtime/Environments/FunctionEnvironment.cs @@ -23,7 +23,8 @@ private enum ThisBindingStatus private JsValue? _thisValue; private ThisBindingStatus _thisBindingStatus; - internal readonly Function _functionObject; + // Mutable so a pooled env can be re-bound to a different Function instance that shares the same State (e.g. function literals re-evaluated in loops). + internal Function _functionObject; public FunctionEnvironment( Engine engine, @@ -42,6 +43,23 @@ public FunctionEnvironment( } } + /// + /// Re-initialize a pooled FunctionEnvironment for a new call. Leaves slot/dictionary handling + /// to the caller — they may either clear in place or replace with fresh storage. + /// + internal void Reset(Function functionObject, JsValue newTarget, Environment? outerEnv) + { + _functionObject = functionObject; + NewTarget = newTarget; + _outerEnv = outerEnv; + _thisValue = null; + _thisBindingStatus = functionObject._functionDefinition?.Function.Type is NodeType.ArrowFunctionExpression + ? ThisBindingStatus.Lexical + : ThisBindingStatus.Uninitialized; + _dictionary?.Clear(); + ClearDisposeCapability(); + } + internal override bool HasThisBinding() => _thisBindingStatus != ThisBindingStatus.Lexical; diff --git a/Jint/Runtime/Environments/JintEnvironment.cs b/Jint/Runtime/Environments/JintEnvironment.cs index 13fb4002cf..0585fa8685 100644 --- a/Jint/Runtime/Environments/JintEnvironment.cs +++ b/Jint/Runtime/Environments/JintEnvironment.cs @@ -77,12 +77,27 @@ internal static DeclarativeEnvironment NewDeclarativeEnvironment(Engine engine, /// internal static FunctionEnvironment NewFunctionEnvironment(Engine engine, Function f, JsValue newTarget) { - var env = new FunctionEnvironment(engine, f, newTarget) + var state = f._functionDefinition?.Initialize(); + FunctionEnvironment env; + + // Reuse a pooled FunctionEnvironment when the function's bindings cannot escape the call + // (no closure capture, not a generator/async). Re-bind to the new function/target/outer env + // and reset transient state. Slot storage is handled below. + if (state is { EnvironmentMayEscape: false } + && Interlocked.Exchange(ref state._cachedEnv, null) is { } cachedEnv + && ReferenceEquals(cachedEnv._engine, engine)) { - _outerEnv = f._environment - }; + cachedEnv.Reset(f, newTarget, f._environment); + env = cachedEnv; + } + else + { + env = new FunctionEnvironment(engine, f, newTarget) + { + _outerEnv = f._environment, + }; + } - var state = f._functionDefinition?.Initialize(); if (state is { UseFixedSlots: true }) { env._slotNames = state.SlotNames; diff --git a/Jint/Runtime/Interpreter/JintFunctionDefinition.cs b/Jint/Runtime/Interpreter/JintFunctionDefinition.cs index 729766e51f..14d0d99839 100644 --- a/Jint/Runtime/Interpreter/JintFunctionDefinition.cs +++ b/Jint/Runtime/Interpreter/JintFunctionDefinition.cs @@ -309,6 +309,7 @@ internal sealed class State public bool CanUseFastFDI; public bool EnvironmentMayEscape; public Binding[]? _cachedSlots; + public Environments.FunctionEnvironment? _cachedEnv; public SourceText SourceText; @@ -558,18 +559,27 @@ internal static State BuildState(IFunction function, string? fullSourceText = nu state.VarSlotCount = varsToInitialize.Count; state.UseFixedSlots = true; state.CanUseFastFDI = lexicalBindingCount == 0; - - if (function.Generator || function.Async) - { - state.EnvironmentMayEscape = true; - } - else - { - state.EnvironmentMayEscape = EnvironmentEscapeAstVisitor.MayEscapeWithReferences(function, slotNames); - } } } + // Compute EnvironmentMayEscape unconditionally so consumers (e.g. FunctionEnvironment pooling) + // can rely on it without first checking UseFixedSlots. Generators / async functions / direct eval + // always escape; otherwise inspect the body. When the function qualified for fixed slots, prefer + // the slot-aware analysis (only escapes if a closure actually references a slot variable); + // otherwise fall back to the conservative "any inner closure means escape" check. + if (function.Generator || function.Async || state.NeedsEvalContext) + { + state.EnvironmentMayEscape = true; + } + else if (state.UseFixedSlots) + { + state.EnvironmentMayEscape = EnvironmentEscapeAstVisitor.MayEscapeWithReferences(function, state.SlotNames!); + } + else + { + state.EnvironmentMayEscape = EnvironmentEscapeAstVisitor.MayEscape(function); + } + state.SourceText = new SourceText(fullSourceText); return state;