perf: optimize screen manager and respawn controller

- Pre-compile ANSI escape and whitespace regex patterns in respawn controller
- Pre-compile screen pattern regex for screen list parsing
- Batch ps and pgrep calls in getScreensWithStats() for multiple screens
- Reduces subprocess spawns from O(2n) to O(2) for n screens

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-01-19 11:49:59 +01:00
co-authored by Claude Opus 4.5
parent ce61a3b2f2
commit d0e5d3aed6
2 changed files with 72 additions and 10 deletions
+7 -2
View File
@@ -6,6 +6,10 @@ const MAX_RESPAWN_BUFFER_SIZE = 1024 * 1024;
// Keep this much when trimming (512KB)
const RESPAWN_BUFFER_TRIM_SIZE = 512 * 1024;
// Pre-compiled patterns for performance
const ANSI_ESCAPE_PATTERN = /\x1b\[[0-9;]*[HJKmsu?lh]/g;
const WHITESPACE_PATTERN = /\s+/g;
/**
* Respawn sequence states
*
@@ -194,9 +198,10 @@ export class RespawnController extends EventEmitter {
// Filter out noise - only count meaningful data as activity
// Ignore: cursor movements, color codes alone, small whitespace-only data
// Uses pre-compiled patterns for performance
const meaningfulData = data
.replace(/\x1b\[[0-9;]*[HJKmsu?lh]/g, '') // Remove ANSI escape sequences
.replace(/\s+/g, '') // Remove whitespace
.replace(ANSI_ESCAPE_PATTERN, '') // Remove ANSI escape sequences
.replace(WHITESPACE_PATTERN, '') // Remove whitespace
.trim();
const isMeaningfulActivity = meaningfulData.length > 0;
+65 -8
View File
@@ -7,6 +7,9 @@ import { ScreenSession, ProcessStats, ScreenSessionWithStats } from './types.js'
const SCREENS_FILE = join(homedir(), '.claudeman', 'screens.json');
// Pre-compiled regex for screen list parsing
const SCREEN_PATTERN = /(\d+)\.(claudeman-([a-f0-9-]+))/g;
export class ScreenManager extends EventEmitter {
private screens: Map<string, ScreenSession> = new Map();
private statsInterval: NodeJS.Timeout | null = null;
@@ -257,9 +260,10 @@ export class ScreenManager extends EventEmitter {
timeout: 5000
});
// Match: "12345.claudeman-abc12345 (Detached)" or similar
const screenPattern = /(\d+)\.(claudeman-([a-f0-9-]+))/g;
// Reset lastIndex since we're reusing the global regex
SCREEN_PATTERN.lastIndex = 0;
let match;
while ((match = screenPattern.exec(output)) !== null) {
while ((match = SCREEN_PATTERN.exec(output)) !== null) {
const pid = parseInt(match[1], 10);
const screenName = match[2];
const sessionIdFragment = match[3];
@@ -341,18 +345,71 @@ export class ScreenManager extends EventEmitter {
}
}
// Get all screens with stats (parallel for better performance)
// Get all screens with stats (batched for better performance)
async getScreensWithStats(): Promise<ScreenSessionWithStats[]> {
const screens = Array.from(this.screens.values());
if (screens.length === 0) {
return [];
}
// Fetch all stats in parallel
const statsPromises = screens.map(screen => this.getProcessStats(screen.sessionId));
const allStats = await Promise.all(statsPromises);
// Batch all PIDs into a single ps call for better performance
const pids = screens.map(s => s.pid);
const statsMap = new Map<number, ProcessStats>();
try {
// Single ps call for all PIDs
const psOutput = execSync(
`ps -o pid=,rss=,pcpu= -p ${pids.join(',')} 2>/dev/null || true`,
{ encoding: 'utf-8', timeout: 5000 }
).trim();
// Parse output - each line: "PID RSS CPU"
for (const line of psOutput.split('\n')) {
const parts = line.trim().split(/\s+/);
if (parts.length >= 3) {
const pid = parseInt(parts[0], 10);
const rss = parseFloat(parts[1]) || 0;
const cpu = parseFloat(parts[2]) || 0;
if (!isNaN(pid)) {
statsMap.set(pid, {
memoryMB: Math.round(rss / 1024 * 10) / 10,
cpuPercent: Math.round(cpu * 10) / 10,
childCount: 0,
updatedAt: Date.now()
});
}
}
}
// Batch child count query - single pgrep call
const pgrepOutput = execSync(
`for p in ${pids.join(' ')}; do echo "$p $(pgrep -P $p 2>/dev/null | wc -l)"; done`,
{ encoding: 'utf-8', timeout: 5000 }
).trim();
for (const line of pgrepOutput.split('\n')) {
const [pidStr, countStr] = line.trim().split(/\s+/);
const pid = parseInt(pidStr, 10);
const count = parseInt(countStr, 10) || 0;
const stats = statsMap.get(pid);
if (stats) {
stats.childCount = count;
}
}
} catch {
// Fall back to individual queries if batch fails
const statsPromises = screens.map(screen => this.getProcessStats(screen.sessionId));
const allStats = await Promise.all(statsPromises);
return screens.map((screen, i) => ({
...screen,
stats: allStats[i] || undefined
}));
}
// Combine screens with their stats
return screens.map((screen, i) => ({
return screens.map(screen => ({
...screen,
stats: allStats[i] || undefined
stats: statsMap.get(screen.pid) || undefined
}));
}