Playground: exit loops which block the page instead of freezing the tab#3618
Open
Dinesh-3018 wants to merge 1 commit into
Open
Playground: exit loops which block the page instead of freezing the tab#3618Dinesh-3018 wants to merge 1 commit into
Dinesh-3018 wants to merge 1 commit into
Conversation
Running code containing an infinite loop (e.g. `while (true) {}`) freezes
the browser tab, because the Run button evals the emitted JS directly on
the main thread with no way to stop it (microsoft#2541).
This adds loop protection in the same style as JSBin, CodePen and the
Babel REPL: before eval, a guard is inserted at the top of every
for/while/do loop body using the TypeScript AST. If user code blocks the
page for more than a second, every guarded loop exits and the Logs pane
shows which line looked infinite. Await-loops, timers and ordinary
sub-second loops are never affected, no newlines are added so error line
numbers still match, and a "Disable Loop Protection" setting turns it
off for intentionally long-running code.
Author
|
@microsoft-github-policy-service agree |
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.
The Problem
Open https://www.typescriptlang.org/play, enter:
then press Run.
The playground tab freezes indefinitely. The only way to recover is to force-close the tab, losing the current code and any unsaved state. The same issue occurs with
for (;;)loops, accidentali--instead ofi++, and other unintentionally non-terminating loops that learners commonly write.(I'm submitting this as a PR rather than an issue, following the issue template guidance that non-critical reports should be submitted as pull requests. The underlying context is described below.)
The root cause is that the Run button evaluates the emitted JavaScript directly on the browser's main thread (
packages/playground/src/sidebar/runtime.ts). While the existingtry/catchcan handle thrown exceptions, it cannot recover from code that never terminates. This limitation was previously discussed in #2541 ("Add ability to stop a running script"), which concluded that interrupting execution wasn't feasible without ShadowRealms. Related discussion: #3155.The Fix
This PR adopts the same strategy used by JSBin (
loop-protect), CodePen, freeCodeCamp, and the Babel REPL (babel/website#352): instrument loops before evaluation instead of moving execution to a Worker or iframe, which would break DOM access and live console capture.Added
insertLoopProtection(packages/playground/src/sidebar/loopProtection.ts), which parses the emitted JavaScript using the existing TypeScript AST and injects:at the beginning of every
for,while, anddoloop body.Unbraced loop bodies are wrapped in a block when necessary. Instrumentation is inserted only inside loop bodies, never before the loop itself, ensuring constructs such as
if (x) while (y) z(),elsebranches, and labeled loops remain syntactically valid. No additional newlines are introduced, so runtime error locations continue to match the JavaScript displayed to the user.The runtime guard tracks synchronous execution bursts. When execution begins, it schedules a zero-delay timeout. Every guard check before the event loop regains control belongs to the same burst. If a burst blocks the main thread for more than 1000 ms, all guarded loops terminate. The Logs pane reports the loop that appears to be infinite, while subsequent loops in the same exhausted burst display an "exited early" message instead of also being reported as infinite.
This approach correctly handles:
while leaving unaffected:
await-based loopssetIntervalcallbacksInstrumentation degrades safely. Code that cannot be parsed, along with edge cases such as declaration-bodied loops (
while (true) let x = 1;) that currently produce a runtimeSyntaxError, is left completely unchanged so behavior matches today's playground exactly.Added a Disable Loop Protection setting for intentionally long-running loops. The setting takes effect on the next Run, allowing it to be rendered without the restart-required banner. Documentation and localized copy were added to
en/playground.tsand the Playground handbook ("Settings Panel" and "Running Code").Known limitations (shared with existing approaches):
Persistent
setIntervalcallbacks from previous runs (the other half of Playground: Add ability to stop a running script #2541) remain intentionally out of scope.Testing
pnpm --filter=@typescript/playground testAdded 18 tests covering:
for...ofover an infinite generatortry/finallyloop bodiessetTimeoutcallback queued behind an over-budget script (must not terminate early)asyncloop that yields (must not terminate early)SyntaxErrorpreservationThe test suite wraps the guard with a hard deadline that throws if protection fails, preventing Jest/CI from hanging during regressions.
pnpm --filter=@typescript/playground buildcompletes successfully.The full
pnpm bootstrapbuild passes.All new and modified files pass the repository's Prettier configuration.