Repository navigation
Conversation
Execute and Evaluate of a small script paid for work it never used: - ExecuteForCompletion passes its state to a static callback in a struct, so the entry allocates neither a closure nor a delegate. - The debugger handler is only touched when it already exists or debug mode is on, so evaluating a script no longer creates it on an engine that never asked for it. - RunAvailableContinuations skips its host-call claim when the drain provably has nothing to do: no queued job or task lane, no Atomics.waitAsync deadline, no pending rejection notification and, on net8.0+, no web-API engine state. - GlobalDeclarationInstantiation returns early for a script that declares nothing, keeping the Annex B block-function case. TinyScriptEntryBenchmark measures the entry cost, with a function-call control row that none of these changes reach.
sad75
marked this pull request as ready for review
October 22, 2026 11:54
This branch has not been deployed
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.
Summary
Cut the per-call overhead of
Execute/Evaluatefor small scripts.Details
Problem
For a small script on a warm engine, the cost of
Evaluateis dominated by the work around the script, not the script itself.engine.Evaluate("a + b")allocates 312 B per call onmain; 248 B of it is avoidable, and so is part of the time. Four steps run unconditionally:ExecuteForCompletionFunc<T>delegate (120 B) on every call, to pass four locals toScriptEvaluation.ScriptEvaluationDebugger.OnBeforeEvaluate. ReadingDebuggercreates aDebugHandleron every engine, even when nothing uses debugging.RunAvailableContinuationsAtomics.waitAsyncwaiter or rejection.GlobalDeclarationInstantiationLists and aHashSet(128 B) before checking whether the script declares anything.Fix
ExecuteForCompletionpasses its state in areadonly record struct ScriptEvaluationStateto astaticcallback, through the existingExecuteWithConstraints<TState, TResult>overload. No allocation.BeforeEvaluateis raised onlyif (_debugger is not null || _isDebugMode). A host can only subscribe by readingengine.Debugger, which creates the handler, so subscribers are still notified; debug mode still creates it at script entry.RunAvailableContinuationsreturns first ifIsContinuationDrainNoOp()(new, inEngine.Pump.cs): no queued job and no task lane, noAtomics.waitAsyncdeadline, no pending rejection notification and, on net8.0+,_webApiis null. Otherwise the drain runs as before.GlobalDeclarationInstantiationreturns right afterMarkScriptEvaluatedwhen there is no function, var or lexical declaration, and no Annex B block function that could bind (sloppy mode only).Engines with web-API engine state (timers, scheduler, idle callbacks) never take shortcut 3. Module evaluation (
Engine.Modules.cs) still readsDebuggerdirectly; left out of scope.Linked issue
None.
Test plan
Jint.Testsdotnet test --configuration ReleaselocallyJint.Tests.Test262— not a spec changeJint.BenchmarkNew tests, one per behaviour a shortcut must keep:
EngineTests.BeforeEvaluateFiresForAHandlerSubscribedAfterEarlierEvaluationsEngineTests.ADrainAfterAnIdleDrainStillRunsNewlyQueuedJobsPromiseRejectionNotificationTests.ARejectionFromAScriptThatQueuesNoJobIsReportedOnceAnnexBBlockFunctionTests.AScriptWhoseOnlyDeclarationIsABlockFunctionStillGetsTheGlobalAlias(source andPrepared<Script>)AnnexBBlockFunctionTests.AStrictScriptWhoseOnlyDeclarationIsABlockFunctionGetsNoGlobalAlias(directive andOptions.Strict)Jint.Tests(39,239 passed) andJint.Tests.PublicInterface(11,085 passed) are green on net472, net8.0 and net10.0.Benchmark
New
TinyScriptEntryBenchmark: three tiny scripts on a warm engine, plus aFunctionBodycontrol row (Invoke) that none of the changes reach.measure-paired.ps1, 8 alternating rounds, baselinemainat 4fd6bb9:Expression(a + b)Comparison(o.x > a && b !== 0)Declaration(var c = a + b; c)FunctionBody(control)Fixes 1 and 4 account for the bytes (120 B + 128 B;
Declarationdeclares avar, so it only gets fix 1). Fixes 2 and 3 save time only on a warm engine; fix 2 also saves oneDebugHandlerper engine, which this benchmark does not measure.Run on GitHub-hosted
ubuntu-latest(AMD EPYC 7763, 2 vCPU), BenchmarkDotNet 0.15.8, .NET 10.0.13. The benchmark file was copied into the baseline tree and BenchmarkDotNet's build timeout raised in both trees; nothing else in the harness changed. The SunSpider/Dromaeo gate was not run: these changes only touch per-entry setup, which a long script crosses once. Happy to run it if wanted.Breaking change?
No. All changes are internal.