Skip to content

Commit 34b81fa

Browse files
committed
fix: reduce CPU load from file watchers and improve performance
- Increased debounce delays to reduce frequency of file system events - Fixed MCP Hub file watcher cleanup to prevent duplicate watchers - Optimized ripgrep process handling with early termination - Improved timeout handling in list-files scanning - Updated tests to match new debounce timings Fixes #7595
1 parent 2e59347 commit 34b81fa

File tree

5 files changed

+50
-38
lines changed

5 files changed

+50
-38
lines changed

src/integrations/workspace/WorkspaceTracker.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ class WorkspaceTracker {
106106
this.prevWorkSpacePath = this.cwd
107107
this.initializeFilePaths()
108108
}
109-
}, 300) // Debounce for 300ms
109+
}, 500) // Increased debounce to 500ms to reduce CPU load
110110
}
111111

112112
private workspaceDidUpdate() {
@@ -125,7 +125,7 @@ class WorkspaceTracker {
125125
openedTabs: this.getOpenedTabsInfo(),
126126
})
127127
this.updateTimer = null
128-
}, 300) // Debounce for 300ms
128+
}, 500) // Increased debounce to 500ms to reduce CPU load
129129
}
130130

131131
private normalizeFilePath(filePath: string): string {

src/integrations/workspace/__tests__/WorkspaceTracker.spec.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -225,8 +225,8 @@ describe("WorkspaceTracker", () => {
225225
// Simulate tab change event
226226
await registeredTabChangeCallback!()
227227

228-
// Run the debounce timer for workspaceDidReset
229-
vitest.advanceTimersByTime(300)
228+
// Run the debounce timer for workspaceDidReset (now 500ms)
229+
vitest.advanceTimersByTime(500)
230230

231231
// Should clear file paths and reset workspace
232232
expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({
@@ -314,8 +314,8 @@ describe("WorkspaceTracker", () => {
314314
// Call again before timer completes
315315
await registeredTabChangeCallback!()
316316

317-
// Advance timer
318-
vitest.advanceTimersByTime(300)
317+
// Advance timer (now 500ms)
318+
vitest.advanceTimersByTime(500)
319319

320320
// Should only have one call to postMessageToWebview
321321
expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({
@@ -338,8 +338,8 @@ describe("WorkspaceTracker", () => {
338338
// Dispose before timer completes
339339
workspaceTracker.dispose()
340340

341-
// Advance timer
342-
vitest.advanceTimersByTime(300)
341+
// Advance timer (now 500ms)
342+
vitest.advanceTimersByTime(500)
343343

344344
// Should have called dispose on all disposables
345345
expect(mockDispose).toHaveBeenCalled()

src/services/code-index/processors/file-watcher.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export class FileWatcher implements IFileWatcher {
3737
private ignoreController: RooIgnoreController
3838
private accumulatedEvents: Map<string, { uri: vscode.Uri; type: "create" | "change" | "delete" }> = new Map()
3939
private batchProcessDebounceTimer?: NodeJS.Timeout
40-
private readonly BATCH_DEBOUNCE_DELAY_MS = 500
40+
private readonly BATCH_DEBOUNCE_DELAY_MS = 1000 // Increased from 500ms to reduce CPU load
4141
private readonly FILE_PROCESSING_CONCURRENCY_LIMIT = 10
4242
private readonly batchSegmentThreshold: number
4343

src/services/glob/list-files.ts

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -626,52 +626,70 @@ async function execRipgrep(rgPath: string, args: string[], limit: number): Promi
626626
const rgProcess = childProcess.spawn(rgPath, args)
627627
let output = ""
628628
let results: string[] = []
629+
let processKilled = false
629630

630-
// Set timeout to avoid hanging
631+
// Set timeout to avoid hanging - increased timeout for slower systems
631632
const timeoutId = setTimeout(() => {
632-
rgProcess.kill()
633-
console.warn("ripgrep timed out, returning partial results")
634-
resolve(results.slice(0, limit))
635-
}, 10_000)
633+
if (!processKilled) {
634+
processKilled = true
635+
rgProcess.kill("SIGTERM")
636+
console.warn("ripgrep timed out, returning partial results")
637+
resolve(results.slice(0, limit))
638+
}
639+
}, 15_000) // Increased timeout to 15 seconds
636640

637641
// Process stdout data as it comes in
638642
rgProcess.stdout.on("data", (data) => {
643+
// Early exit if we've already killed the process
644+
if (processKilled) return
645+
639646
output += data.toString()
640647
processRipgrepOutput()
641648

642649
// Kill the process if we've reached the limit
643-
if (results.length >= limit) {
644-
rgProcess.kill()
645-
clearTimeout(timeoutId) // Clear the timeout when we kill the process due to reaching the limit
650+
if (results.length >= limit && !processKilled) {
651+
processKilled = true
652+
clearTimeout(timeoutId)
653+
rgProcess.kill("SIGTERM")
654+
// Resolve immediately when limit is reached
655+
resolve(results.slice(0, limit))
646656
}
647657
})
648658

649659
// Process stderr but don't fail on non-zero exit codes
650660
rgProcess.stderr.on("data", (data) => {
651-
console.error(`ripgrep stderr: ${data}`)
661+
// Only log errors if not killed intentionally
662+
if (!processKilled) {
663+
console.error(`ripgrep stderr: ${data}`)
664+
}
652665
})
653666

654667
// Handle process completion
655668
rgProcess.on("close", (code) => {
656669
// Clear the timeout to avoid memory leaks
657670
clearTimeout(timeoutId)
658671

659-
// Process any remaining output
660-
processRipgrepOutput(true)
672+
// Only process if not already resolved
673+
if (!processKilled) {
674+
// Process any remaining output
675+
processRipgrepOutput(true)
661676

662-
// Log non-zero exit codes but don't fail
663-
if (code !== 0 && code !== null && code !== 143 /* SIGTERM */) {
664-
console.warn(`ripgrep process exited with code ${code}, returning partial results`)
665-
}
677+
// Log non-zero exit codes but don't fail
678+
if (code !== 0 && code !== null && code !== 143 /* SIGTERM */) {
679+
console.warn(`ripgrep process exited with code ${code}, returning partial results`)
680+
}
666681

667-
resolve(results.slice(0, limit))
682+
resolve(results.slice(0, limit))
683+
}
668684
})
669685

670686
// Handle process errors
671687
rgProcess.on("error", (error) => {
672688
// Clear the timeout to avoid memory leaks
673689
clearTimeout(timeoutId)
674-
reject(new Error(`ripgrep process error: ${error.message}`))
690+
if (!processKilled) {
691+
reject(new Error(`ripgrep process error: ${error.message}`))
692+
}
675693
})
676694

677695
// Helper function to process output buffer

src/services/mcp/McpHub.ts

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -286,11 +286,11 @@ export class McpHub {
286286
clearTimeout(existingTimer)
287287
}
288288

289-
// Set new timer
289+
// Set new timer with longer debounce to reduce CPU usage
290290
const timer = setTimeout(async () => {
291291
this.configChangeDebounceTimers.delete(key)
292292
await this.handleConfigFileChange(filePath, source)
293-
}, 500) // 500ms debounce
293+
}, 1000) // Increased to 1000ms debounce to reduce CPU load
294294

295295
this.configChangeDebounceTimers.set(key, timer)
296296
}
@@ -1033,15 +1033,14 @@ export class McpHub {
10331033
if (manageConnectingState) {
10341034
this.isConnecting = true
10351035
}
1036-
this.removeAllFileWatchers()
10371036
// Filter connections by source
10381037
const currentConnections = this.connections.filter(
10391038
(conn) => conn.server.source === source || (!conn.server.source && source === "global"),
10401039
)
10411040
const currentNames = new Set(currentConnections.map((conn) => conn.server.name))
10421041
const newNames = new Set(Object.keys(newServers))
10431042

1044-
// Delete removed servers
1043+
// Delete removed servers (this will also clean up their file watchers)
10451044
for (const name of currentNames) {
10461045
if (!newNames.has(name)) {
10471046
await this.deleteConnection(name, source)
@@ -1065,22 +1064,17 @@ export class McpHub {
10651064
if (!currentConnection) {
10661065
// New server
10671066
try {
1068-
// Only setup file watcher for enabled servers
1069-
if (!validatedConfig.disabled) {
1070-
this.setupFileWatcher(name, validatedConfig, source)
1071-
}
1067+
// connectToServer will handle file watcher setup for enabled servers
10721068
await this.connectToServer(name, validatedConfig, source)
10731069
} catch (error) {
10741070
this.showErrorMessage(`Failed to connect to new MCP server ${name}`, error)
10751071
}
10761072
} else if (!deepEqual(JSON.parse(currentConnection.server.config), config)) {
10771073
// Existing server with changed config
10781074
try {
1079-
// Only setup file watcher for enabled servers
1080-
if (!validatedConfig.disabled) {
1081-
this.setupFileWatcher(name, validatedConfig, source)
1082-
}
1075+
// Delete connection first (this cleans up old file watchers)
10831076
await this.deleteConnection(name, source)
1077+
// connectToServer will handle file watcher setup for enabled servers
10841078
await this.connectToServer(name, validatedConfig, source)
10851079
} catch (error) {
10861080
this.showErrorMessage(`Failed to reconnect MCP server ${name}`, error)

0 commit comments

Comments
 (0)