12 KiB
VibeTunnel Architecture Analysis - Mario's Technical Deep Dive
This document contains comprehensive technical insights from Mario's debugging session about VibeTunnel's architecture, critical performance issues, and detailed solutions.
Executive Summary
Mario identified two critical issues causing performance problems in VibeTunnel:
- 850MB Session Bug: External terminal sessions (via
fwd.ts) bypass the clear sequence truncation instream-watcher.ts, sending entire gigabyte files instead of the last 2MB - Resize Loop: Claude terminal app issues full clear sequence (
\x1b[2J) and re-renders entire scroll buffer on every resize event, creating exponential data growth
Note: A third issue with Node-PTY's shared pipe architecture causing Electron crashes has already been resolved with a custom PTY implementation.
System Architecture
Core Components
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ Client │────▶│ Web Server │────▶│ PTY Process │
└─────────────┘ └──────────────┘ └─────────────┘
│ │ │
│ ▼ ▼
│ ┌──────────┐ ┌──────────┐
└─────────────▶│ Terminal │ │ Ascinema │
│ Manager │ │ Files │
└──────────┘ └──────────┘
Detailed Sequence Flow
sequenceDiagram
participant UI as Browser/Electron
participant WS as WebSocket
participant Server as Node Server
participant PTY as PTYManager
participant FWD as fwd.ts
participant Proc as User Process
UI->>WS: keystrokes
WS->>Server: /api/sessions/:id/input
Server->>PTY: IPC Socket message
PTY->>Proc: write to stdin
Proc-->>PTY: stdout (ANSI sequences)
PTY-->>Server: write to ascinema file
alt External Terminal
FWD-->>TTY: mirror stdout to terminal
end
Server-->>UI: SSE stream (truncated)
Key Files and Their Roles
| File | Purpose | Critical Functions |
|---|---|---|
server.ts |
Main web server | HTTP endpoints, WebSocket handling |
pty-manager.ts |
PTY lifecycle management | createSession(), setupPtyHandlers() |
stream-watcher.ts |
Monitors ascinema files | sendExistingContent() - implements clear truncation |
fwd.ts |
External terminal forwarding | Process spawning, BYPASSES TRUNCATION |
terminal-manager.ts |
Binary buffer rendering | Converts ANSI to binary cells format |
Data Flow Paths
Input Path (Keystroke → Terminal)
- Browser captures key press
- WebSocket sends to
/api/sessions/:id/input - Server writes to IPC socket
- PTY Manager writes to process stdin
- Process executes command
Output Path (Terminal → Browser)
- Process writes to stdout
- PTY Manager captures via
onDatahandler - Writes to ascinema file (with write queue for backpressure)
- Stream watcher monitors file changes
- For existing content: scans for last clear sequence
- Client receives via:
- SSE:
/api/sessions/:id/stream(text/ascinema format) - WebSocket:
/buffers(binary cell format)
- SSE:
Binary Cell Buffer Format
The terminal manager pre-renders terminal output into a binary format for efficient transmission:
For each cell at (row, column):
- Character (UTF-8 encoded)
- Foreground color (RGB values)
- Background color (RGB values)
- Attributes (bold, italic, underline, etc.)
Benefits:
- Server-side ANSI parsing eliminates client CPU usage
- Efficient binary transmission reduces bandwidth
- Only last 10,000 lines kept in memory
- Client simply renders pre-computed cells
Critical Bugs Analysis
1. The 850MB Session Loading Bug
Symptom: Sessions with large output (850MB+) cause infinite loading and browser unresponsiveness.
Root Cause: External terminal sessions via fwd.ts bypass the clear sequence truncation logic.
Technical Details:
// In stream-watcher.ts - WORKING CORRECTLY
sendExistingContent() {
// Scans backwards for last clear sequence
const lastClear = content.lastIndexOf('\x1b[2J');
// Sends only content after clear
return content.slice(lastClear); // 2MB instead of 850MB
}
Evidence from Testing:
- Test file: 980MB containing 2,400 clear sequences
- Server-created sessions: Correctly send only last 2MB
- External terminal sessions: Send entire 980MB file
- Processing time: 2-3 seconds to scan 1GB file
- Client receives instant replay for 2MB truncated content
The Issue: External terminal path doesn't trigger sendExistingContent(), sending gigabyte files to clients.
2. Resize Event Performance Catastrophe
Problem: Each resize event causes Claude to re-render the entire terminal history.
Claude's Behavior:
1. Resize event received
2. Claude issues clear sequence: \x1b[2J
3. Re-renders ENTIRE scroll buffer from line 1
4. Rendering causes viewport changes
5. Viewport changes trigger resize event
6. GOTO step 1 (infinite loop)
Technical Evidence:
- In 850MB session: each resize → full buffer re-render
- Claude renders from "Welcome to Claude" message every time
- Mobile UI particularly problematic (frequent resize events)
- Header button position shifts during rendering indicate viewport instability
- Session with 39 resize events can generate 850MB+ files
Contributing Factors:
- React Ink (TUI framework) unnecessarily re-renders entire components
- Session-detail-view has buggy resize observer
- Mobile Safari behaves differently than desktop at same viewport size
- Touch events vs mouse events complicate scrolling behavior
3. Node-PTY Architecture Flaw (ALREADY FIXED)
This issue has been resolved by implementing a custom PTY solution without the shared pipe architecture.
Ascinema Format Details
VibeTunnel uses the ascinema format for recording terminal sessions:
// Format: [timestamp, event_type, data]
[1.234, "o", "Hello World\n"] // Output event
[1.235, "i", "k"] // Input event (keypress)
[1.236, "r", "80x24"] // Resize event
Clear sequence detection:
const CLEAR_SEQUENCE = '\x1b[2J'; // ANSI clear screen
const CLEAR_WITH_HOME = '\x1b[H\x1b[2J'; // Home + clear
function findLastClearSequence(buffer) {
// Search from end for efficiency
let lastClear = buffer.lastIndexOf(CLEAR_SEQUENCE);
return lastClear === -1 ? 0 : lastClear;
}
Proposed Solutions
Priority 1: Fix External Terminal Clear Truncation (IMMEDIATE)
Problem: External terminal sessions don't use sendExistingContent() truncation.
Investigation Needed:
- Trace how
fwd.tsconnects to client streams - Determine why it bypasses stream-watcher's truncation
- Ensure external terminals use same code path as server sessions
- Test with 980MB file to verify fix
Expected Impact: Immediate fix for users experiencing infinite loading with large sessions.
Priority 2: Fix Resize Handling (INVESTIGATION)
Debugging Approach:
- Instrument session-detail-view with resize observer logging
- Identify what causes viewport expansion
- Implement resize event debouncing
- Fix mobile-specific issues:
- Keyboard state affects scrolling
- Touch vs mouse event handling
- Scrollbar visibility problems
Code to Add:
// Add to session-detail-view
let resizeCount = 0;
new ResizeObserver((entries) => {
console.log(`Resize ${++resizeCount}:`, entries[0].contentRect);
// Debounce resize events
clearTimeout(this.resizeTimeout);
this.resizeTimeout = setTimeout(() => {
this.handleResize();
}, 100);
}).observe(this.terminalElement);
Implementation Details
Write Queue Implementation
The PTY manager implements backpressure handling:
class WriteQueue {
constructor(writer) {
this.queue = [];
this.writing = false;
this.writer = writer;
}
async write(data) {
this.queue.push(data);
if (!this.writing) {
await this.flush();
}
}
async flush() {
this.writing = true;
while (this.queue.length > 0) {
const chunk = this.queue.shift();
await this.writer.write(chunk);
}
this.writing = false;
}
}
Platform-Specific Considerations
macOS:
- Screen Recording permission required for terminal access
- Terminal.app specific behaviors and quirks
Mobile Safari:
- Different behavior than desktop Safari at same viewport
- Touch events complicate scrolling
- Keyboard state affects scroll behavior
- Missing/hidden scrollbars
- Viewport meta tag issues
Windows (Future):
- ConPTY vs WinPTY support
- Different ANSI sequence handling
- Path normalization requirements
Performance Metrics
| Metric | Current | After Fix |
|---|---|---|
| 980MB session initial load | Infinite/Crash | 2-3 seconds |
| Data sent to client | 980MB | 2MB |
| Memory per terminal | 50-100MB | Target: 10MB |
| Clear sequence scan time | N/A | ~2 seconds for 1GB |
| Resize event storms | Exponential growth | Debounced |
Testing and Debugging
Test Large Session Handling
# Create large session file
cd web
npm run dev
# In another terminal, create session
SESSION_ID=$(curl -X POST localhost:3000/api/sessions | jq -r .id)
# Stop server, inject large file
cp /path/to/850mb-test-file ~/.vibetunnel/sessions/$SESSION_ID/stdout
# Restart and verify truncation works
npm run dev
Debug Resize Events
// Add to any component to detect resize loops
window.addEventListener('resize', () => {
console.count('resize');
console.trace('Resize triggered from:');
});
Monitor Network Traffic
- Check
/api/sessions/:id/streamresponse size - Verify only sends data after last clear
- Monitor WebSocket
/buffersfor binary updates
Architectural Insights
Why Current Architecture Works (When Not Bugged)
- Simplicity: "Es ist die todeleinfachste Variante" - It's the simplest possible approach
- Efficiency: 2MB instead of 980MB transmission after clear sequence truncation
- Server-side rendering: Binary cell format eliminates client ANSI parsing
Community Contribution Challenges
- High development velocity makes contribution difficult
- "Velocity kills" - rapid changes discourage contributors
- LitElement/Web Components unfamiliar to most developers
- Large file sizes cause AI tools to refuse processing
Future Architecture Considerations
Go Migration Benefits:
- Automatic test dependency tracking
- Only runs tests that changed
- Pre-allocated buffers minimize GC
- Better suited for AI-assisted development
Rust Benefits:
- 2MB static binary
- 10MB RAM usage
- Direct C interop for PTY code
- No garbage collection overhead
Action Plan Summary
-
Immediate (End of Week): Fix external terminal truncation bug
- Debug why
fwd.tsbypassessendExistingContent() - Deploy fix for immediate user relief
- Debug why
-
Short Term: Comprehensive resize fix
- Debug session-detail-view triggers
- Implement proper debouncing
- Fix mobile-specific issues
-
Long Term: Consider architecture migration
- Evaluate Rust forward binary
- Consider Go for web server
- Maintain backwards compatibility
Key Technical Quotes
- "Wir schicken 2MB statt 980MB" - We send 2MB instead of 980MB
- "Die haben einen Shared Pipe, wo alle reinschreiben" - They have a shared pipe where everyone writes
- "Es gibt keinen Grund, warum ich von da weg alles neu rendern muss" - There's no reason to re-render everything from the beginning
- "Das ist known good" - Referring to battle-tested implementations
This architecture analysis provides the technical foundation for fixing VibeTunnel's critical performance issues while maintaining its elegant simplicity.