mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-05 15:09:42 +02:00
perf: implement phase 1-3 performance optimizations
Add implementation plans and code structure analysis for a 3-phase performance optimization effort. Refactor core modules to reduce timer overhead, consolidate regex usage, extract exec timeout config, add debouncer utility, and streamline server/schema validation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -506,3 +506,42 @@ export const RalphLoopStartSchema = z.object({
|
||||
)
|
||||
.optional(),
|
||||
});
|
||||
|
||||
// ========== Inferred Types ==========
|
||||
|
||||
export type CreateSessionInput = z.infer<typeof CreateSessionSchema>;
|
||||
export type RunPromptInput = z.infer<typeof RunPromptSchema>;
|
||||
export type ResizeInput = z.infer<typeof ResizeSchema>;
|
||||
export type CreateCaseInput = z.infer<typeof CreateCaseSchema>;
|
||||
export type QuickStartInput = z.infer<typeof QuickStartSchema>;
|
||||
export type HookEventInput = z.infer<typeof HookEventSchema>;
|
||||
export type RespawnConfigInput = z.infer<typeof RespawnConfigSchema>;
|
||||
export type ConfigUpdateInput = z.infer<typeof ConfigUpdateSchema>;
|
||||
export type SettingsUpdateInput = z.infer<typeof SettingsUpdateSchema>;
|
||||
export type SessionInputWithLimitInput = z.infer<typeof SessionInputWithLimitSchema>;
|
||||
export type SessionNameInput = z.infer<typeof SessionNameSchema>;
|
||||
export type SessionColorInput = z.infer<typeof SessionColorSchema>;
|
||||
export type RalphConfigInput = z.infer<typeof RalphConfigSchema>;
|
||||
export type FixPlanImportInput = z.infer<typeof FixPlanImportSchema>;
|
||||
export type RalphPromptWriteInput = z.infer<typeof RalphPromptWriteSchema>;
|
||||
export type AutoClearInput = z.infer<typeof AutoClearSchema>;
|
||||
export type AutoCompactInput = z.infer<typeof AutoCompactSchema>;
|
||||
export type ImageWatcherInput = z.infer<typeof ImageWatcherSchema>;
|
||||
export type FlickerFilterInput = z.infer<typeof FlickerFilterSchema>;
|
||||
export type QuickRunInput = z.infer<typeof QuickRunSchema>;
|
||||
export type ScheduledRunInput = z.infer<typeof ScheduledRunSchema>;
|
||||
export type LinkCaseInput = z.infer<typeof LinkCaseSchema>;
|
||||
export type GeneratePlanInput = z.infer<typeof GeneratePlanSchema>;
|
||||
export type GeneratePlanDetailedInput = z.infer<typeof GeneratePlanDetailedSchema>;
|
||||
export type CancelPlanInput = z.infer<typeof CancelPlanSchema>;
|
||||
export type PlanTaskUpdateInput = z.infer<typeof PlanTaskUpdateSchema>;
|
||||
export type PlanTaskAddInput = z.infer<typeof PlanTaskAddSchema>;
|
||||
export type CpuLimitInput = z.infer<typeof CpuLimitSchema>;
|
||||
export type ModelConfigUpdateInput = z.infer<typeof ModelConfigUpdateSchema>;
|
||||
export type SubagentWindowStatesInput = z.infer<typeof SubagentWindowStatesSchema>;
|
||||
export type SubagentParentMapInput = z.infer<typeof SubagentParentMapSchema>;
|
||||
export type InteractiveRespawnInput = z.infer<typeof InteractiveRespawnSchema>;
|
||||
export type RespawnEnableInput = z.infer<typeof RespawnEnableSchema>;
|
||||
export type PushSubscribeInput = z.infer<typeof PushSubscribeSchema>;
|
||||
export type PushPreferencesUpdateInput = z.infer<typeof PushPreferencesUpdateSchema>;
|
||||
export type RalphLoopStartInput = z.infer<typeof RalphLoopStartSchema>;
|
||||
|
||||
+53
-79
@@ -123,7 +123,7 @@ import {
|
||||
RalphLoopStartSchema,
|
||||
isValidWorkingDir,
|
||||
} from './schemas.js';
|
||||
import { StaleExpirationMap } from '../utils/index.js';
|
||||
import { CleanupManager, KeyedDebouncer, StaleExpirationMap } from '../utils/index.js';
|
||||
import { MAX_CONCURRENT_SESSIONS, MAX_SSE_CLIENTS } from '../config/map-limits.js';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
@@ -420,16 +420,14 @@ export class WebServer extends EventEmitter {
|
||||
ttlMs: 5 * 60 * 1000, // 5 minutes - auto-expire stale session timing data
|
||||
refreshOnGet: false, // Don't refresh on reads, only on explicit sets
|
||||
});
|
||||
// Scheduled runs cleanup timer
|
||||
private scheduledCleanupTimer: NodeJS.Timeout | null = null;
|
||||
// Centralized cleanup for standalone timers (intervals + resettable timeouts)
|
||||
private cleanup = new CleanupManager();
|
||||
// SSE event batching
|
||||
private taskUpdateBatches: Map<string, { sessionId: string; task: BackgroundTask }> = new Map();
|
||||
private taskUpdateBatchTimer: NodeJS.Timeout | null = null;
|
||||
private taskUpdateBatchTimerId: string | null = null;
|
||||
// State update batching (reduce expensive toDetailedState() serialization)
|
||||
private stateUpdatePending: Set<string> = new Set();
|
||||
private stateUpdateTimer: NodeJS.Timeout | null = null;
|
||||
// SSE client health check timer
|
||||
private sseHealthCheckTimer: NodeJS.Timeout | null = null;
|
||||
private stateUpdateTimerId: string | null = null;
|
||||
// Flag to prevent new timers during shutdown
|
||||
private _isStopping: boolean = false;
|
||||
// Cached light state for SSE init (avoids rebuilding on every reconnect)
|
||||
@@ -439,14 +437,13 @@ export class WebServer extends EventEmitter {
|
||||
private cachedSessionsList: { data: unknown[]; timestamp: number } | null = null;
|
||||
// Token recording for daily stats (track what's been recorded to avoid double-counting)
|
||||
private lastRecordedTokens: Map<string, { input: number; output: number }> = new Map();
|
||||
private tokenRecordingTimer: NodeJS.Timeout | null = null;
|
||||
// Server startup time for respawn grace period calculation
|
||||
private readonly serverStartTime: number = Date.now();
|
||||
// Pending respawn start timers (for cleanup on shutdown)
|
||||
private pendingRespawnStarts: Map<string, NodeJS.Timeout> = new Map();
|
||||
// Active plan orchestrators (for cancellation via API)
|
||||
private activePlanOrchestrators: Map<string, PlanOrchestrator> = new Map();
|
||||
private persistDebounceTimers: Map<string, ReturnType<typeof setTimeout>> = new Map();
|
||||
private persistDeb = new KeyedDebouncer(100);
|
||||
// Grace period before starting restored respawn controllers (2 minutes)
|
||||
private static readonly RESPAWN_RESTORE_GRACE_PERIOD_MS = 2 * 60 * 1000;
|
||||
// Stored listener handlers for cleanup
|
||||
@@ -4646,18 +4643,12 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
|
||||
|
||||
/** Debounced wrapper — coalesces rapid persistSessionState calls per session */
|
||||
private persistSessionState(session: Session): void {
|
||||
const existing = this.persistDebounceTimers.get(session.id);
|
||||
if (existing) clearTimeout(existing);
|
||||
this.persistDebounceTimers.set(
|
||||
session.id,
|
||||
setTimeout(() => {
|
||||
this.persistDebounceTimers.delete(session.id);
|
||||
// Session may have been removed during debounce
|
||||
if (this.sessions.has(session.id)) {
|
||||
this._persistSessionStateNow(session);
|
||||
}
|
||||
}, 100)
|
||||
);
|
||||
this.persistDeb.schedule(session.id, () => {
|
||||
// Session may have been removed during debounce
|
||||
if (this.sessions.has(session.id)) {
|
||||
this._persistSessionStateNow(session);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Persists full session state including respawn config to state.json */
|
||||
@@ -4845,11 +4836,7 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
|
||||
}
|
||||
|
||||
// Clear pending persist-debounce timer (prevents stale closure holding session ref)
|
||||
const pendingPersist = this.persistDebounceTimers.get(sessionId);
|
||||
if (pendingPersist) {
|
||||
clearTimeout(pendingPersist);
|
||||
this.persistDebounceTimers.delete(sessionId);
|
||||
}
|
||||
this.persistDeb.cancelKey(sessionId);
|
||||
|
||||
// Clear batches, per-session timers, and pending state updates
|
||||
this.terminalBatches.delete(sessionId);
|
||||
@@ -5079,11 +5066,7 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
|
||||
this.lastTerminalEventTime.delete(session.id);
|
||||
|
||||
// Clear pending persist-debounce timer
|
||||
const pendingPersist = this.persistDebounceTimers.get(session.id);
|
||||
if (pendingPersist) {
|
||||
clearTimeout(pendingPersist);
|
||||
this.persistDebounceTimers.delete(session.id);
|
||||
}
|
||||
this.persistDeb.cancelKey(session.id);
|
||||
|
||||
// Close any active file streams
|
||||
fileStreamManager.closeSessionStreams(session.id);
|
||||
@@ -6000,11 +5983,15 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
|
||||
const key = `${sessionId}:${task.id}`;
|
||||
this.taskUpdateBatches.set(key, { sessionId, task });
|
||||
|
||||
if (!this.taskUpdateBatchTimer) {
|
||||
this.taskUpdateBatchTimer = setTimeout(() => {
|
||||
this.flushTaskUpdateBatches();
|
||||
this.taskUpdateBatchTimer = null;
|
||||
}, TASK_UPDATE_BATCH_INTERVAL);
|
||||
if (!this.taskUpdateBatchTimerId) {
|
||||
this.taskUpdateBatchTimerId = this.cleanup.setTimeout(
|
||||
() => {
|
||||
this.taskUpdateBatchTimerId = null;
|
||||
this.flushTaskUpdateBatches();
|
||||
},
|
||||
TASK_UPDATE_BATCH_INTERVAL,
|
||||
{ description: 'task update batch flush' }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6031,11 +6018,15 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
|
||||
|
||||
this.stateUpdatePending.add(sessionId);
|
||||
|
||||
if (!this.stateUpdateTimer) {
|
||||
this.stateUpdateTimer = setTimeout(() => {
|
||||
this.flushStateUpdates();
|
||||
this.stateUpdateTimer = null;
|
||||
}, STATE_UPDATE_DEBOUNCE_INTERVAL);
|
||||
if (!this.stateUpdateTimerId) {
|
||||
this.stateUpdateTimerId = this.cleanup.setTimeout(
|
||||
() => {
|
||||
this.stateUpdateTimerId = null;
|
||||
this.flushStateUpdates();
|
||||
},
|
||||
STATE_UPDATE_DEBOUNCE_INTERVAL,
|
||||
{ description: 'state update debounce flush' }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6226,21 +6217,30 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
|
||||
process.env.CODEMAN_API_URL = `${protocol}://localhost:${this.port}`;
|
||||
|
||||
// Start scheduled runs cleanup timer
|
||||
this.scheduledCleanupTimer = setInterval(() => {
|
||||
this.cleanupScheduledRuns();
|
||||
}, SCHEDULED_CLEANUP_INTERVAL);
|
||||
this.cleanup.setInterval(
|
||||
() => {
|
||||
this.cleanupScheduledRuns();
|
||||
},
|
||||
SCHEDULED_CLEANUP_INTERVAL,
|
||||
{ description: 'scheduled runs cleanup' }
|
||||
);
|
||||
|
||||
// Start SSE client health check timer (prevents memory leaks from dead connections)
|
||||
this.sseHealthCheckTimer = setInterval(() => {
|
||||
this.cleanupDeadSSEClients();
|
||||
}, SSE_HEALTH_CHECK_INTERVAL);
|
||||
this.cleanup.setInterval(
|
||||
() => {
|
||||
this.cleanupDeadSSEClients();
|
||||
},
|
||||
SSE_HEALTH_CHECK_INTERVAL,
|
||||
{ description: 'SSE client health check' }
|
||||
);
|
||||
|
||||
// Start token recording timer (every 5 minutes for long-running sessions)
|
||||
this.tokenRecordingTimer = setInterval(
|
||||
this.cleanup.setInterval(
|
||||
() => {
|
||||
this.recordPeriodicTokenUsage();
|
||||
},
|
||||
5 * 60 * 1000
|
||||
5 * 60 * 1000,
|
||||
{ description: 'periodic token recording' }
|
||||
);
|
||||
|
||||
// Start subagent watcher for Claude Code background agent visibility (if enabled)
|
||||
@@ -6531,11 +6531,8 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
|
||||
// Set stopping flag to prevent new timer creation during shutdown
|
||||
this._isStopping = true;
|
||||
|
||||
// Clear SSE health check timer
|
||||
if (this.sseHealthCheckTimer) {
|
||||
clearInterval(this.sseHealthCheckTimer);
|
||||
this.sseHealthCheckTimer = null;
|
||||
}
|
||||
// Dispose all managed timers (intervals + resettable timeouts)
|
||||
this.cleanup.dispose();
|
||||
|
||||
// Gracefully close all SSE connections before clearing
|
||||
for (const client of this.sseClients) {
|
||||
@@ -6558,43 +6555,20 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
|
||||
this.terminalBatches.clear();
|
||||
this.terminalBatchSizes.clear();
|
||||
|
||||
if (this.taskUpdateBatchTimer) {
|
||||
clearTimeout(this.taskUpdateBatchTimer);
|
||||
this.taskUpdateBatchTimer = null;
|
||||
}
|
||||
this.taskUpdateBatches.clear();
|
||||
|
||||
if (this.stateUpdateTimer) {
|
||||
clearTimeout(this.stateUpdateTimer);
|
||||
this.stateUpdateTimer = null;
|
||||
}
|
||||
this.stateUpdatePending.clear();
|
||||
|
||||
// Clear token recording timer
|
||||
if (this.tokenRecordingTimer) {
|
||||
clearInterval(this.tokenRecordingTimer);
|
||||
this.tokenRecordingTimer = null;
|
||||
}
|
||||
this.lastRecordedTokens.clear();
|
||||
|
||||
// Clear scheduled cleanup timer
|
||||
if (this.scheduledCleanupTimer) {
|
||||
clearInterval(this.scheduledCleanupTimer);
|
||||
this.scheduledCleanupTimer = null;
|
||||
}
|
||||
|
||||
// Stop multiplexer and flush pending saves
|
||||
this.mux.destroy();
|
||||
|
||||
// Flush any pending persist-debounce timers and persist dirty sessions
|
||||
for (const [sessionId, timer] of this.persistDebounceTimers) {
|
||||
clearTimeout(timer);
|
||||
this.persistDeb.flushAll((sessionId) => {
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (session) {
|
||||
this._persistSessionStateNow(session);
|
||||
}
|
||||
}
|
||||
this.persistDebounceTimers.clear();
|
||||
});
|
||||
|
||||
// Clear cached state
|
||||
this.cachedLightState = null;
|
||||
|
||||
Reference in New Issue
Block a user