mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
perf: add event debouncing to InnerLoopTracker
- Add EVENT_DEBOUNCE_MS constant (50ms) for batching rapid updates - Add debounce timers and pending flags for todo/loop updates - Add emitTodoUpdateDebounced() and emitLoopUpdateDebounced() methods - Add flushPendingEvents() for testing/immediate sync needs - Update reset()/fullReset() to clear debounce timers - Replace rapid-fire emit calls with debounced versions - Reduces UI jitter from rapid consecutive updates - Tests updated to use flushPendingEvents() for synchronous testing Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
+119
-5
@@ -42,6 +42,13 @@ const TODO_EXPIRY_MS = 60 * 60 * 1000;
|
||||
*/
|
||||
const CLEANUP_THROTTLE_MS = 30 * 1000;
|
||||
|
||||
/**
|
||||
* Debounce interval for event emissions (milliseconds).
|
||||
* Prevents UI jitter from rapid consecutive updates.
|
||||
* Default: 50ms
|
||||
*/
|
||||
const EVENT_DEBOUNCE_MS = 50;
|
||||
|
||||
// ========== Pre-compiled Regex Patterns ==========
|
||||
// Pre-compiled for performance (avoid re-compilation on each call)
|
||||
|
||||
@@ -266,6 +273,18 @@ export class InnerLoopTracker extends EventEmitter {
|
||||
/** Timestamp of last cleanup check for throttling */
|
||||
private _lastCleanupTime: number = 0;
|
||||
|
||||
/** Debounce timer for todoUpdate events */
|
||||
private _todoUpdateTimer: NodeJS.Timeout | null = null;
|
||||
|
||||
/** Debounce timer for loopUpdate events */
|
||||
private _loopUpdateTimer: NodeJS.Timeout | null = null;
|
||||
|
||||
/** Flag indicating pending todoUpdate emission */
|
||||
private _todoUpdatePending: boolean = false;
|
||||
|
||||
/** Flag indicating pending loopUpdate emission */
|
||||
private _loopUpdatePending: boolean = false;
|
||||
|
||||
/**
|
||||
* Creates a new InnerLoopTracker instance.
|
||||
* Starts in disabled state until Ralph patterns are detected.
|
||||
@@ -330,12 +349,16 @@ export class InnerLoopTracker extends EventEmitter {
|
||||
* @fires todoUpdate
|
||||
*/
|
||||
reset(): void {
|
||||
// Clear debounce timers
|
||||
this.clearDebounceTimers();
|
||||
|
||||
const wasEnabled = this._loopState.enabled;
|
||||
this._loopState = createInitialInnerLoopState();
|
||||
this._loopState.enabled = wasEnabled; // Keep enabled status
|
||||
this._todos.clear();
|
||||
this._completionPhraseCount.clear();
|
||||
this._lineBuffer = '';
|
||||
// Emit immediately on reset (no debounce)
|
||||
this.emit('loopUpdate', this.loopState);
|
||||
this.emit('todoUpdate', this.todos);
|
||||
}
|
||||
@@ -348,14 +371,100 @@ export class InnerLoopTracker extends EventEmitter {
|
||||
* @fires todoUpdate
|
||||
*/
|
||||
fullReset(): void {
|
||||
// Clear debounce timers
|
||||
this.clearDebounceTimers();
|
||||
|
||||
this._loopState = createInitialInnerLoopState();
|
||||
this._todos.clear();
|
||||
this._completionPhraseCount.clear();
|
||||
this._lineBuffer = '';
|
||||
// Emit immediately on reset (no debounce)
|
||||
this.emit('loopUpdate', this.loopState);
|
||||
this.emit('todoUpdate', this.todos);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all debounce timers.
|
||||
* Called during reset/fullReset to prevent stale emissions.
|
||||
*/
|
||||
private clearDebounceTimers(): void {
|
||||
if (this._todoUpdateTimer) {
|
||||
clearTimeout(this._todoUpdateTimer);
|
||||
this._todoUpdateTimer = null;
|
||||
}
|
||||
if (this._loopUpdateTimer) {
|
||||
clearTimeout(this._loopUpdateTimer);
|
||||
this._loopUpdateTimer = null;
|
||||
}
|
||||
this._todoUpdatePending = false;
|
||||
this._loopUpdatePending = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit todoUpdate event with debouncing.
|
||||
* Batches rapid consecutive calls to reduce UI jitter.
|
||||
* The event fires after EVENT_DEBOUNCE_MS of inactivity.
|
||||
*/
|
||||
private emitTodoUpdateDebounced(): void {
|
||||
this._todoUpdatePending = true;
|
||||
|
||||
if (this._todoUpdateTimer) {
|
||||
clearTimeout(this._todoUpdateTimer);
|
||||
}
|
||||
|
||||
this._todoUpdateTimer = setTimeout(() => {
|
||||
if (this._todoUpdatePending) {
|
||||
this._todoUpdatePending = false;
|
||||
this._todoUpdateTimer = null;
|
||||
this.emit('todoUpdate', this.todos);
|
||||
}
|
||||
}, EVENT_DEBOUNCE_MS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit loopUpdate event with debouncing.
|
||||
* Batches rapid consecutive calls to reduce UI jitter.
|
||||
* The event fires after EVENT_DEBOUNCE_MS of inactivity.
|
||||
*/
|
||||
private emitLoopUpdateDebounced(): void {
|
||||
this._loopUpdatePending = true;
|
||||
|
||||
if (this._loopUpdateTimer) {
|
||||
clearTimeout(this._loopUpdateTimer);
|
||||
}
|
||||
|
||||
this._loopUpdateTimer = setTimeout(() => {
|
||||
if (this._loopUpdatePending) {
|
||||
this._loopUpdatePending = false;
|
||||
this._loopUpdateTimer = null;
|
||||
this.emit('loopUpdate', this.loopState);
|
||||
}
|
||||
}, EVENT_DEBOUNCE_MS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush all pending debounced events immediately.
|
||||
* Useful for testing or when immediate state sync is needed.
|
||||
*/
|
||||
flushPendingEvents(): void {
|
||||
if (this._todoUpdatePending) {
|
||||
this._todoUpdatePending = false;
|
||||
if (this._todoUpdateTimer) {
|
||||
clearTimeout(this._todoUpdateTimer);
|
||||
this._todoUpdateTimer = null;
|
||||
}
|
||||
this.emit('todoUpdate', this.todos);
|
||||
}
|
||||
if (this._loopUpdatePending) {
|
||||
this._loopUpdatePending = false;
|
||||
if (this._loopUpdateTimer) {
|
||||
clearTimeout(this._loopUpdateTimer);
|
||||
this._loopUpdateTimer = null;
|
||||
}
|
||||
this.emit('loopUpdate', this.loopState);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a copy of the current loop state.
|
||||
* @returns Shallow copy of loop state (safe to modify)
|
||||
@@ -806,7 +915,8 @@ export class InnerLoopTracker extends EventEmitter {
|
||||
if (!isNaN(maxIter) && maxIter > 0) {
|
||||
this._loopState.maxIterations = maxIter;
|
||||
this._loopState.lastActivity = Date.now();
|
||||
this.emit('loopUpdate', this.loopState);
|
||||
// Use debounced emit for settings changes
|
||||
this.emitLoopUpdateDebounced();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -824,7 +934,8 @@ export class InnerLoopTracker extends EventEmitter {
|
||||
this._loopState.maxIterations = maxIter;
|
||||
}
|
||||
this._loopState.lastActivity = Date.now();
|
||||
this.emit('loopUpdate', this.loopState);
|
||||
// Use debounced emit for rapid iteration updates
|
||||
this.emitLoopUpdateDebounced();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -833,7 +944,8 @@ export class InnerLoopTracker extends EventEmitter {
|
||||
if (elapsedMatch) {
|
||||
this._loopState.elapsedHours = parseFloat(elapsedMatch[1]);
|
||||
this._loopState.lastActivity = Date.now();
|
||||
this.emit('loopUpdate', this.loopState);
|
||||
// Use debounced emit for elapsed time updates
|
||||
this.emitLoopUpdateDebounced();
|
||||
}
|
||||
|
||||
// Check for cycle count (legacy pattern)
|
||||
@@ -843,7 +955,8 @@ export class InnerLoopTracker extends EventEmitter {
|
||||
if (!isNaN(cycleNum) && cycleNum > this._loopState.cycleCount) {
|
||||
this._loopState.cycleCount = cycleNum;
|
||||
this._loopState.lastActivity = Date.now();
|
||||
this.emit('loopUpdate', this.loopState);
|
||||
// Use debounced emit for cycle updates
|
||||
this.emitLoopUpdateDebounced();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -929,7 +1042,8 @@ export class InnerLoopTracker extends EventEmitter {
|
||||
}
|
||||
|
||||
if (updated) {
|
||||
this.emit('todoUpdate', this.todos);
|
||||
// Use debounced emit to batch rapid todo updates and reduce UI jitter
|
||||
this.emitTodoUpdateDebounced();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -205,6 +205,7 @@ describe('InnerLoopTracker', () => {
|
||||
tracker.on('todoUpdate', todoHandler);
|
||||
|
||||
tracker.processTerminalData('- [ ] First task\n');
|
||||
tracker.flushPendingEvents(); // Flush debounced events
|
||||
|
||||
expect(todoHandler).toHaveBeenCalled();
|
||||
const todos = tracker.todos;
|
||||
@@ -380,7 +381,9 @@ describe('InnerLoopTracker', () => {
|
||||
tracker.on('todoUpdate', todoHandler);
|
||||
|
||||
tracker.processTerminalData('- [ ] Task 1\n');
|
||||
tracker.flushPendingEvents(); // Flush debounced events
|
||||
tracker.processTerminalData('- [ ] Task 2\n');
|
||||
tracker.flushPendingEvents(); // Flush debounced events
|
||||
|
||||
expect(todoHandler).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user