perf: optimize hot paths across server, session, and frontend

- Pre-compile regex patterns (ANSI escape, token parsing) in session.ts
- Optimize SSE broadcast to serialize JSON once for all clients
- Add DOM element caching ($() helper) to avoid repeated getElementById
- Throttle resize observer with dimension change detection
- Use array.join() for session tab HTML building
- Replace DOM-based escapeHtml with regex replacement
- Cache toast container reference

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-01-19 11:48:02 +01:00
co-authored by Claude Opus 4.5
parent 0af813b3c4
commit ce61a3b2f2
3 changed files with 152 additions and 56 deletions
+15 -8
View File
@@ -26,6 +26,10 @@ const LINE_BUFFER_FLUSH_INTERVAL = 100;
// ^[[I (focus in), ^[[O (focus out), and the enable/disable sequences // ^[[I (focus in), ^[[O (focus out), and the enable/disable sequences
const FOCUS_ESCAPE_FILTER = /\x1b\[\?1004[hl]|\x1b\[[IO]/g; const FOCUS_ESCAPE_FILTER = /\x1b\[\?1004[hl]|\x1b\[[IO]/g;
// Pre-compiled regex patterns for performance (avoid re-compilation on each call)
const ANSI_ESCAPE_PATTERN = /\x1b\[[0-9;]*m/g;
const TOKEN_PATTERN = /(\d+(?:\.\d+)?)\s*([kKmM])?\s*tokens/;
export interface ClaudeMessage { export interface ClaudeMessage {
type: 'system' | 'assistant' | 'user' | 'result'; type: 'system' | 'assistant' | 'user' | 'result';
subtype?: string; subtype?: string;
@@ -658,8 +662,8 @@ export class Session extends EventEmitter {
for (const line of lines) { for (const line of lines) {
const trimmed = line.trim(); const trimmed = line.trim();
// Remove ANSI escape codes for JSON parsing // Remove ANSI escape codes for JSON parsing (use pre-compiled pattern)
const cleanLine = trimmed.replace(/\x1b\[[0-9;]*m/g, ''); const cleanLine = trimmed.replace(ANSI_ESCAPE_PATTERN, '');
if (cleanLine.startsWith('{') && cleanLine.endsWith('}')) { if (cleanLine.startsWith('{') && cleanLine.endsWith('}')) {
try { try {
@@ -716,12 +720,12 @@ export class Session extends EventEmitter {
// Parse token count from Claude's status line in interactive mode // Parse token count from Claude's status line in interactive mode
// Matches patterns like "123.4k tokens", "5234 tokens", "1.2M tokens" // Matches patterns like "123.4k tokens", "5234 tokens", "1.2M tokens"
private parseTokensFromStatusLine(data: string): void { private parseTokensFromStatusLine(data: string): void {
// Remove ANSI escape codes for cleaner parsing // Remove ANSI escape codes for cleaner parsing (use pre-compiled pattern)
const cleanData = data.replace(/\x1b\[[0-9;]*m/g, ''); const cleanData = data.replace(ANSI_ESCAPE_PATTERN, '');
// Match patterns: "123.4k tokens", "5234 tokens", "1.2M tokens" // Match patterns: "123.4k tokens", "5234 tokens", "1.2M tokens"
// The status line typically shows total tokens like "1.2k tokens" near the prompt // The status line typically shows total tokens like "1.2k tokens" near the prompt
const tokenMatch = cleanData.match(/(\d+(?:\.\d+)?)\s*([kKmM])?\s*tokens/); const tokenMatch = cleanData.match(TOKEN_PATTERN);
if (tokenMatch) { if (tokenMatch) {
let tokenCount = parseFloat(tokenMatch[1]); let tokenCount = parseFloat(tokenMatch[1]);
@@ -863,10 +867,13 @@ export class Session extends EventEmitter {
this._currentTaskId = null; this._currentTaskId = null;
// Kill the associated screen session if requested // Kill the associated screen session if requested
if (this._screenSession && this._screenManager && killScreen) { if (killScreen && this._screenManager) {
// Try to kill screen even if _screenSession is not set (e.g., restored sessions)
try { try {
await this._screenManager.killScreen(this.id); const killed = await this._screenManager.killScreen(this.id);
console.log('[Session] Killed screen session:', this._screenSession.screenName); if (killed) {
console.log('[Session] Killed screen session for:', this.id);
}
} catch (err) { } catch (err) {
console.error('[Session] Failed to kill screen session:', err); console.error('[Session] Failed to kill screen session:', err);
} }
+125 -47
View File
@@ -29,9 +29,20 @@ class ClaudemanApp {
// System stats polling // System stats polling
this.systemStatsInterval = null; this.systemStatsInterval = null;
// DOM element cache for performance (avoid repeated getElementById calls)
this._elemCache = {};
this.init(); this.init();
} }
// Cached element getter - avoids repeated DOM queries
$(id) {
if (!this._elemCache[id]) {
this._elemCache[id] = document.getElementById(id);
}
return this._elemCache[id];
}
init() { init() {
this.initTerminal(); this.initTerminal();
this.loadFontSize(); this.loadFontSize();
@@ -92,24 +103,36 @@ class ClaudemanApp {
// Welcome message // Welcome message
this.showWelcome(); this.showWelcome();
// Handle resize // Handle resize with throttling for performance
window.addEventListener('resize', () => this.fitAddon && this.fitAddon.fit()); this._resizeTimeout = null;
this._lastResizeDims = null;
const resizeObserver = new ResizeObserver(() => { const throttledResize = () => {
if (this.fitAddon) { if (this._resizeTimeout) return;
this.fitAddon.fit(); this._resizeTimeout = setTimeout(() => {
if (this.activeSessionId) { this._resizeTimeout = null;
const dims = this.fitAddon.proposeDimensions(); if (this.fitAddon) {
if (dims) { this.fitAddon.fit();
fetch(`/api/sessions/${this.activeSessionId}/resize`, { if (this.activeSessionId) {
method: 'POST', const dims = this.fitAddon.proposeDimensions();
headers: { 'Content-Type': 'application/json' }, // Only send resize if dimensions actually changed
body: JSON.stringify({ cols: dims.cols, rows: dims.rows }) if (dims && (!this._lastResizeDims ||
}); dims.cols !== this._lastResizeDims.cols ||
dims.rows !== this._lastResizeDims.rows)) {
this._lastResizeDims = { cols: dims.cols, rows: dims.rows };
fetch(`/api/sessions/${this.activeSessionId}/resize`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ cols: dims.cols, rows: dims.rows })
});
}
} }
} }
} }, 100); // Throttle to 100ms
}); };
window.addEventListener('resize', throttledResize);
const resizeObserver = new ResizeObserver(throttledResize);
resizeObserver.observe(container); resizeObserver.observe(container);
// Handle keyboard input // Handle keyboard input
@@ -291,19 +314,23 @@ class ClaudemanApp {
this.eventSource.addEventListener('session:idle', (e) => { this.eventSource.addEventListener('session:idle', (e) => {
const data = JSON.parse(e.data); const data = JSON.parse(e.data);
console.log('[DEBUG] session:idle event for:', data.id);
const session = this.sessions.get(data.id); const session = this.sessions.get(data.id);
if (session) { if (session) {
session.status = 'idle'; session.status = 'idle';
this.renderSessionTabs(); this.renderSessionTabs();
this.sendPendingCtrlL(data.id);
} }
}); });
this.eventSource.addEventListener('session:working', (e) => { this.eventSource.addEventListener('session:working', (e) => {
const data = JSON.parse(e.data); const data = JSON.parse(e.data);
console.log('[DEBUG] session:working event for:', data.id);
const session = this.sessions.get(data.id); const session = this.sessions.get(data.id);
if (session) { if (session) {
session.status = 'busy'; session.status = 'busy';
this.renderSessionTabs(); this.renderSessionTabs();
this.sendPendingCtrlL(data.id);
} }
}); });
@@ -512,10 +539,10 @@ class ClaudemanApp {
} }
_renderSessionTabsImmediate() { _renderSessionTabsImmediate() {
const container = document.getElementById('sessionTabs'); const container = this.$('sessionTabs');
// Build tabs HTML // Build tabs HTML using array for better string concatenation performance
let html = ''; const parts = [];
for (const [id, session] of this.sessions) { for (const [id, session] of this.sessions) {
const isActive = id === this.activeSessionId; const isActive = id === this.activeSessionId;
const status = session.status || 'idle'; const status = session.status || 'idle';
@@ -524,19 +551,17 @@ class ClaudemanApp {
const taskStats = session.taskStats || { running: 0, total: 0 }; const taskStats = session.taskStats || { running: 0, total: 0 };
const hasRunningTasks = taskStats.running > 0; const hasRunningTasks = taskStats.running > 0;
html += ` parts.push(`<div class="session-tab ${isActive ? 'active' : ''}" data-id="${id}" onclick="app.selectSession('${id}')" oncontextmenu="event.preventDefault(); app.startInlineRename('${id}')">
<div class="session-tab ${isActive ? 'active' : ''}" data-id="${id}" onclick="app.selectSession('${id}')" oncontextmenu="event.preventDefault(); app.startInlineRename('${id}')">
<span class="tab-status ${status}"></span> <span class="tab-status ${status}"></span>
${mode === 'shell' ? '<span class="tab-mode shell">sh</span>' : ''} ${mode === 'shell' ? '<span class="tab-mode shell">sh</span>' : ''}
<span class="tab-name" data-session-id="${id}">${this.escapeHtml(name)}</span> <span class="tab-name" data-session-id="${id}">${this.escapeHtml(name)}</span>
${hasRunningTasks ? `<span class="tab-badge" onclick="event.stopPropagation(); app.toggleTaskPanel()">${taskStats.running}</span>` : ''} ${hasRunningTasks ? `<span class="tab-badge" onclick="event.stopPropagation(); app.toggleTaskPanel()">${taskStats.running}</span>` : ''}
<span class="tab-gear" onclick="event.stopPropagation(); app.openSessionOptions('${id}')" title="Session options">&#x2699;</span> <span class="tab-gear" onclick="event.stopPropagation(); app.openSessionOptions('${id}')" title="Session options">&#x2699;</span>
<span class="tab-close" onclick="event.stopPropagation(); app.requestCloseSession('${id}')">&times;</span> <span class="tab-close" onclick="event.stopPropagation(); app.requestCloseSession('${id}')">&times;</span>
</div> </div>`);
`;
} }
container.innerHTML = html; container.innerHTML = parts.join('');
} }
getSessionName(session) { getSessionName(session) {
@@ -597,7 +622,8 @@ class ClaudemanApp {
} }
// Update task panel if open // Update task panel if open
if (document.getElementById('taskPanel').classList.contains('open')) { const taskPanel = document.getElementById('taskPanel');
if (taskPanel && taskPanel.classList.contains('open')) {
this.renderTaskPanel(); this.renderTaskPanel();
} }
@@ -824,12 +850,16 @@ class ClaudemanApp {
// Send resize to the new session // Send resize to the new session
const dims = this.fitAddon.proposeDimensions(); const dims = this.fitAddon.proposeDimensions();
if (dims) { if (dims) {
fetch(`/api/sessions/${firstSessionId}/resize`, { await fetch(`/api/sessions/${firstSessionId}/resize`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ cols: dims.cols, rows: dims.rows }) body: JSON.stringify({ cols: dims.cols, rows: dims.rows })
}); });
} }
// Mark this session as needing Ctrl+L fix once Claude is running
this.pendingCtrlL = this.pendingCtrlL || new Set();
this.pendingCtrlL.add(firstSessionId);
console.log('[DEBUG] Marked session for pending Ctrl+L:', firstSessionId);
this.loadQuickStartCases(); this.loadQuickStartCases();
} }
@@ -929,7 +959,7 @@ class ClaudemanApp {
// ========== Respawn Banner ========== // ========== Respawn Banner ==========
showRespawnBanner() { showRespawnBanner() {
document.getElementById('respawnBanner').style.display = 'flex'; this.$('respawnBanner').style.display = 'flex';
// Also show timer if there's a timed respawn // Also show timer if there's a timed respawn
if (this.activeSessionId && this.respawnTimers[this.activeSessionId]) { if (this.activeSessionId && this.respawnTimers[this.activeSessionId]) {
this.showRespawnTimer(); this.showRespawnTimer();
@@ -942,16 +972,16 @@ class ClaudemanApp {
} }
hideRespawnBanner() { hideRespawnBanner() {
document.getElementById('respawnBanner').style.display = 'none'; this.$('respawnBanner').style.display = 'none';
this.hideRespawnTimer(); this.hideRespawnTimer();
} }
updateRespawnBanner(state) { updateRespawnBanner(state) {
document.getElementById('respawnState').textContent = state.replace(/_/g, ' '); this.$('respawnState').textContent = state.replace(/_/g, ' ');
} }
showRespawnTimer() { showRespawnTimer() {
const timerEl = document.getElementById('respawnTimer'); const timerEl = this.$('respawnTimer');
timerEl.style.display = ''; timerEl.style.display = '';
this.updateRespawnTimer(); this.updateRespawnTimer();
// Update every second // Update every second
@@ -960,7 +990,7 @@ class ClaudemanApp {
} }
hideRespawnTimer() { hideRespawnTimer() {
document.getElementById('respawnTimer').style.display = 'none'; this.$('respawnTimer').style.display = 'none';
if (this.respawnTimerInterval) { if (this.respawnTimerInterval) {
clearInterval(this.respawnTimerInterval); clearInterval(this.respawnTimerInterval);
this.respawnTimerInterval = null; this.respawnTimerInterval = null;
@@ -978,17 +1008,17 @@ class ClaudemanApp {
const remaining = Math.max(0, timer.endAt - now); const remaining = Math.max(0, timer.endAt - now);
if (remaining <= 0) { if (remaining <= 0) {
document.getElementById('respawnTimer').textContent = 'Time up'; this.$('respawnTimer').textContent = 'Time up';
delete this.respawnTimers[this.activeSessionId]; delete this.respawnTimers[this.activeSessionId];
this.hideRespawnTimer(); this.hideRespawnTimer();
return; return;
} }
document.getElementById('respawnTimer').textContent = this.formatTime(remaining); this.$('respawnTimer').textContent = this.formatTime(remaining);
} }
updateRespawnTokens(totalTokens) { updateRespawnTokens(totalTokens) {
const tokensEl = document.getElementById('respawnTokens'); const tokensEl = this.$('respawnTokens');
if (totalTokens > 0) { if (totalTokens > 0) {
tokensEl.style.display = ''; tokensEl.style.display = '';
tokensEl.textContent = `${(totalTokens / 1000).toFixed(1)}k tokens`; tokensEl.textContent = `${(totalTokens / 1000).toFixed(1)}k tokens`;
@@ -1044,6 +1074,40 @@ class ClaudemanApp {
this.terminal.clear(); this.terminal.clear();
} }
// Send Ctrl+L to fix display for newly created sessions once Claude is running
sendPendingCtrlL(sessionId) {
console.log('[DEBUG] sendPendingCtrlL called for:', sessionId, 'pending:', this.pendingCtrlL ? [...this.pendingCtrlL] : 'none');
if (!this.pendingCtrlL || !this.pendingCtrlL.has(sessionId)) {
console.log('[DEBUG] No pending Ctrl+L for this session');
return;
}
this.pendingCtrlL.delete(sessionId);
// Only send if this is the active session
if (sessionId !== this.activeSessionId) {
console.log('[DEBUG] Not active session, skipping Ctrl+L');
return;
}
console.log('[DEBUG] Sending resize + Ctrl+L for session:', sessionId);
// Send resize + Ctrl+L to fix the display
const dims = this.fitAddon.proposeDimensions();
if (dims) {
fetch(`/api/sessions/${sessionId}/resize`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ cols: dims.cols, rows: dims.rows })
}).then(() => {
console.log('[DEBUG] Resize sent, now sending Ctrl+L');
fetch(`/api/sessions/${sessionId}/input`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ input: '\x0c' })
});
});
}
}
async copyTerminal() { async copyTerminal() {
try { try {
const buffer = this.terminal.buffer.active; const buffer = this.terminal.buffer.active;
@@ -1151,7 +1215,7 @@ class ClaudemanApp {
}); });
this.totalTokens = total; this.totalTokens = total;
const display = total >= 1000 ? `${(total / 1000).toFixed(1)}k` : total; const display = total >= 1000 ? `${(total / 1000).toFixed(1)}k` : total;
document.getElementById('headerTokens').textContent = `${display} tokens`; this.$('headerTokens').textContent = `${display} tokens`;
} }
// ========== Session Options Modal ========== // ========== Session Options Modal ==========
@@ -1813,18 +1877,24 @@ class ClaudemanApp {
// ========== Toast ========== // ========== Toast ==========
// Cached toast container for performance
_toastContainer = null;
showToast(message, type = 'info') { showToast(message, type = 'info') {
const toast = document.createElement('div'); const toast = document.createElement('div');
toast.className = `toast toast-${type}`; toast.className = `toast toast-${type}`;
toast.textContent = message; toast.textContent = message;
let container = document.querySelector('.toast-container'); // Cache toast container reference
if (!container) { if (!this._toastContainer) {
container = document.createElement('div'); this._toastContainer = document.querySelector('.toast-container');
container.className = 'toast-container'; if (!this._toastContainer) {
document.body.appendChild(container); this._toastContainer = document.createElement('div');
this._toastContainer.className = 'toast-container';
document.body.appendChild(this._toastContainer);
}
} }
container.appendChild(toast); this._toastContainer.appendChild(toast);
requestAnimationFrame(() => toast.classList.add('show')); requestAnimationFrame(() => toast.classList.add('show'));
@@ -1857,10 +1927,10 @@ class ClaudemanApp {
} }
updateSystemStatsDisplay(stats) { updateSystemStatsDisplay(stats) {
const cpuEl = document.getElementById('statCpu'); const cpuEl = this.$('statCpu');
const cpuBar = document.getElementById('statCpuBar'); const cpuBar = this.$('statCpuBar');
const memEl = document.getElementById('statMem'); const memEl = this.$('statMem');
const memBar = document.getElementById('statMemBar'); const memBar = this.$('statMemBar');
if (cpuEl && cpuBar) { if (cpuEl && cpuBar) {
cpuEl.textContent = `${stats.cpu}%`; cpuEl.textContent = `${stats.cpu}%`;
@@ -1896,11 +1966,19 @@ class ClaudemanApp {
// ========== Utility ========== // ========== Utility ==========
// Pre-compiled HTML escape map for performance (avoids DOM element creation)
static _htmlEscapeMap = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
};
static _htmlEscapePattern = /[&<>"']/g;
escapeHtml(text) { escapeHtml(text) {
if (!text) return ''; if (!text) return '';
const div = document.createElement('div'); return text.replace(ClaudemanApp._htmlEscapePattern, char => ClaudemanApp._htmlEscapeMap[char]);
div.textContent = text;
return div.innerHTML;
} }
} }
+12 -1
View File
@@ -1160,9 +1160,20 @@ export class WebServer extends EventEmitter {
} }
} }
// Optimized: send pre-formatted SSE message to a client
private sendSSEPreformatted(reply: FastifyReply, message: string): void {
try {
reply.raw.write(message);
} catch {
this.sseClients.delete(reply);
}
}
private broadcast(event: string, data: unknown): void { private broadcast(event: string, data: unknown): void {
// Performance optimization: serialize JSON once for all clients
const message = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
for (const client of this.sseClients) { for (const client of this.sseClients) {
this.sendSSE(client, event, data); this.sendSSEPreformatted(client, message);
} }
} }