Munder-Difflin Performance Considerations: How to Run Dozens of AI Agents Without CPU or Memory Bottlenecks
Munder-Difflin achieves smooth real-time performance through a two-plane architecture that decouples data streams from rendering, persistent terminal pools that eliminate object recreation overhead, and strict bounds on scrollback memory—enabling dozens of live Claude Code agents on a single desktop.
Munder-Difflin is a real-time, multi-agent desktop UI built on Electron. Its performance hinges on architectural decisions that keep the hot path (the UI main loop) as lean as possible while managing heavy I/O from multiple terminal sessions and AI agents. This article breaks down the specific techniques implemented in the chaitanyagiri/munder-difflin codebase that make this scalability possible.
Two-Plane Architecture: Separating Data from Rendering
The foundation of Munder-Difflin performance is its two-plane architecture, documented in SPEC.md. This design isolates two distinct data streams:
- Event Plane: JSON events from Claude Code hooks that drive avatar state
- Terminal Plane: Raw PTY bytes from tmux sessions that feed terminal displays
This separation, illustrated in the two-plane diagram, ensures that heavy I/O operations never block the graphics loop. The event-driven avatar state updates independently from the byte-stream terminal view, preventing either stream from saturating the main thread.
Persistent Terminal Pool: Eliminating Recreation Overhead
One of the most critical performance optimizations is the persistent terminal pool pattern. Rather than creating and destroying xterm.js instances as users switch tabs or views, Munder-Difflin maintains one Terminal instance per PTY for the entire application lifetime.
How the Pool Works
The Terminal object and its associated PTY process live in a map keyed by entry ID. Views only borrow the terminal's host element—detaching and re-attaching it as needed without ever calling Terminal.dispose().
// Conceptual implementation based on src/main/terminalPool.ts pattern
import { Terminal } from 'xterm';
import { spawn } from 'node-pty';
type TerminalEntry = {
term: Terminal;
host: HTMLElement;
opened: boolean;
};
const pool = new Map<string, TerminalEntry>();
export function getOrCreate(entryId: string, ptyCommand: string, args: string[]) {
if (!pool.has(entryId)) {
const pty = spawn(ptyCommand, args, { name: 'xterm-color' });
const term = new Terminal();
const host = document.createElement('div'); // detached host
pty.onData(data => term.write(data));
pool.set(entryId, { term, host, opened: false });
}
return pool.get(entryId)!;
}
This pattern guarantees that PTY output is always buffered and eliminates the CPU thrash of repeatedly constructing Terminal objects. As documented in the performance blog post, this is technique 1 for rendering many live terminals.
Borrow-Only View Pattern
The renderer implements a borrow-only pattern where the view merely re-parents the existing host element:
// Conceptual implementation based on src/renderer/TerminalView.tsx pattern
import React, { useEffect, useRef } from 'react';
import { getOrCreate } from '../main/terminalPool';
export function TerminalView({ entryId }: { entryId: string }) {
const container = useRef<HTMLDivElement>(null);
useEffect(() => {
const { term, host, opened } = getOrCreate(entryId, 'bash', []);
if (container.current) {
container.current.appendChild(host);
if (!opened) {
term.open(host);
opened = true;
}
}
return () => {
// detach but keep alive
if (container.current?.contains(host)) {
container.current.removeChild(host);
}
};
}, [entryId]);
return <div ref={container} className="terminal-view" />;
}
The cleanup function only detaches—never destroys—the terminal. This is technique 2 in the performance playbook: render-only-visible terminals.
Render-Only-Visible Terminals
Munder-Difflin reduces per-frame rendering cost from #terminals × draw cost to visible-terminals × draw cost by ensuring only on-screen terminals are painted each frame. Background terminals continue receiving data in their xterm.js buffers without triggering any DOM updates until they become visible.
This visibility-aware rendering is essential for scenarios with dozens of agent terminals, most of which are off-screen at any given moment.
Bounded Scrollback: Capping Memory Per Terminal
Unbounded terminal scrollback would cause runaway RAM consumption when many terminals are active. Munder-Difflin enforces a bounded scrollback limit:
// src/renderer/setupTerminal.ts pattern
term.setOption('scrollBack', 5000); // limit to 5k lines per terminal
With a 10,000-line default (configurable to 5,000 lines as shown), this technique 3 from the performance blog prevents memory explosion while preserving enough history for interactive debugging.
Optional Accelerated Renderer: Profile-First Optimization
Rather than defaulting to the heaviest rendering path, Munder-Difflin follows a measure-first guideline. The default DOM renderer suffices for most cases. Only after profiling identifies a bottleneck does the system enable the Canvas/WebGL renderer:
// Conditionally enable based on throughput measurement
if (highThroughput) {
term.loadAddon(new WebglAddon()); // switch to canvas/WebGL
}
This technique 5, described in the performance blog, keeps CPU usage flat for high-throughput streams without burdening normal usage with unnecessary complexity.
Efficient IPC: Dedicated Channels with Batched Writes
Each PTY operates on a dedicated IPC channel between main and renderer processes. The implementation follows two rules:
- Messages are sent only when the target is alive
xterm.jsbatches writes internally to reduce serialization overhead
This technique 4, detailed in the performance blog, avoids unnecessary data copying and protects against race conditions when a PTY exits.
Pixi.js Avatar Rendering: Lightweight Sprite Animation
The avatar visualization layer uses Pixi.js with deliberately simple rendering:
- 32–64 pixel sprite sizes
- Lightweight lerp-based movement (no physics engine)
- No pathfinding in the MVP
As noted in [DESIGN.md](https://github.com/chaitanyagiri/munder-difflin/blob/main/DESIGN.md#L21-L25), these constraints guarantee 60fps even when the floor contains dozens of agents. Avatar state updates flow through the Event Plane, completely separate from terminal rendering.
SQLite Persistence: Zero-Configuration, Memory-Efficient Storage
All metadata—agent configurations, layout state, and event history—persists to a single-file SQLite database via better-sqlite3:
- Fast synchronous reads/writes
- No separate server process
- Minimal memory footprint compared to in-memory stores or full database servers
The schema is defined in [SPEC.md](https://github.com/chaitanyagiri/munder-difflin/blob/main/SPEC.md#L3-L41), enabling quick session restoration without bloating the application's working set.
Key Source Files for Performance Analysis
| File | Purpose |
|---|---|
SPEC.md |
Two-plane architecture diagram, SQLite schema |
DESIGN.md |
Pixi.js avatar rendering strategy |
blog/src/posts/rendering-many-live-terminals-performance.md |
Complete performance techniques playbook |
blog/src/posts/visualizing-ai-agents-pixijs.md |
Avatar rendering performance notes |
Summary
- Keep the hot loop tiny: Only the Pixi canvas and visible terminals run each frame
- Decouple lifetimes: PTY-terminal objects outlive React views, avoiding recreation overhead
- Batch and bound: Scrollback is capped, writes are batched, and accelerated rendering is opt-in after measurement
- Measure before optimizing: The codebase follows explicit "profile-first" guidelines to prevent premature complexity
These strategies enable Munder-Difflin to display dozens of live Claude Code agents on a single desktop without saturating CPU or exhausting RAM.
Frequently Asked Questions
How does Munder-Difflin handle 50+ terminal sessions without lag?
Munder-Difflin uses a persistent terminal pool where xterm.js instances live for the entire application lifetime. Only the DOM host element is moved between views, eliminating the CPU cost of repeatedly creating and destroying terminals. Combined with render-only-visible optimization, the per-frame cost scales with visible terminals rather than total terminals.
Why does Munder-Difflin limit terminal scrollback?
Scrollback is bounded (typically 5,000–10,000 lines) to cap memory usage per terminal. Without this bound, long-running agent sessions with verbose output would consume unbounded RAM. The limit preserves enough history for debugging while keeping total memory predictable when dozens of terminals are active.
When should I enable the WebGL renderer in xterm.js?
Enable WebglAddon only after profiling shows the DOM renderer is a bottleneck. Munder-Difflin's default DOM renderer handles most workloads efficiently; the accelerated renderer adds complexity and should be reserved for high-throughput scenarios where measurement confirms a need.
How does the two-plane architecture improve performance?
The Event Plane (JSON from Claude Code hooks) and Terminal Plane (raw PTY bytes) operate on independent paths that never block each other. This decouples heavy I/O from the graphics loop, ensuring that terminal data floods or agent event bursts cannot stall the UI main thread.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →