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
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 {
type: 'system' | 'assistant' | 'user' | 'result';
subtype?: string;
@@ -658,8 +662,8 @@ export class Session extends EventEmitter {
for (const line of lines) {
const trimmed = line.trim();
// Remove ANSI escape codes for JSON parsing
const cleanLine = trimmed.replace(/\x1b\[[0-9;]*m/g, '');
// Remove ANSI escape codes for JSON parsing (use pre-compiled pattern)
const cleanLine = trimmed.replace(ANSI_ESCAPE_PATTERN, '');
if (cleanLine.startsWith('{') && cleanLine.endsWith('}')) {
try {
@@ -716,12 +720,12 @@ export class Session extends EventEmitter {
// Parse token count from Claude's status line in interactive mode
// Matches patterns like "123.4k tokens", "5234 tokens", "1.2M tokens"
private parseTokensFromStatusLine(data: string): void {
// Remove ANSI escape codes for cleaner parsing
const cleanData = data.replace(/\x1b\[[0-9;]*m/g, '');
// Remove ANSI escape codes for cleaner parsing (use pre-compiled pattern)
const cleanData = data.replace(ANSI_ESCAPE_PATTERN, '');
// Match patterns: "123.4k tokens", "5234 tokens", "1.2M tokens"
// 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) {
let tokenCount = parseFloat(tokenMatch[1]);
@@ -863,10 +867,13 @@ export class Session extends EventEmitter {
this._currentTaskId = null;
// 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 {
await this._screenManager.killScreen(this.id);
console.log('[Session] Killed screen session:', this._screenSession.screenName);
const killed = await this._screenManager.killScreen(this.id);
if (killed) {
console.log('[Session] Killed screen session for:', this.id);
}
} catch (err) {
console.error('[Session] Failed to kill screen session:', err);
}
+125 -47
View File
@@ -29,9 +29,20 @@ class ClaudemanApp {
// System stats polling
this.systemStatsInterval = null;
// DOM element cache for performance (avoid repeated getElementById calls)
this._elemCache = {};
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() {
this.initTerminal();
this.loadFontSize();
@@ -92,24 +103,36 @@ class ClaudemanApp {
// Welcome message
this.showWelcome();
// Handle resize
window.addEventListener('resize', () => this.fitAddon && this.fitAddon.fit());
// Handle resize with throttling for performance
this._resizeTimeout = null;
this._lastResizeDims = null;
const resizeObserver = new ResizeObserver(() => {
if (this.fitAddon) {
this.fitAddon.fit();
if (this.activeSessionId) {
const dims = this.fitAddon.proposeDimensions();
if (dims) {
fetch(`/api/sessions/${this.activeSessionId}/resize`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ cols: dims.cols, rows: dims.rows })
});
const throttledResize = () => {
if (this._resizeTimeout) return;
this._resizeTimeout = setTimeout(() => {
this._resizeTimeout = null;
if (this.fitAddon) {
this.fitAddon.fit();
if (this.activeSessionId) {
const dims = this.fitAddon.proposeDimensions();
// Only send resize if dimensions actually changed
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);
// Handle keyboard input
@@ -291,19 +314,23 @@ class ClaudemanApp {
this.eventSource.addEventListener('session:idle', (e) => {
const data = JSON.parse(e.data);
console.log('[DEBUG] session:idle event for:', data.id);
const session = this.sessions.get(data.id);
if (session) {
session.status = 'idle';
this.renderSessionTabs();
this.sendPendingCtrlL(data.id);
}
});
this.eventSource.addEventListener('session:working', (e) => {
const data = JSON.parse(e.data);
console.log('[DEBUG] session:working event for:', data.id);
const session = this.sessions.get(data.id);
if (session) {
session.status = 'busy';
this.renderSessionTabs();
this.sendPendingCtrlL(data.id);
}
});
@@ -512,10 +539,10 @@ class ClaudemanApp {
}
_renderSessionTabsImmediate() {
const container = document.getElementById('sessionTabs');
const container = this.$('sessionTabs');
// Build tabs HTML
let html = '';
// Build tabs HTML using array for better string concatenation performance
const parts = [];
for (const [id, session] of this.sessions) {
const isActive = id === this.activeSessionId;
const status = session.status || 'idle';
@@ -524,19 +551,17 @@ class ClaudemanApp {
const taskStats = session.taskStats || { running: 0, total: 0 };
const hasRunningTasks = taskStats.running > 0;
html += `
<div class="session-tab ${isActive ? 'active' : ''}" data-id="${id}" onclick="app.selectSession('${id}')" oncontextmenu="event.preventDefault(); app.startInlineRename('${id}')">
parts.push(`<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>
${mode === 'shell' ? '<span class="tab-mode shell">sh</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>` : ''}
<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>
</div>
`;
</div>`);
}
container.innerHTML = html;
container.innerHTML = parts.join('');
}
getSessionName(session) {
@@ -597,7 +622,8 @@ class ClaudemanApp {
}
// 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();
}
@@ -824,12 +850,16 @@ class ClaudemanApp {
// Send resize to the new session
const dims = this.fitAddon.proposeDimensions();
if (dims) {
fetch(`/api/sessions/${firstSessionId}/resize`, {
await fetch(`/api/sessions/${firstSessionId}/resize`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
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();
}
@@ -929,7 +959,7 @@ class ClaudemanApp {
// ========== Respawn Banner ==========
showRespawnBanner() {
document.getElementById('respawnBanner').style.display = 'flex';
this.$('respawnBanner').style.display = 'flex';
// Also show timer if there's a timed respawn
if (this.activeSessionId && this.respawnTimers[this.activeSessionId]) {
this.showRespawnTimer();
@@ -942,16 +972,16 @@ class ClaudemanApp {
}
hideRespawnBanner() {
document.getElementById('respawnBanner').style.display = 'none';
this.$('respawnBanner').style.display = 'none';
this.hideRespawnTimer();
}
updateRespawnBanner(state) {
document.getElementById('respawnState').textContent = state.replace(/_/g, ' ');
this.$('respawnState').textContent = state.replace(/_/g, ' ');
}
showRespawnTimer() {
const timerEl = document.getElementById('respawnTimer');
const timerEl = this.$('respawnTimer');
timerEl.style.display = '';
this.updateRespawnTimer();
// Update every second
@@ -960,7 +990,7 @@ class ClaudemanApp {
}
hideRespawnTimer() {
document.getElementById('respawnTimer').style.display = 'none';
this.$('respawnTimer').style.display = 'none';
if (this.respawnTimerInterval) {
clearInterval(this.respawnTimerInterval);
this.respawnTimerInterval = null;
@@ -978,17 +1008,17 @@ class ClaudemanApp {
const remaining = Math.max(0, timer.endAt - now);
if (remaining <= 0) {
document.getElementById('respawnTimer').textContent = 'Time up';
this.$('respawnTimer').textContent = 'Time up';
delete this.respawnTimers[this.activeSessionId];
this.hideRespawnTimer();
return;
}
document.getElementById('respawnTimer').textContent = this.formatTime(remaining);
this.$('respawnTimer').textContent = this.formatTime(remaining);
}
updateRespawnTokens(totalTokens) {
const tokensEl = document.getElementById('respawnTokens');
const tokensEl = this.$('respawnTokens');
if (totalTokens > 0) {
tokensEl.style.display = '';
tokensEl.textContent = `${(totalTokens / 1000).toFixed(1)}k tokens`;
@@ -1044,6 +1074,40 @@ class ClaudemanApp {
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() {
try {
const buffer = this.terminal.buffer.active;
@@ -1151,7 +1215,7 @@ class ClaudemanApp {
});
this.totalTokens = 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 ==========
@@ -1813,18 +1877,24 @@ class ClaudemanApp {
// ========== Toast ==========
// Cached toast container for performance
_toastContainer = null;
showToast(message, type = 'info') {
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
toast.textContent = message;
let container = document.querySelector('.toast-container');
if (!container) {
container = document.createElement('div');
container.className = 'toast-container';
document.body.appendChild(container);
// Cache toast container reference
if (!this._toastContainer) {
this._toastContainer = document.querySelector('.toast-container');
if (!this._toastContainer) {
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'));
@@ -1857,10 +1927,10 @@ class ClaudemanApp {
}
updateSystemStatsDisplay(stats) {
const cpuEl = document.getElementById('statCpu');
const cpuBar = document.getElementById('statCpuBar');
const memEl = document.getElementById('statMem');
const memBar = document.getElementById('statMemBar');
const cpuEl = this.$('statCpu');
const cpuBar = this.$('statCpuBar');
const memEl = this.$('statMem');
const memBar = this.$('statMemBar');
if (cpuEl && cpuBar) {
cpuEl.textContent = `${stats.cpu}%`;
@@ -1896,11 +1966,19 @@ class ClaudemanApp {
// ========== Utility ==========
// Pre-compiled HTML escape map for performance (avoids DOM element creation)
static _htmlEscapeMap = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
};
static _htmlEscapePattern = /[&<>"']/g;
escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
return text.replace(ClaudemanApp._htmlEscapePattern, char => ClaudemanApp._htmlEscapeMap[char]);
}
}
+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 {
// Performance optimization: serialize JSON once for all clients
const message = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
for (const client of this.sseClients) {
this.sendSSE(client, event, data);
this.sendSSEPreformatted(client, message);
}
}