Skip to content

No memory limit on xterm.js scrollback — each pane can consume unbounded memory #305

@Axelj00

Description

@Axelj00

Problem

The scrollback buffer size is user-configurable with a default of `5000` lines:

```ts
// pane.ts:148
scrollback: config.scrollback,
```

Each xterm.js line with 200 columns uses ~1.6KB of memory (cell data + attribute data). With the default 5000 lines:

  • Per pane: ~8MB of scrollback buffer memory
  • 10 panes: ~80MB just for terminal scrollback

But the real problem is that xterm.js allocates memory proportional to the widest line ever rendered, not the current column count. A single wide line (e.g., 1000-character error message) can permanently inflate the per-line memory for all subsequent lines in that buffer.

With agents producing heavy output

AI agents like Claude Code can produce thousands of lines of output per session. Users who increase scrollback to 10,000+ lines (common for agent workflows) can see:

  • 10 panes × 10,000 lines × 1.6KB = 160MB
  • Plus hidden-tab pending write buffers (512KB cap per pane = 5MB for 10 panes)

Code reference

`config-types.ts:128` — `scrollback: number` in config
`pane.ts:82` — `MAX_HIDDEN_PENDING_BYTES = 512 * 1024` (512KB per pane)

Proposed solution

1. Adaptive scrollback based on tab count

Reduce scrollback dynamically when many tabs are open:

```ts
const effectiveScrollback = Math.max(
1000,
Math.floor(config.scrollback / Math.max(1, totalPaneCount / 4))
);
```

2. Virtual scrollback with disk backing

For long-running agent sessions, page older scrollback to disk instead of keeping it in memory:

  • Keep the most recent 2000 lines in xterm.js
  • Write older lines to a temporary file on disk
  • Load from disk when the user scrolls back

3. Trim scrollback on hidden tabs

When a tab is hidden, trim its scrollback to a minimum (e.g., 1000 lines). Restore when shown:

```ts
hide() {
// Compact scrollback for memory savings
this.terminal.options.scrollback = 1000;
this.terminal.clear(); // Frees excess buffer memory
}
```

4. Reduce hidden-tab pending buffer

The current 512KB cap per hidden pane is generous. For 10 hidden panes, that's 5MB of raw PTY data queued in JS arrays. Consider reducing to 128KB.

5. Memory usage monitoring

Add a diagnostic command that shows per-tab memory usage:

```ts
// Command palette: "Show Memory Usage"
for (const [id, tab] of this.tabs) {
const lines = tab.terminal.buffer.active.length;
const estMb = (lines * 1600 / 1_000_000).toFixed(1);
console.log(`Tab ${id}: ${lines} lines (~${estMb} MB)`);
}
```

Impact

  • Memory: 80-160MB+ consumed by scrollback buffers alone with 10+ panes
  • GC pressure: Large xterm.js buffers increase V8 GC pause times
  • Swap thrashing: On 8GB machines, heavy multi-tab usage can push into swap
  • OOM risk: Users with 20+ tabs and high scrollback can exhaust available memory

Metrics to validate

  • Measure actual process memory (`process.memoryUsage()`) vs tab count
  • Profile xterm.js buffer memory per scrollback setting
  • Test memory usage with 20 tabs, 10k scrollback, agents producing output

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions