Measure the execution timeout against an inline deadline - #2747
Merged
Conversation
TimeConstraint allocated a CancellationTokenSource with a timer on every Reset - every top-level execution - and only reported a timeout once that timer''s callback had run on the thread pool, so detection was bounded by callback scheduling rather than by the timeout. Reset now captures a Stopwatch deadline and Check compares timestamps, the same shape the regular expression timeout uses. The comment justifying the token source predates amortized constraint checking: with a check interval of 64 statements the timestamp is not read per statement. A configured timeout cost 288 bytes per top-level execution; it now allocates exactly as much as an engine with no timeout at all. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This was referenced Jul 28, 2026
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
Measure the execution timeout against an inline deadline instead of a
CancellationTokenSourcetimer.Details
TimeConstraintallocated aCancellationTokenSourcewith a timer on everyReset— that is, on every top-level execution — andCheckreported a timeout only once that timer's callback had run on the thread pool. Detection was therefore bounded by callback scheduling rather than by the timeout itself.It now captures a
Stopwatchdeadline inResetand compares timestamps inCheck. This is the same shape the regular expression timeout already uses.The original comment justified the token source as avoiding a time check per statement. That trade-off predates amortized constraint checking: with
EvaluationContext.AmortizedConstraintCheckInterval = 64, the per-statement cost is a countdown decrement and the timestamp is read once per 64 statements, not once per statement.Numbers
Measured against
mainwith a harness that runs both configurations in the same process (Jintreferenced from a pristinemainworktree vs this branch):So a configured timeout costs 288 bytes per top-level execution on
main— the token source plus its timer registration — and costs nothing here: the figure is now identical to an engine with no timeout at all. That number is deterministic and reproduced byte-for-byte across runs.On throughput I have no claim to make. I measured a statement-heavy script (a 300k-iteration loop) with and without a timeout, five passes per arm, and the timeout tax came out at +1.46% on
main(range −2.19% to +10.33%) and −3.46% here (range −25.66% to +4.65%). A −25% "tax" is obviously noise, so the harness cannot resolve the difference on my machine; I am not going to dress that up as a win. The mechanism says the change trades one allocation plus a timer registration per execution for oneStopwatch.GetTimestamp()per 64 statements, and the measurement is consistent with that being below the noise floor.Honest note on motivation
I looked at this because
ModuleTests.ShouldSupportConstraintsfailed once on a Windows CI run (asserting aTimeoutExceptionthat was never thrown) and a timer-scheduling delay was my hypothesis. I could not reproduce that. A repro against unmodifiedmainthat forcesThreadPool.SetMinThreads(1, …)and parks 256 blocked work items still threw theTimeoutExceptionevery time, so I make no claim that this fixes that failure. The change stands on the allocation figure above and on detection no longer depending on when a callback happens to run.Linked issue
None.
Test plan
Jint.Testsdotnet test --configuration Releaselocally (Jint.Tests3880 passed,Jint.Tests.PublicInterface114 passed,Jint.Tests.CommonScripts28 passed)Jint.Tests.Test262and confirmed no regressions — n/a, no spec behaviour touchedJint.Tests/Runtime/Interop— n/aJint.Benchmark— see above (a dedicated harness rather than a suite benchmark, since no existing benchmark configures a timeout)Three tests pin the contract that the rewrite has to keep: a
Checkbefore any execution does not fail (the previous null-token-source state, now a not-started sentinel); each top-level execution is re-armed with the full interval; and once the interval has elapsed the nextCheckfails. The existing timeout tests — including the tight-loop andInvokeones — continue to pass unchanged.Breaking change?
No.
TimeConstraintis internal and the observable behaviour — when aTimeoutExceptionis raised — is unchanged apart from being detected promptly rather than when a timer callback runs.🤖 Generated with Claude Code