Skip to content

Commit c270201

Browse files
authored
fix: add async yield mechanism to prevent infinite recursion from freezing UI (#6086)
1 parent 120e37e commit c270201

File tree

1 file changed

+55
-1
lines changed

1 file changed

+55
-1
lines changed

js/logo.js

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,16 @@ class Logo {
324324
// Midi Data
325325
this._midiData = {};
326326

327+
// Async yield mechanism: prevent infinite recursion from freezing the UI.
328+
// _syncCounter tracks consecutive synchronous block executions.
329+
// When it exceeds _YIELD_AFTER_SYNC_RUNS, we yield to the event loop
330+
// via setTimeout(0) so the browser can process paint/input events.
331+
// _totalIterations is a per-run safety limit to catch true infinite loops.
332+
this._syncCounter = 0;
333+
this._YIELD_AFTER_SYNC_RUNS = 1000;
334+
this._totalIterations = 0;
335+
this._MAX_ITERATIONS = 1000000;
336+
327337
// When running in step-by-step mode, the next command to run
328338
// is queued here.
329339
this.stepQueue = {};
@@ -1128,6 +1138,10 @@ class Logo {
11281138

11291139
this.stopTurtle = false;
11301140

1141+
// Reset async yield counters for this run.
1142+
this._syncCounter = 0;
1143+
this._totalIterations = 0;
1144+
11311145
this.activity.blocks.unhighlightAll();
11321146
this.activity.blocks.bringToTop(); // Draw under the blocks.
11331147

@@ -1399,6 +1413,10 @@ class Logo {
13991413

14001414
this.receivedArg = receivedArg;
14011415

1416+
// Reset async yield counters – execution will go through
1417+
// setTimeout below, giving the event loop a chance to breathe.
1418+
logo._syncCounter = 0;
1419+
14021420
const tur = logo.activity.turtles.ithTurtle(turtle);
14031421

14041422
const delay = logo.turtleDelay + tur.waitTime;
@@ -1437,6 +1455,22 @@ class Logo {
14371455

14381456
this.receivedArg = receivedArg;
14391457

1458+
// Safety check: stop execution if we have exceeded the maximum
1459+
// iteration limit, which indicates an infinite loop (e.g., an
1460+
// Action block that calls itself with no delay).
1461+
logo._totalIterations++;
1462+
if (logo._totalIterations > logo._MAX_ITERATIONS) {
1463+
logo.activity.errorMsg(
1464+
_("Infinite loop detected. Execution stopped to prevent browser freeze."),
1465+
blk
1466+
);
1467+
logo.stopTurtle = true;
1468+
logo._alreadyRunning = false;
1469+
logo._syncCounter = 0;
1470+
logo._totalIterations = 0;
1471+
return;
1472+
}
1473+
14401474
// Sometimes we don't want to unwind the entire queue.
14411475
if (queueStart === undefined) queueStart = 0;
14421476

@@ -1779,7 +1813,27 @@ class Logo {
17791813
}
17801814

17811815
if (isflow) {
1782-
logo.runFromBlockNow(logo, turtle, nextBlock, isflow, passArg, queueStart);
1816+
// Async yield: periodically yield to the event loop so the
1817+
// browser can process paint/input events and the UI does not
1818+
// freeze during long-running or infinitely-recursive programs.
1819+
logo._syncCounter++;
1820+
if (logo._syncCounter >= logo._YIELD_AFTER_SYNC_RUNS) {
1821+
logo._syncCounter = 0;
1822+
setTimeout(() => {
1823+
if (!logo.stopTurtle) {
1824+
logo.runFromBlockNow(
1825+
logo,
1826+
turtle,
1827+
nextBlock,
1828+
isflow,
1829+
passArg,
1830+
queueStart
1831+
);
1832+
}
1833+
}, 0);
1834+
} else {
1835+
logo.runFromBlockNow(logo, turtle, nextBlock, isflow, passArg, queueStart);
1836+
}
17831837
} else {
17841838
logo.runFromBlock(logo, turtle, nextBlock, isflow, passArg);
17851839
}

0 commit comments

Comments
 (0)