Skip to content

Measure the execution timeout against an inline deadline - #2747

Merged
lahma merged 1 commit into
sebastienros:mainfrom
lahma:time-constraint-deadline
Jul 23, 2026
Merged

Measure the execution timeout against an inline deadline#2747
lahma merged 1 commit into
sebastienros:mainfrom
lahma:time-constraint-deadline

Conversation

@lahma

@lahma lahma commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

Summary

Measure the execution timeout against an inline deadline instead of a CancellationTokenSource timer.

Details

TimeConstraint allocated a CancellationTokenSource with a timer on every Reset — that is, on every top-level execution — and Check reported 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 Stopwatch deadline in Reset and compares timestamps in Check. 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 main with a harness that runs both configurations in the same process (Jint referenced from a pristine main worktree vs this branch):

Measurement main this branch
Per-execution allocation, timeout configured 1708.3 B 1420.3 B
Per-execution allocation, no timeout configured 1420.3 B 1420.3 B

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 one Stopwatch.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.ShouldSupportConstraints failed once on a Windows CI run (asserting a TimeoutException that was never thrown) and a timer-scheduling delay was my hypothesis. I could not reproduce that. A repro against unmodified main that forces ThreadPool.SetMinThreads(1, …) and parks 256 blocked work items still threw the TimeoutException every 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

  • Added or updated unit tests in Jint.Tests
  • Ran dotnet test --configuration Release locally (Jint.Tests 3880 passed, Jint.Tests.PublicInterface 114 passed, Jint.Tests.CommonScripts 28 passed)
  • For ECMAScript spec changes: ran Jint.Tests.Test262 and confirmed no regressions — n/a, no spec behaviour touched
  • For interop changes: covered in Jint.Tests/Runtime/Interop — n/a
  • For perf changes: included before/after numbers from Jint.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 Check before 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 next Check fails. The existing timeout tests — including the tight-loop and Invoke ones — continue to pass unchanged.

Breaking change?

No. TimeConstraint is internal and the observable behaviour — when a TimeoutException is raised — is unchanged apart from being detected promptly rather than when a timer callback runs.

🤖 Generated with Claude Code

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant