# Munder-Difflin Performance Considerations: How to Run Dozens of AI Agents Without CPU or Memory Bottlenecks

> Discover Munder-Difflin performance considerations. Run dozens of AI agents seamlessly without CPU or memory bottlenecks using its unique two-plane architecture and persistent terminal pools.

- Repository: [Chaitanya Giri/munder-difflin](https://github.com/chaitanyagiri/munder-difflin)
- Tags: performance
- Published: 2026-08-20

---

**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`](https://github.com/chaitanyagiri/munder-difflin/blob/main/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](https://github.com/chaitanyagiri/munder-difflin/blob/main/SPEC.md#L28-L46), 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`](https://github.com/chaitanyagiri/munder-difflin/blob/main/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()`.

```typescript
// 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](https://github.com/chaitanyagiri/munder-difflin/blob/main/blog/src/posts/rendering-many-live-terminals-performance.md#L49-L58), 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:

```tsx
// 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](https://github.com/chaitanyagiri/munder-difflin/blob/main/blog/src/posts/rendering-many-live-terminals-performance.md#L74-L82).

## 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`](https://github.com/chaitanyagiri/munder-difflin/blob/main/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**:

```typescript
// 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](https://github.com/chaitanyagiri/munder-difflin/blob/main/blog/src/posts/rendering-many-live-terminals-performance.md#L85-L93) 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:

```typescript
// Conditionally enable based on throughput measurement
if (highThroughput) {
  term.loadAddon(new WebglAddon()); // switch to canvas/WebGL
}

```

This **technique 5**, described in the [performance blog](https://github.com/chaitanyagiri/munder-difflin/blob/main/blog/src/posts/rendering-many-live-terminals-performance.md#L14-L20), 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:

1. Messages are sent only when the target is alive
2. [`xterm.js`](https://github.com/chaitanyagiri/munder-difflin/blob/main/xterm.js) batches writes internally to reduce serialization overhead

This **technique 4**, detailed in the [performance blog](https://github.com/chaitanyagiri/munder-difflin/blob/main/blog/src/posts/rendering-many-live-terminals-performance.md#L96-L105), 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)](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)](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`](https://github.com/chaitanyagiri/munder-difflin/blob/main/SPEC.md) | Two-plane architecture diagram, SQLite schema |
| [`DESIGN.md`](https://github.com/chaitanyagiri/munder-difflin/blob/main/DESIGN.md) | Pixi.js avatar rendering strategy |
| [`blog/src/posts/rendering-many-live-terminals-performance.md`](https://github.com/chaitanyagiri/munder-difflin/blob/main/blog/src/posts/rendering-many-live-terminals-performance.md) | Complete performance techniques playbook |
| [`blog/src/posts/visualizing-ai-agents-pixijs.md`](https://github.com/chaitanyagiri/munder-difflin/blob/main/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`](https://github.com/chaitanyagiri/munder-difflin/blob/main/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.