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:
@@ -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 = [
|
||||
|
||||
@@ -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
@@ -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';
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user