perf: optimize for long-running Claude sessions

Critical fixes (unbounded growth):
- Add 64KB line buffer limit with 100ms periodic flush in session.ts
- Add task tracker cleanup (max 100 completed tasks)
- Add scheduled runs auto-cleanup after 1 hour
- Fix session leaks in scheduled run iterations

High priority (performance):
- Add SSE event batching (50ms output, 100ms task updates)
- Parallelize screen stats with Promise.all()
- Add 1MB buffer limit to respawn controller

Medium priority (frontend):
- Make xterm scrollback configurable (default 5000)
- Debounce renderSessionTabs() at 100ms

New feature:
- Add CPU and memory usage display in frontend header
- New /api/system/stats endpoint

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-01-19 06:51:25 +01:00
co-authored by Claude Opus 4.5
parent 40fd13480d
commit 7979e77924
9 changed files with 389 additions and 19 deletions
+37
View File
@@ -17,6 +17,10 @@ const MAX_TEXT_OUTPUT_SIZE = 2 * 1024 * 1024;
const TEXT_OUTPUT_TRIM_SIZE = 1.5 * 1024 * 1024;
// Maximum number of Claude messages to keep in memory
const MAX_MESSAGES = 1000;
// Maximum line buffer size (64KB) - prevents unbounded growth for long lines
const MAX_LINE_BUFFER_SIZE = 64 * 1024;
// Line buffer flush interval (100ms) - forces processing of partial lines
const LINE_BUFFER_FLUSH_INTERVAL = 100;
// Filter out terminal focus escape sequences (focus in/out reports)
// ^[[I (focus in), ^[[O (focus out), and the enable/disable sequences
@@ -77,6 +81,7 @@ export class Session extends EventEmitter {
private _totalCost: number = 0;
private _messages: ClaudeMessage[] = [];
private _lineBuffer: string = '';
private _lineBufferFlushTimer: NodeJS.Timeout | null = null;
private resolvePromise: ((value: { result: string; cost: number }) => void) | null = null;
private rejectPromise: ((reason: Error) => void) | null = null;
private _isWorking: boolean = false;
@@ -605,9 +610,35 @@ export class Session extends EventEmitter {
private processOutput(data: string): void {
// Try to extract JSON from output (Claude may output JSON in stream mode)
this._lineBuffer += data;
// Prevent unbounded line buffer growth for very long lines
if (this._lineBuffer.length > MAX_LINE_BUFFER_SIZE) {
// Force flush the oversized buffer as text output
this._textOutput += this._lineBuffer + '\n';
this._lineBuffer = '';
}
// Start flush timer if not running (handles partial lines after 100ms)
if (!this._lineBufferFlushTimer && this._lineBuffer.length > 0) {
this._lineBufferFlushTimer = setTimeout(() => {
this._lineBufferFlushTimer = null;
if (this._lineBuffer.length > 0) {
// Flush partial line as text output
this._textOutput += this._lineBuffer;
this._lineBuffer = '';
}
}, LINE_BUFFER_FLUSH_INTERVAL);
}
const lines = this._lineBuffer.split('\n');
this._lineBuffer = lines.pop() || '';
// Clear flush timer if buffer is now empty
if (this._lineBuffer.length === 0 && this._lineBufferFlushTimer) {
clearTimeout(this._lineBufferFlushTimer);
this._lineBufferFlushTimer = null;
}
for (const line of lines) {
const trimmed = line.trim();
// Remove ANSI escape codes for JSON parsing
@@ -771,6 +802,12 @@ export class Session extends EventEmitter {
this.activityTimeout = null;
}
// Clear line buffer flush timer
if (this._lineBufferFlushTimer) {
clearTimeout(this._lineBufferFlushTimer);
this._lineBufferFlushTimer = null;
}
if (this.ptyProcess) {
const pid = this.ptyProcess.pid;