mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-03 21:03:08 +00:00
fix: throttle terminal output to prevent system lockup under heavy load
- Batch terminal output at ~60fps max to prevent overwhelming WebSocket - Reduce scrollback buffer from 100KB to 50KB per terminal - Clean up flush timeouts on session kill/cleanup - Should fix lockups when running npm run dev with high output 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,7 +11,11 @@ import * as os from "os";
|
|||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
|
|
||||||
// Maximum scrollback buffer size (characters)
|
// Maximum scrollback buffer size (characters)
|
||||||
const MAX_SCROLLBACK_SIZE = 100000; // ~100KB per terminal
|
const MAX_SCROLLBACK_SIZE = 50000; // ~50KB per terminal
|
||||||
|
|
||||||
|
// Throttle output to prevent overwhelming WebSocket under heavy load
|
||||||
|
const OUTPUT_THROTTLE_MS = 16; // ~60fps max update rate
|
||||||
|
const OUTPUT_BATCH_SIZE = 8192; // Max bytes to send per batch
|
||||||
|
|
||||||
export interface TerminalSession {
|
export interface TerminalSession {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -20,6 +24,8 @@ export interface TerminalSession {
|
|||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
shell: string;
|
shell: string;
|
||||||
scrollbackBuffer: string; // Store recent output for replay on reconnect
|
scrollbackBuffer: string; // Store recent output for replay on reconnect
|
||||||
|
outputBuffer: string; // Pending output to be flushed
|
||||||
|
flushTimeout: NodeJS.Timeout | null; // Throttle timer
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TerminalOptions {
|
export interface TerminalOptions {
|
||||||
@@ -205,11 +211,33 @@ export class TerminalService extends EventEmitter {
|
|||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
shell,
|
shell,
|
||||||
scrollbackBuffer: "",
|
scrollbackBuffer: "",
|
||||||
|
outputBuffer: "",
|
||||||
|
flushTimeout: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.sessions.set(id, session);
|
this.sessions.set(id, session);
|
||||||
|
|
||||||
// Forward data events and store in scrollback buffer
|
// Flush buffered output to clients (throttled)
|
||||||
|
const flushOutput = () => {
|
||||||
|
if (session.outputBuffer.length === 0) return;
|
||||||
|
|
||||||
|
// Send in batches if buffer is large
|
||||||
|
let dataToSend = session.outputBuffer;
|
||||||
|
if (dataToSend.length > OUTPUT_BATCH_SIZE) {
|
||||||
|
dataToSend = session.outputBuffer.slice(0, OUTPUT_BATCH_SIZE);
|
||||||
|
session.outputBuffer = session.outputBuffer.slice(OUTPUT_BATCH_SIZE);
|
||||||
|
// Schedule another flush for remaining data
|
||||||
|
session.flushTimeout = setTimeout(flushOutput, OUTPUT_THROTTLE_MS);
|
||||||
|
} else {
|
||||||
|
session.outputBuffer = "";
|
||||||
|
session.flushTimeout = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dataCallbacks.forEach((cb) => cb(id, dataToSend));
|
||||||
|
this.emit("data", id, dataToSend);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Forward data events with throttling
|
||||||
ptyProcess.onData((data) => {
|
ptyProcess.onData((data) => {
|
||||||
// Append to scrollback buffer
|
// Append to scrollback buffer
|
||||||
session.scrollbackBuffer += data;
|
session.scrollbackBuffer += data;
|
||||||
@@ -218,8 +246,13 @@ export class TerminalService extends EventEmitter {
|
|||||||
session.scrollbackBuffer = session.scrollbackBuffer.slice(-MAX_SCROLLBACK_SIZE);
|
session.scrollbackBuffer = session.scrollbackBuffer.slice(-MAX_SCROLLBACK_SIZE);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.dataCallbacks.forEach((cb) => cb(id, data));
|
// Buffer output for throttled delivery
|
||||||
this.emit("data", id, data);
|
session.outputBuffer += data;
|
||||||
|
|
||||||
|
// Schedule flush if not already scheduled
|
||||||
|
if (!session.flushTimeout) {
|
||||||
|
session.flushTimeout = setTimeout(flushOutput, OUTPUT_THROTTLE_MS);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle exit
|
// Handle exit
|
||||||
@@ -274,6 +307,11 @@ export class TerminalService extends EventEmitter {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
// Clean up flush timeout
|
||||||
|
if (session.flushTimeout) {
|
||||||
|
clearTimeout(session.flushTimeout);
|
||||||
|
session.flushTimeout = null;
|
||||||
|
}
|
||||||
session.pty.kill();
|
session.pty.kill();
|
||||||
this.sessions.delete(sessionId);
|
this.sessions.delete(sessionId);
|
||||||
console.log(`[Terminal] Session ${sessionId} killed`);
|
console.log(`[Terminal] Session ${sessionId} killed`);
|
||||||
@@ -339,6 +377,10 @@ export class TerminalService extends EventEmitter {
|
|||||||
console.log(`[Terminal] Cleaning up ${this.sessions.size} sessions`);
|
console.log(`[Terminal] Cleaning up ${this.sessions.size} sessions`);
|
||||||
this.sessions.forEach((session, id) => {
|
this.sessions.forEach((session, id) => {
|
||||||
try {
|
try {
|
||||||
|
// Clean up flush timeout
|
||||||
|
if (session.flushTimeout) {
|
||||||
|
clearTimeout(session.flushTimeout);
|
||||||
|
}
|
||||||
session.pty.kill();
|
session.pty.kill();
|
||||||
} catch {
|
} catch {
|
||||||
// Ignore errors during cleanup
|
// Ignore errors during cleanup
|
||||||
|
|||||||
Reference in New Issue
Block a user