fix(run): keep console output flowing in background tabs (#400)

Run console output is flushed on requestAnimationFrame, which stops while the page is hidden, so output from a run in a hidden window piled up and replayed when it was shown again. Pending flushes now move to a 100 ms timeout while hidden and back to rAF when visible.

Co-authored-by: a-rookie-of-C-language <186315048+a-rookie-of-C-language@users.noreply.github.com>
This commit is contained in:
arookieofc
2026-10-30 19:16:42 +08:00
committed by GitHub
co-authored by a-rookie-of-C-language
parent 12f676e0c0
commit ed6092a4e9
2 changed files with 46 additions and 9 deletions
+7
View File
@@ -52,6 +52,7 @@ import {
CURRENT_FILE,
currentFileSelected,
prepareLaunch,
rescheduleConsoleFlushes,
setBeforeLaunchRunner,
loadConfigs,
failedTests,
@@ -534,6 +535,12 @@ export function activate(): () => void {
}),
);
// Hidden tabs pause rAF: move pending console flushes to timeouts (and back on
// return) so output keeps draining while a run continues in the background.
const onVisibility = () => rescheduleConsoleFlushes();
document.addEventListener("visibilitychange", onVisibility);
disposers.push(() => document.removeEventListener("visibilitychange", onVisibility));
return () => {
clearTimeout(reloadTimer);
disposers.reverse().forEach((d) => d());
+39 -9
View File
@@ -71,7 +71,8 @@ interface SessionRuntime {
tests: TestResults | null;
parser: TestOutputParser | null;
queue: OutputChunk[];
flushHandle: number | null;
/** rAF id while the page is visible, timeout id while hidden (rAF pauses in background tabs). */
flushHandle: { raf: number } | { timeout: number } | null;
/** Bumped per start so output from a previous run in the same tab is ignored. */
generation: number;
/** Called with every complete output line (e.g. waiting for a JDWP agent). */
@@ -340,10 +341,7 @@ function getSession(id: string): RunSession | undefined {
function flush(sessionId: string) {
const rt = runtimes.get(sessionId);
if (!rt) return;
if (rt.flushHandle !== null) {
cancelAnimationFrame(rt.flushHandle);
rt.flushHandle = null;
}
cancelFlush(rt);
if (rt.queue.length === 0) return;
const chunks = rt.queue.splice(0);
for (const c of chunks) rt.console.append(c);
@@ -358,7 +356,39 @@ function enqueue(sessionId: string, chunk: OutputChunk) {
return;
}
rt.queue.push(chunk);
if (rt.flushHandle === null) rt.flushHandle = requestAnimationFrame(() => flush(sessionId));
if (rt.flushHandle === null) {
// rAF pauses in background tabs; a timeout keeps consuming output so it doesn't
// pile up until the console is shown again (issue #319: the console "kept
// scrolling" the previous run's backlog after switching back to it).
rt.flushHandle = document.hidden
? { timeout: window.setTimeout(() => flush(sessionId), 100) }
: { raf: requestAnimationFrame(() => flush(sessionId)) };
}
}
function cancelFlush(rt: SessionRuntime) {
if (rt.flushHandle === null) return;
if ("raf" in rt.flushHandle) cancelAnimationFrame(rt.flushHandle.raf);
else clearTimeout(rt.flushHandle.timeout);
rt.flushHandle = null;
}
/**
* `visibilitychange`: rAF stops firing while the page is hidden, so any pending
* rAF flush is moved to a timeout (and back on return) — otherwise output that
* arrives while a background tab is hidden would only land when it is shown.
*/
export function rescheduleConsoleFlushes() {
for (const [id, rt] of runtimes) {
if (rt.flushHandle === null) continue;
const hidden = document.hidden;
const isTimeout = "timeout" in rt.flushHandle;
if (hidden === isTimeout) continue;
cancelFlush(rt);
rt.flushHandle = hidden
? { timeout: window.setTimeout(() => flush(id), 100) }
: { raf: requestAnimationFrame(() => flush(id)) };
}
}
/** Settings › Console › Override console cycle buffer size, in characters (null = line cap only). */
@@ -504,7 +534,7 @@ export async function startRun(
const prev = runtimes.get(session.id);
const rt = newRuntime(config);
rt.generation = (prev?.generation ?? 0) + 1;
if (prev?.flushHandle != null) cancelAnimationFrame(prev.flushHandle);
if (prev) cancelFlush(prev);
runtimes.set(session.id, rt);
session = {
...session,
@@ -964,7 +994,7 @@ export function closeSession(sessionId: string) {
if (!session) return;
if (session.status === "running") void stopSession(sessionId);
const rt = runtimes.get(sessionId);
if (rt?.flushHandle != null) cancelAnimationFrame(rt.flushHandle);
if (rt) cancelFlush(rt);
runtimes.delete(sessionId);
useRun.setState((s) => {
const index = s.sessions.findIndex((x) => x.id === sessionId);
@@ -984,7 +1014,7 @@ export function runningSessions(): RunSession[] {
/** Stops everything and forgets sessions (project closed). */
export function resetRuns() {
for (const s of runningSessions()) void stopSession(s.id);
for (const rt of runtimes.values()) if (rt.flushHandle != null) cancelAnimationFrame(rt.flushHandle);
for (const rt of runtimes.values()) cancelFlush(rt);
runtimes.clear();
pendingExits.clear();
useRun.setState({