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:
arkon
2026-02-28 17:39:44 +01:00
co-authored by Claude Opus 4.6
parent 562b14ab61
commit e0a2774d37
26 changed files with 4443 additions and 624 deletions
+1 -3
View File
@@ -12,9 +12,7 @@ import { execSync } from 'node:child_process';
import { existsSync } from 'node:fs';
import { delimiter, dirname, join } from 'node:path';
import { homedir } from 'node:os';
/** Timeout for exec commands (5 seconds) */
const EXEC_TIMEOUT_MS = 5000;
import { EXEC_TIMEOUT_MS } from '../config/exec-timeout.js';
/** Common directories where the Claude CLI binary may be installed */
const CLAUDE_SEARCH_DIRS = [
+174
View File
@@ -0,0 +1,174 @@
/**
* @fileoverview Debounce utilities to replace manual timer management.
*
* Two variants:
* - `Debouncer` — single debounced operation (replaces timer + clearTimeout pattern)
* - `KeyedDebouncer` — per-key debouncing (replaces Map<string, Timeout> pattern)
*
* Both integrate with CleanupManager via dispose().
*
* @module utils/debouncer
*/
/**
* Single-operation debouncer.
*
* Replaces the common pattern of:
* ```
* private timer: NodeJS.Timeout | null = null;
* debounce(fn) { if (this.timer) clearTimeout(this.timer); this.timer = setTimeout(fn, delay); }
* cancel() { if (this.timer) { clearTimeout(this.timer); this.timer = null; } }
* ```
*
* @example
* ```typescript
* private saveDeb = new Debouncer(500);
*
* onChange() {
* this.saveDeb.schedule(() => this.save());
* }
*
* stop() {
* this.saveDeb.dispose();
* }
* ```
*/
export class Debouncer {
private timer: NodeJS.Timeout | null = null;
constructor(private readonly delayMs: number) {}
/**
* Schedule a debounced callback. Resets the timer on each call.
* If a previous call is pending, it is cancelled.
*/
schedule(fn: () => void): void {
this.cancel();
this.timer = setTimeout(() => {
this.timer = null;
fn();
}, this.delayMs);
}
/** Cancel any pending execution without invoking the callback. */
cancel(): void {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
}
/** Whether a callback is currently pending. */
get isPending(): boolean {
return this.timer !== null;
}
/**
* Cancel pending callback and flush immediately.
* Useful for shutdown: cancel the timer but run the action now.
*
* @param fn - The flush function to run (typically the same function passed to schedule)
*/
flush(fn: () => void): void {
this.cancel();
fn();
}
/** Alias for cancel() — matches CleanupManager/Disposable convention. */
dispose(): void {
this.cancel();
}
}
/**
* Per-key debouncer for operations that need independent timers per resource.
*
* Replaces the common pattern of:
* ```
* private timers = new Map<string, NodeJS.Timeout>();
* debounce(key, fn) {
* const existing = this.timers.get(key);
* if (existing) clearTimeout(existing);
* this.timers.set(key, setTimeout(() => { this.timers.delete(key); fn(); }, delay));
* }
* ```
*
* @example
* ```typescript
* private fileDebouncers = new KeyedDebouncer(100);
*
* onFileChange(path: string) {
* this.fileDebouncers.schedule(path, () => this.processFile(path));
* }
*
* stop() {
* this.fileDebouncers.dispose();
* }
* ```
*/
export class KeyedDebouncer {
private timers = new Map<string, NodeJS.Timeout>();
constructor(private readonly delayMs: number) {}
/**
* Schedule a debounced callback for a specific key.
* Each key has its own independent timer.
*/
schedule(key: string, fn: () => void): void {
this.cancelKey(key);
this.timers.set(
key,
setTimeout(() => {
this.timers.delete(key);
fn();
}, this.delayMs)
);
}
/** Cancel a pending callback for a specific key. */
cancelKey(key: string): void {
const existing = this.timers.get(key);
if (existing) {
clearTimeout(existing);
this.timers.delete(key);
}
}
/** Whether a callback is pending for a specific key. */
has(key: string): boolean {
return this.timers.has(key);
}
/** Number of active timers. */
get size(): number {
return this.timers.size;
}
/** Get all currently active keys. */
keys(): IterableIterator<string> {
return this.timers.keys();
}
/** Cancel all pending callbacks. */
dispose(): void {
for (const timer of this.timers.values()) {
clearTimeout(timer);
}
this.timers.clear();
}
/**
* Cancel all pending callbacks and run a flush function for each active key.
* Useful for shutdown: cancel timers but run the action for each pending key.
*
* @param fn - Called once per active key with the key as argument
*/
flushAll(fn: (key: string) => void): void {
const activeKeys = Array.from(this.timers.keys());
this.dispose();
for (const key of activeKeys) {
fn(key);
}
}
}
+6 -1
View File
@@ -9,14 +9,19 @@
export { BufferAccumulator } from './buffer-accumulator.js';
export { LRUMap, type LRUMapOptions } from './lru-map.js';
export { CleanupManager, type TimerOptions } from './cleanup-manager.js';
export { Debouncer, KeyedDebouncer } from './debouncer.js';
export { StaleExpirationMap, type StaleExpirationMapOptions } from './stale-expiration-map.js';
export {
ANSI_ESCAPE_PATTERN_FULL,
ANSI_ESCAPE_PATTERN_SIMPLE,
TOKEN_PATTERN,
SPINNER_PATTERN,
createAnsiPatternFull,
createAnsiPatternSimple,
stripAnsi,
SAFE_PATH_PATTERN,
} from './regex-patterns.js';
export { MAX_SESSION_TOKENS } from './token-validation.js';
export { MAX_SESSION_TOKENS, validateTokenCounts, validateTokensAndCost } from './token-validation.js';
export { stringSimilarity, fuzzyPhraseMatch, todoContentHash } from './string-similarity.js';
export { assertNever } from './type-safety.js';
export { wrapWithNice } from './nice-wrapper.js';
+1 -3
View File
@@ -11,9 +11,7 @@ import { execSync } from 'node:child_process';
import { existsSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { homedir } from 'node:os';
/** Timeout for exec commands (5 seconds) */
const EXEC_TIMEOUT_MS = 5000;
import { EXEC_TIMEOUT_MS } from '../config/exec-timeout.js';
/** Common directories where the OpenCode CLI binary may be installed */
const OPENCODE_SEARCH_DIRS = [
+3 -19
View File
@@ -24,7 +24,7 @@
* levenshteinDistance('hello', 'helo') // 1 (one deletion)
* levenshteinDistance('COMPLETE', 'COMPLET') // 1 (one deletion)
*/
export function levenshteinDistance(a: string, b: string): number {
function levenshteinDistance(a: string, b: string): number {
// Ensure a is the shorter string for space efficiency
if (a.length > b.length) {
[a, b] = [b, a];
@@ -91,22 +91,6 @@ export function stringSimilarity(a: string, b: string): number {
return 1 - distance / maxLength;
}
/**
* Check if two strings are similar within a given threshold.
*
* @param a - First string
* @param b - Second string
* @param threshold - Minimum similarity ratio (default: 0.85 = 85% similar)
* @returns True if similarity >= threshold
*
* @example
* isSimilar('COMPLETE', 'COMPLET', 0.85) // true (87.5% similar)
* isSimilar('COMPLETE', 'DONE', 0.85) // false (0% similar)
*/
export function isSimilar(a: string, b: string, threshold = 0.85): boolean {
return stringSimilarity(a, b) >= threshold;
}
/**
* Check if two strings are similar with edit distance tolerance.
* More intuitive for short strings than percentage-based threshold.
@@ -120,7 +104,7 @@ export function isSimilar(a: string, b: string, threshold = 0.85): boolean {
* isSimilarByDistance('COMPLETE', 'COMPLET', 2) // true (distance 1)
* isSimilarByDistance('COMPLETE', 'COMP', 2) // false (distance 4)
*/
export function isSimilarByDistance(a: string, b: string, maxDistance = 2): boolean {
function isSimilarByDistance(a: string, b: string, maxDistance = 2): boolean {
return levenshteinDistance(a, b) <= maxDistance;
}
@@ -136,7 +120,7 @@ export function isSimilarByDistance(a: string, b: string, maxDistance = 2): bool
* normalizePhrase('TASK-DONE') // 'TASKDONE'
* normalizePhrase('Task Done') // 'TASKDONE'
*/
export function normalizePhrase(phrase: string): string {
function normalizePhrase(phrase: string): string {
return phrase
.toUpperCase()
.replace(/[\s_\-.]+/g, '') // Remove whitespace, underscores, hyphens, dots