fix(tui): throttle render scheduling under streaming load

This commit is contained in:
Mario Zechner 2026-04-06 02:03:50 +02:00
parent 78d4625a48
commit 6f5f37f85a
2 changed files with 45 additions and 2 deletions

View file

@ -2,6 +2,10 @@
## [Unreleased]
### Fixed
- Fixed render scheduling under heavy streaming output by coalescing `requestRender()` calls to a 16ms frame budget while preserving immediate `requestRender(true)` behavior.
## [0.65.1] - 2026-04-05
## [0.65.0] - 2026-04-03

View file

@ -5,6 +5,7 @@
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { performance } from "node:perf_hooks";
import { isKeyRelease, matchesKey } from "./keys.js";
import type { Terminal } from "./terminal.js";
import { getCapabilities, isImageLine, setCellDimensions } from "./terminal-image.js";
@ -221,6 +222,9 @@ export class TUI extends Container {
/** Global callback for debug key (Shift+Ctrl+D). Called before input is forwarded to focused component. */
public onDebug?: () => void;
private renderRequested = false;
private renderTimer: NodeJS.Timeout | undefined;
private lastRenderAt = 0;
private static readonly MIN_RENDER_INTERVAL_MS = 16;
private cursorRow = 0; // Logical cursor row (end of rendered content)
private hardwareCursorRow = 0; // Actual terminal cursor row (may differ due to IME positioning)
private showHardwareCursor = process.env.PI_HARDWARE_CURSOR === "1";
@ -442,6 +446,10 @@ export class TUI extends Container {
stop(): void {
this.stopped = true;
if (this.renderTimer) {
clearTimeout(this.renderTimer);
this.renderTimer = undefined;
}
// Move cursor to the end of the content to prevent overwriting/artifacts on exit
if (this.previousLines.length > 0) {
const targetRow = this.previousLines.length; // Line after the last content
@ -467,13 +475,44 @@ export class TUI extends Container {
this.hardwareCursorRow = 0;
this.maxLinesRendered = 0;
this.previousViewportTop = 0;
if (this.renderTimer) {
clearTimeout(this.renderTimer);
this.renderTimer = undefined;
}
this.renderRequested = true;
process.nextTick(() => {
if (this.stopped || !this.renderRequested) {
return;
}
this.renderRequested = false;
this.lastRenderAt = performance.now();
this.doRender();
});
return;
}
if (this.renderRequested) return;
this.renderRequested = true;
process.nextTick(() => {
process.nextTick(() => this.scheduleRender());
}
private scheduleRender(): void {
if (this.stopped || this.renderTimer || !this.renderRequested) {
return;
}
const elapsed = performance.now() - this.lastRenderAt;
const delay = Math.max(0, TUI.MIN_RENDER_INTERVAL_MS - elapsed);
this.renderTimer = setTimeout(() => {
this.renderTimer = undefined;
if (this.stopped || !this.renderRequested) {
return;
}
this.renderRequested = false;
this.lastRenderAt = performance.now();
this.doRender();
});
if (this.renderRequested) {
this.scheduleRender();
}
}, delay);
}
private handleInput(data: string): void {