mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-08 08:29:42 +02:00
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:
@@ -6,6 +6,10 @@ const MAX_RESPAWN_BUFFER_SIZE = 1024 * 1024;
|
|||||||
// Keep this much when trimming (512KB)
|
// Keep this much when trimming (512KB)
|
||||||
const RESPAWN_BUFFER_TRIM_SIZE = 512 * 1024;
|
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
|
* Respawn sequence states
|
||||||
*
|
*
|
||||||
@@ -194,9 +198,10 @@ export class RespawnController extends EventEmitter {
|
|||||||
|
|
||||||
// Filter out noise - only count meaningful data as activity
|
// Filter out noise - only count meaningful data as activity
|
||||||
// Ignore: cursor movements, color codes alone, small whitespace-only data
|
// Ignore: cursor movements, color codes alone, small whitespace-only data
|
||||||
|
// Uses pre-compiled patterns for performance
|
||||||
const meaningfulData = data
|
const meaningfulData = data
|
||||||
.replace(/\x1b\[[0-9;]*[HJKmsu?lh]/g, '') // Remove ANSI escape sequences
|
.replace(ANSI_ESCAPE_PATTERN, '') // Remove ANSI escape sequences
|
||||||
.replace(/\s+/g, '') // Remove whitespace
|
.replace(WHITESPACE_PATTERN, '') // Remove whitespace
|
||||||
.trim();
|
.trim();
|
||||||
|
|
||||||
const isMeaningfulActivity = meaningfulData.length > 0;
|
const isMeaningfulActivity = meaningfulData.length > 0;
|
||||||
|
|||||||
+65
-8
@@ -7,6 +7,9 @@ import { ScreenSession, ProcessStats, ScreenSessionWithStats } from './types.js'
|
|||||||
|
|
||||||
const SCREENS_FILE = join(homedir(), '.claudeman', 'screens.json');
|
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 {
|
export class ScreenManager extends EventEmitter {
|
||||||
private screens: Map<string, ScreenSession> = new Map();
|
private screens: Map<string, ScreenSession> = new Map();
|
||||||
private statsInterval: NodeJS.Timeout | null = null;
|
private statsInterval: NodeJS.Timeout | null = null;
|
||||||
@@ -257,9 +260,10 @@ export class ScreenManager extends EventEmitter {
|
|||||||
timeout: 5000
|
timeout: 5000
|
||||||
});
|
});
|
||||||
// Match: "12345.claudeman-abc12345 (Detached)" or similar
|
// 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;
|
let match;
|
||||||
while ((match = screenPattern.exec(output)) !== null) {
|
while ((match = SCREEN_PATTERN.exec(output)) !== null) {
|
||||||
const pid = parseInt(match[1], 10);
|
const pid = parseInt(match[1], 10);
|
||||||
const screenName = match[2];
|
const screenName = match[2];
|
||||||
const sessionIdFragment = match[3];
|
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[]> {
|
async getScreensWithStats(): Promise<ScreenSessionWithStats[]> {
|
||||||
const screens = Array.from(this.screens.values());
|
const screens = Array.from(this.screens.values());
|
||||||
|
if (screens.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch all stats in parallel
|
// Batch all PIDs into a single ps call for better performance
|
||||||
const statsPromises = screens.map(screen => this.getProcessStats(screen.sessionId));
|
const pids = screens.map(s => s.pid);
|
||||||
const allStats = await Promise.all(statsPromises);
|
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
|
// Combine screens with their stats
|
||||||
return screens.map((screen, i) => ({
|
return screens.map(screen => ({
|
||||||
...screen,
|
...screen,
|
||||||
stats: allStats[i] || undefined
|
stats: statsMap.get(screen.pid) || undefined
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user