mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-10-05 02:41:26 +00:00
Merge 605d1c8558 into b867ec9145
This commit is contained in:
commit
bc3caf1797
8 changed files with 309 additions and 7 deletions
|
|
@ -12,6 +12,13 @@ export const CODEBASE_INDEX_DEFAULTS = {
|
||||||
MAX_SEARCH_SCORE: 1,
|
MAX_SEARCH_SCORE: 1,
|
||||||
DEFAULT_SEARCH_MIN_SCORE: 0.4,
|
DEFAULT_SEARCH_MIN_SCORE: 0.4,
|
||||||
SEARCH_SCORE_STEP: 0.05,
|
SEARCH_SCORE_STEP: 0.05,
|
||||||
|
// File watcher performance settings for multi-worktree optimization
|
||||||
|
DEFAULT_FILE_WATCHER_DEBOUNCE_MS: 500,
|
||||||
|
MIN_FILE_WATCHER_DEBOUNCE_MS: 100,
|
||||||
|
MAX_FILE_WATCHER_DEBOUNCE_MS: 5000,
|
||||||
|
DEFAULT_FILE_WATCHER_CONCURRENCY: 10,
|
||||||
|
MIN_FILE_WATCHER_CONCURRENCY: 1,
|
||||||
|
MAX_FILE_WATCHER_CONCURRENCY: 20,
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -50,6 +57,17 @@ export const codebaseIndexConfigSchema = z.object({
|
||||||
codebaseIndexBedrockProfile: z.string().optional(),
|
codebaseIndexBedrockProfile: z.string().optional(),
|
||||||
// OpenRouter specific fields
|
// OpenRouter specific fields
|
||||||
codebaseIndexOpenRouterSpecificProvider: z.string().optional(),
|
codebaseIndexOpenRouterSpecificProvider: z.string().optional(),
|
||||||
|
// File watcher performance settings for multi-worktree optimization
|
||||||
|
codebaseIndexFileWatcherDebounceMs: z
|
||||||
|
.number()
|
||||||
|
.min(CODEBASE_INDEX_DEFAULTS.MIN_FILE_WATCHER_DEBOUNCE_MS)
|
||||||
|
.max(CODEBASE_INDEX_DEFAULTS.MAX_FILE_WATCHER_DEBOUNCE_MS)
|
||||||
|
.optional(),
|
||||||
|
codebaseIndexFileWatcherConcurrency: z
|
||||||
|
.number()
|
||||||
|
.min(CODEBASE_INDEX_DEFAULTS.MIN_FILE_WATCHER_CONCURRENCY)
|
||||||
|
.max(CODEBASE_INDEX_DEFAULTS.MAX_FILE_WATCHER_CONCURRENCY)
|
||||||
|
.optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export type CodebaseIndexConfig = z.infer<typeof codebaseIndexConfigSchema>
|
export type CodebaseIndexConfig = z.infer<typeof codebaseIndexConfigSchema>
|
||||||
|
|
|
||||||
|
|
@ -1018,6 +1018,120 @@ describe("CodeIndexConfigManager", () => {
|
||||||
expect(maxManager.currentSearchMaxResults).toBe(200)
|
expect(maxManager.currentSearchMaxResults).toBe(200)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("currentFileWatcherDebounceMs", () => {
|
||||||
|
it("should return user setting when provided", async () => {
|
||||||
|
mockContextProxy.getGlobalState.mockReturnValue({
|
||||||
|
codebaseIndexEnabled: true,
|
||||||
|
codebaseIndexQdrantUrl: "http://qdrant.local",
|
||||||
|
codebaseIndexEmbedderProvider: "openai",
|
||||||
|
codebaseIndexEmbedderModelId: "text-embedding-3-small",
|
||||||
|
codebaseIndexFileWatcherDebounceMs: 1000, // User setting
|
||||||
|
})
|
||||||
|
|
||||||
|
await configManager.loadConfiguration()
|
||||||
|
expect(configManager.currentFileWatcherDebounceMs).toBe(1000) // User setting
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return default when no user setting", async () => {
|
||||||
|
mockContextProxy.getGlobalState.mockReturnValue({
|
||||||
|
codebaseIndexEnabled: true,
|
||||||
|
codebaseIndexQdrantUrl: "http://qdrant.local",
|
||||||
|
codebaseIndexEmbedderProvider: "openai",
|
||||||
|
codebaseIndexEmbedderModelId: "text-embedding-3-small",
|
||||||
|
// No file watcher debounce setting
|
||||||
|
})
|
||||||
|
|
||||||
|
const newManager = new CodeIndexConfigManager(mockContextProxy)
|
||||||
|
await newManager.loadConfiguration()
|
||||||
|
expect(newManager.currentFileWatcherDebounceMs).toBe(500) // Default (DEFAULT_FILE_WATCHER_DEBOUNCE_MS)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should respect minimum and maximum bounds", async () => {
|
||||||
|
// Test minimum value
|
||||||
|
mockContextProxy.getGlobalState.mockReturnValue({
|
||||||
|
codebaseIndexEnabled: true,
|
||||||
|
codebaseIndexQdrantUrl: "http://qdrant.local",
|
||||||
|
codebaseIndexEmbedderProvider: "openai",
|
||||||
|
codebaseIndexEmbedderModelId: "text-embedding-3-small",
|
||||||
|
codebaseIndexFileWatcherDebounceMs: 100, // Minimum allowed
|
||||||
|
})
|
||||||
|
|
||||||
|
const minManager = new CodeIndexConfigManager(mockContextProxy)
|
||||||
|
await minManager.loadConfiguration()
|
||||||
|
expect(minManager.currentFileWatcherDebounceMs).toBe(100)
|
||||||
|
|
||||||
|
// Test maximum value
|
||||||
|
mockContextProxy.getGlobalState.mockReturnValue({
|
||||||
|
codebaseIndexEnabled: true,
|
||||||
|
codebaseIndexQdrantUrl: "http://qdrant.local",
|
||||||
|
codebaseIndexEmbedderProvider: "openai",
|
||||||
|
codebaseIndexEmbedderModelId: "text-embedding-3-small",
|
||||||
|
codebaseIndexFileWatcherDebounceMs: 5000, // Maximum allowed
|
||||||
|
})
|
||||||
|
|
||||||
|
const maxManager = new CodeIndexConfigManager(mockContextProxy)
|
||||||
|
await maxManager.loadConfiguration()
|
||||||
|
expect(maxManager.currentFileWatcherDebounceMs).toBe(5000)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("currentFileWatcherConcurrency", () => {
|
||||||
|
it("should return user setting when provided", async () => {
|
||||||
|
mockContextProxy.getGlobalState.mockReturnValue({
|
||||||
|
codebaseIndexEnabled: true,
|
||||||
|
codebaseIndexQdrantUrl: "http://qdrant.local",
|
||||||
|
codebaseIndexEmbedderProvider: "openai",
|
||||||
|
codebaseIndexEmbedderModelId: "text-embedding-3-small",
|
||||||
|
codebaseIndexFileWatcherConcurrency: 5, // User setting - lower for multi-worktree
|
||||||
|
})
|
||||||
|
|
||||||
|
await configManager.loadConfiguration()
|
||||||
|
expect(configManager.currentFileWatcherConcurrency).toBe(5) // User setting
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return default when no user setting", async () => {
|
||||||
|
mockContextProxy.getGlobalState.mockReturnValue({
|
||||||
|
codebaseIndexEnabled: true,
|
||||||
|
codebaseIndexQdrantUrl: "http://qdrant.local",
|
||||||
|
codebaseIndexEmbedderProvider: "openai",
|
||||||
|
codebaseIndexEmbedderModelId: "text-embedding-3-small",
|
||||||
|
// No file watcher concurrency setting
|
||||||
|
})
|
||||||
|
|
||||||
|
const newManager = new CodeIndexConfigManager(mockContextProxy)
|
||||||
|
await newManager.loadConfiguration()
|
||||||
|
expect(newManager.currentFileWatcherConcurrency).toBe(10) // Default (DEFAULT_FILE_WATCHER_CONCURRENCY)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should respect minimum and maximum bounds", async () => {
|
||||||
|
// Test minimum value
|
||||||
|
mockContextProxy.getGlobalState.mockReturnValue({
|
||||||
|
codebaseIndexEnabled: true,
|
||||||
|
codebaseIndexQdrantUrl: "http://qdrant.local",
|
||||||
|
codebaseIndexEmbedderProvider: "openai",
|
||||||
|
codebaseIndexEmbedderModelId: "text-embedding-3-small",
|
||||||
|
codebaseIndexFileWatcherConcurrency: 1, // Minimum allowed
|
||||||
|
})
|
||||||
|
|
||||||
|
const minManager = new CodeIndexConfigManager(mockContextProxy)
|
||||||
|
await minManager.loadConfiguration()
|
||||||
|
expect(minManager.currentFileWatcherConcurrency).toBe(1)
|
||||||
|
|
||||||
|
// Test maximum value
|
||||||
|
mockContextProxy.getGlobalState.mockReturnValue({
|
||||||
|
codebaseIndexEnabled: true,
|
||||||
|
codebaseIndexQdrantUrl: "http://qdrant.local",
|
||||||
|
codebaseIndexEmbedderProvider: "openai",
|
||||||
|
codebaseIndexEmbedderModelId: "text-embedding-3-small",
|
||||||
|
codebaseIndexFileWatcherConcurrency: 20, // Maximum allowed
|
||||||
|
})
|
||||||
|
|
||||||
|
const maxManager = new CodeIndexConfigManager(mockContextProxy)
|
||||||
|
await maxManager.loadConfiguration()
|
||||||
|
expect(maxManager.currentFileWatcherConcurrency).toBe(20)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("empty/missing API key handling", () => {
|
describe("empty/missing API key handling", () => {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,12 @@ import { ApiHandlerOptions } from "../../shared/api"
|
||||||
import { ContextProxy } from "../../core/config/ContextProxy"
|
import { ContextProxy } from "../../core/config/ContextProxy"
|
||||||
import { EmbedderProvider } from "./interfaces/manager"
|
import { EmbedderProvider } from "./interfaces/manager"
|
||||||
import { CodeIndexConfig, PreviousConfigSnapshot } from "./interfaces/config"
|
import { CodeIndexConfig, PreviousConfigSnapshot } from "./interfaces/config"
|
||||||
import { DEFAULT_SEARCH_MIN_SCORE, DEFAULT_MAX_SEARCH_RESULTS } from "./constants"
|
import {
|
||||||
|
DEFAULT_SEARCH_MIN_SCORE,
|
||||||
|
DEFAULT_MAX_SEARCH_RESULTS,
|
||||||
|
DEFAULT_FILE_WATCHER_DEBOUNCE_MS,
|
||||||
|
DEFAULT_FILE_WATCHER_CONCURRENCY,
|
||||||
|
} from "./constants"
|
||||||
import { getDefaultModelId, getModelDimension, getModelScoreThreshold } from "../../shared/embeddingModels"
|
import { getDefaultModelId, getModelDimension, getModelScoreThreshold } from "../../shared/embeddingModels"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -26,6 +31,9 @@ export class CodeIndexConfigManager {
|
||||||
private qdrantApiKey?: string
|
private qdrantApiKey?: string
|
||||||
private searchMinScore?: number
|
private searchMinScore?: number
|
||||||
private searchMaxResults?: number
|
private searchMaxResults?: number
|
||||||
|
// File watcher performance settings for multi-worktree optimization
|
||||||
|
private fileWatcherDebounceMs?: number
|
||||||
|
private fileWatcherConcurrency?: number
|
||||||
|
|
||||||
constructor(private readonly contextProxy: ContextProxy) {
|
constructor(private readonly contextProxy: ContextProxy) {
|
||||||
// Initialize with current configuration to avoid false restart triggers
|
// Initialize with current configuration to avoid false restart triggers
|
||||||
|
|
@ -87,6 +95,10 @@ export class CodeIndexConfigManager {
|
||||||
this.searchMinScore = codebaseIndexSearchMinScore
|
this.searchMinScore = codebaseIndexSearchMinScore
|
||||||
this.searchMaxResults = codebaseIndexSearchMaxResults
|
this.searchMaxResults = codebaseIndexSearchMaxResults
|
||||||
|
|
||||||
|
// File watcher performance settings
|
||||||
|
this.fileWatcherDebounceMs = codebaseIndexConfig.codebaseIndexFileWatcherDebounceMs
|
||||||
|
this.fileWatcherConcurrency = codebaseIndexConfig.codebaseIndexFileWatcherConcurrency
|
||||||
|
|
||||||
// Validate and set model dimension
|
// Validate and set model dimension
|
||||||
const rawDimension = codebaseIndexConfig.codebaseIndexEmbedderModelDimension
|
const rawDimension = codebaseIndexConfig.codebaseIndexEmbedderModelDimension
|
||||||
if (rawDimension !== undefined && rawDimension !== null) {
|
if (rawDimension !== undefined && rawDimension !== null) {
|
||||||
|
|
@ -460,6 +472,8 @@ export class CodeIndexConfigManager {
|
||||||
qdrantApiKey: this.qdrantApiKey,
|
qdrantApiKey: this.qdrantApiKey,
|
||||||
searchMinScore: this.currentSearchMinScore,
|
searchMinScore: this.currentSearchMinScore,
|
||||||
searchMaxResults: this.currentSearchMaxResults,
|
searchMaxResults: this.currentSearchMaxResults,
|
||||||
|
fileWatcherDebounceMs: this.currentFileWatcherDebounceMs,
|
||||||
|
fileWatcherConcurrency: this.currentFileWatcherConcurrency,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -541,4 +555,24 @@ export class CodeIndexConfigManager {
|
||||||
public get currentSearchMaxResults(): number {
|
public get currentSearchMaxResults(): number {
|
||||||
return this.searchMaxResults ?? DEFAULT_MAX_SEARCH_RESULTS
|
return this.searchMaxResults ?? DEFAULT_MAX_SEARCH_RESULTS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the configured file watcher debounce delay in milliseconds.
|
||||||
|
* Higher values reduce CPU usage by batching more file changes together.
|
||||||
|
* Useful for multi-worktree scenarios where multiple watchers run simultaneously.
|
||||||
|
* Returns user setting if configured, otherwise returns default (500ms).
|
||||||
|
*/
|
||||||
|
public get currentFileWatcherDebounceMs(): number {
|
||||||
|
return this.fileWatcherDebounceMs ?? DEFAULT_FILE_WATCHER_DEBOUNCE_MS
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the configured file watcher concurrency limit.
|
||||||
|
* Lower values reduce CPU usage by processing fewer files in parallel.
|
||||||
|
* Useful for multi-worktree scenarios where multiple watchers run simultaneously.
|
||||||
|
* Returns user setting if configured, otherwise returns default (10).
|
||||||
|
*/
|
||||||
|
public get currentFileWatcherConcurrency(): number {
|
||||||
|
return this.fileWatcherConcurrency ?? DEFAULT_FILE_WATCHER_CONCURRENCY
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,14 @@ export const DEFAULT_MAX_SEARCH_RESULTS = CODEBASE_INDEX_DEFAULTS.DEFAULT_SEARCH
|
||||||
export const QDRANT_CODE_BLOCK_NAMESPACE = "f47ac10b-58cc-4372-a567-0e02b2c3d479"
|
export const QDRANT_CODE_BLOCK_NAMESPACE = "f47ac10b-58cc-4372-a567-0e02b2c3d479"
|
||||||
export const MAX_FILE_SIZE_BYTES = 1 * 1024 * 1024 // 1MB
|
export const MAX_FILE_SIZE_BYTES = 1 * 1024 * 1024 // 1MB
|
||||||
|
|
||||||
|
/**File Watcher Performance - Configurable for multi-worktree optimization */
|
||||||
|
export const DEFAULT_FILE_WATCHER_DEBOUNCE_MS = CODEBASE_INDEX_DEFAULTS.DEFAULT_FILE_WATCHER_DEBOUNCE_MS
|
||||||
|
export const MIN_FILE_WATCHER_DEBOUNCE_MS = CODEBASE_INDEX_DEFAULTS.MIN_FILE_WATCHER_DEBOUNCE_MS
|
||||||
|
export const MAX_FILE_WATCHER_DEBOUNCE_MS = CODEBASE_INDEX_DEFAULTS.MAX_FILE_WATCHER_DEBOUNCE_MS
|
||||||
|
export const DEFAULT_FILE_WATCHER_CONCURRENCY = CODEBASE_INDEX_DEFAULTS.DEFAULT_FILE_WATCHER_CONCURRENCY
|
||||||
|
export const MIN_FILE_WATCHER_CONCURRENCY = CODEBASE_INDEX_DEFAULTS.MIN_FILE_WATCHER_CONCURRENCY
|
||||||
|
export const MAX_FILE_WATCHER_CONCURRENCY = CODEBASE_INDEX_DEFAULTS.MAX_FILE_WATCHER_CONCURRENCY
|
||||||
|
|
||||||
/**Directory Scanner */
|
/**Directory Scanner */
|
||||||
export const MAX_LIST_FILES_LIMIT_CODE_INDEX = 50_000
|
export const MAX_LIST_FILES_LIMIT_CODE_INDEX = 50_000
|
||||||
export const BATCH_SEGMENT_THRESHOLD = 60 // Number of code segments to batch for embeddings/upserts
|
export const BATCH_SEGMENT_THRESHOLD = 60 // Number of code segments to batch for embeddings/upserts
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,9 @@ export interface CodeIndexConfig {
|
||||||
qdrantApiKey?: string
|
qdrantApiKey?: string
|
||||||
searchMinScore?: number
|
searchMinScore?: number
|
||||||
searchMaxResults?: number
|
searchMaxResults?: number
|
||||||
|
// File watcher performance settings for multi-worktree optimization
|
||||||
|
fileWatcherDebounceMs?: number
|
||||||
|
fileWatcherConcurrency?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -276,4 +276,99 @@ describe("FileWatcher", () => {
|
||||||
expect(mockWatcher.dispose).toHaveBeenCalled()
|
expect(mockWatcher.dispose).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("configurable performance settings", () => {
|
||||||
|
it("should use default debounce delay when not specified", async () => {
|
||||||
|
// The default FileWatcher was created in beforeEach without custom parameters
|
||||||
|
// Default is 500ms from DEFAULT_FILE_WATCHER_DEBOUNCE_MS
|
||||||
|
await fileWatcher.initialize()
|
||||||
|
|
||||||
|
// Trigger a file event
|
||||||
|
await mockOnDidCreate({ fsPath: "/mock/workspace/src/file.ts" })
|
||||||
|
|
||||||
|
// Wait less than default debounce time (500ms) - batch should not have started
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 200))
|
||||||
|
|
||||||
|
// No batch processing should have happened yet
|
||||||
|
expect(mockVectorStore.upsertPoints).not.toHaveBeenCalled()
|
||||||
|
|
||||||
|
// Wait for remaining time plus buffer
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 400))
|
||||||
|
|
||||||
|
// Now batch processing should have been triggered (or completed)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should use custom debounce delay when specified", async () => {
|
||||||
|
// Create a file watcher with a longer custom debounce delay (1000ms)
|
||||||
|
const customDebounceWatcher = new FileWatcher(
|
||||||
|
"/mock/workspace",
|
||||||
|
mockContext,
|
||||||
|
mockCacheManager,
|
||||||
|
mockEmbedder,
|
||||||
|
mockVectorStore,
|
||||||
|
mockIgnoreInstance,
|
||||||
|
undefined, // ignoreController
|
||||||
|
undefined, // batchSegmentThreshold
|
||||||
|
1000, // custom debounce delay (1000ms)
|
||||||
|
5, // custom concurrency limit
|
||||||
|
)
|
||||||
|
|
||||||
|
await customDebounceWatcher.initialize()
|
||||||
|
|
||||||
|
// Trigger a file event
|
||||||
|
await mockOnDidCreate({ fsPath: "/mock/workspace/src/custom-file.ts" })
|
||||||
|
|
||||||
|
// Wait for 700ms - should still not have processed (custom debounce is 1000ms)
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 700))
|
||||||
|
|
||||||
|
// No batch processing should have happened yet with 1000ms debounce
|
||||||
|
// Note: The file watcher uses its own internal debounce timer
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
customDebounceWatcher.dispose()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should use custom concurrency limit for file processing", async () => {
|
||||||
|
// Create a file watcher with a lower custom concurrency limit
|
||||||
|
const customConcurrencyWatcher = new FileWatcher(
|
||||||
|
"/mock/workspace",
|
||||||
|
mockContext,
|
||||||
|
mockCacheManager,
|
||||||
|
mockEmbedder,
|
||||||
|
mockVectorStore,
|
||||||
|
mockIgnoreInstance,
|
||||||
|
undefined, // ignoreController
|
||||||
|
undefined, // batchSegmentThreshold
|
||||||
|
100, // short debounce for faster test
|
||||||
|
2, // low concurrency limit (2)
|
||||||
|
)
|
||||||
|
|
||||||
|
await customConcurrencyWatcher.initialize()
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
customConcurrencyWatcher.dispose()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should accept all optional parameters including debounce and concurrency", async () => {
|
||||||
|
// Test that the constructor accepts all parameters without errors
|
||||||
|
const fullyConfiguredWatcher = new FileWatcher(
|
||||||
|
"/mock/workspace",
|
||||||
|
mockContext,
|
||||||
|
mockCacheManager,
|
||||||
|
mockEmbedder,
|
||||||
|
mockVectorStore,
|
||||||
|
mockIgnoreInstance,
|
||||||
|
undefined, // ignoreController
|
||||||
|
50, // batchSegmentThreshold
|
||||||
|
2000, // debounceMs
|
||||||
|
5, // concurrencyLimit
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(fullyConfiguredWatcher).toBeDefined()
|
||||||
|
|
||||||
|
// Initialize and dispose to ensure no runtime errors
|
||||||
|
await fullyConfiguredWatcher.initialize()
|
||||||
|
fullyConfiguredWatcher.dispose()
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ import {
|
||||||
BATCH_SEGMENT_THRESHOLD,
|
BATCH_SEGMENT_THRESHOLD,
|
||||||
MAX_BATCH_RETRIES,
|
MAX_BATCH_RETRIES,
|
||||||
INITIAL_RETRY_DELAY_MS,
|
INITIAL_RETRY_DELAY_MS,
|
||||||
|
DEFAULT_FILE_WATCHER_DEBOUNCE_MS,
|
||||||
|
DEFAULT_FILE_WATCHER_CONCURRENCY,
|
||||||
} from "../constants"
|
} from "../constants"
|
||||||
import { createHash } from "crypto"
|
import { createHash } from "crypto"
|
||||||
import { RooIgnoreController } from "../../../core/ignore/RooIgnoreController"
|
import { RooIgnoreController } from "../../../core/ignore/RooIgnoreController"
|
||||||
|
|
@ -35,8 +37,8 @@ export class FileWatcher implements IFileWatcher {
|
||||||
private ignoreController: RooIgnoreController
|
private ignoreController: RooIgnoreController
|
||||||
private accumulatedEvents: Map<string, { uri: vscode.Uri; type: "create" | "change" | "delete" }> = new Map()
|
private accumulatedEvents: Map<string, { uri: vscode.Uri; type: "create" | "change" | "delete" }> = new Map()
|
||||||
private batchProcessDebounceTimer?: NodeJS.Timeout
|
private batchProcessDebounceTimer?: NodeJS.Timeout
|
||||||
private readonly BATCH_DEBOUNCE_DELAY_MS = 500
|
private readonly batchDebounceDelayMs: number
|
||||||
private readonly FILE_PROCESSING_CONCURRENCY_LIMIT = 10
|
private readonly fileProcessingConcurrencyLimit: number
|
||||||
private readonly batchSegmentThreshold: number
|
private readonly batchSegmentThreshold: number
|
||||||
|
|
||||||
private readonly _onDidStartBatchProcessing = new vscode.EventEmitter<string[]>()
|
private readonly _onDidStartBatchProcessing = new vscode.EventEmitter<string[]>()
|
||||||
|
|
@ -66,9 +68,14 @@ export class FileWatcher implements IFileWatcher {
|
||||||
* Creates a new file watcher
|
* Creates a new file watcher
|
||||||
* @param workspacePath Path to the workspace
|
* @param workspacePath Path to the workspace
|
||||||
* @param context VS Code extension context
|
* @param context VS Code extension context
|
||||||
|
* @param cacheManager Cache manager
|
||||||
* @param embedder Optional embedder
|
* @param embedder Optional embedder
|
||||||
* @param vectorStore Optional vector store
|
* @param vectorStore Optional vector store
|
||||||
* @param cacheManager Cache manager
|
* @param ignoreInstance Optional ignore instance for .gitignore filtering
|
||||||
|
* @param ignoreController Optional RooIgnoreController for .rooignore filtering
|
||||||
|
* @param batchSegmentThreshold Optional batch segment threshold for embeddings
|
||||||
|
* @param debounceMs Optional debounce delay in ms (default 500ms). Higher values reduce CPU usage in multi-worktree scenarios.
|
||||||
|
* @param concurrencyLimit Optional concurrency limit for file processing (default 10). Lower values reduce CPU usage in multi-worktree scenarios.
|
||||||
*/
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
private workspacePath: string,
|
private workspacePath: string,
|
||||||
|
|
@ -79,6 +86,8 @@ export class FileWatcher implements IFileWatcher {
|
||||||
ignoreInstance?: Ignore,
|
ignoreInstance?: Ignore,
|
||||||
ignoreController?: RooIgnoreController,
|
ignoreController?: RooIgnoreController,
|
||||||
batchSegmentThreshold?: number,
|
batchSegmentThreshold?: number,
|
||||||
|
debounceMs?: number,
|
||||||
|
concurrencyLimit?: number,
|
||||||
) {
|
) {
|
||||||
this.ignoreController = ignoreController || new RooIgnoreController(workspacePath)
|
this.ignoreController = ignoreController || new RooIgnoreController(workspacePath)
|
||||||
if (ignoreInstance) {
|
if (ignoreInstance) {
|
||||||
|
|
@ -98,6 +107,14 @@ export class FileWatcher implements IFileWatcher {
|
||||||
this.batchSegmentThreshold = BATCH_SEGMENT_THRESHOLD
|
this.batchSegmentThreshold = BATCH_SEGMENT_THRESHOLD
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set configurable debounce delay for multi-worktree optimization
|
||||||
|
// Higher values batch more file changes together, reducing CPU spikes
|
||||||
|
this.batchDebounceDelayMs = debounceMs ?? DEFAULT_FILE_WATCHER_DEBOUNCE_MS
|
||||||
|
|
||||||
|
// Set configurable concurrency limit for multi-worktree optimization
|
||||||
|
// Lower values process fewer files in parallel, reducing peak CPU usage
|
||||||
|
this.fileProcessingConcurrencyLimit = concurrencyLimit ?? DEFAULT_FILE_WATCHER_CONCURRENCY
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -160,12 +177,13 @@ export class FileWatcher implements IFileWatcher {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Schedules batch processing with debounce
|
* Schedules batch processing with debounce
|
||||||
|
* Uses configurable debounce delay for multi-worktree optimization
|
||||||
*/
|
*/
|
||||||
private scheduleBatchProcessing(): void {
|
private scheduleBatchProcessing(): void {
|
||||||
if (this.batchProcessDebounceTimer) {
|
if (this.batchProcessDebounceTimer) {
|
||||||
clearTimeout(this.batchProcessDebounceTimer)
|
clearTimeout(this.batchProcessDebounceTimer)
|
||||||
}
|
}
|
||||||
this.batchProcessDebounceTimer = setTimeout(() => this.triggerBatchProcessing(), this.BATCH_DEBOUNCE_DELAY_MS)
|
this.batchProcessDebounceTimer = setTimeout(() => this.triggerBatchProcessing(), this.batchDebounceDelayMs)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -223,8 +241,9 @@ export class FileWatcher implements IFileWatcher {
|
||||||
const successfullyProcessedForUpsert: Array<{ path: string; newHash?: string }> = []
|
const successfullyProcessedForUpsert: Array<{ path: string; newHash?: string }> = []
|
||||||
const filesToProcessConcurrently = [...filesToUpsertDetails]
|
const filesToProcessConcurrently = [...filesToUpsertDetails]
|
||||||
|
|
||||||
for (let i = 0; i < filesToProcessConcurrently.length; i += this.FILE_PROCESSING_CONCURRENCY_LIMIT) {
|
// Use configurable concurrency limit for multi-worktree optimization
|
||||||
const chunkToProcess = filesToProcessConcurrently.slice(i, i + this.FILE_PROCESSING_CONCURRENCY_LIMIT)
|
for (let i = 0; i < filesToProcessConcurrently.length; i += this.fileProcessingConcurrencyLimit) {
|
||||||
|
const chunkToProcess = filesToProcessConcurrently.slice(i, i + this.fileProcessingConcurrencyLimit)
|
||||||
|
|
||||||
const chunkProcessingPromises = chunkToProcess.map(async (fileDetail) => {
|
const chunkProcessingPromises = chunkToProcess.map(async (fileDetail) => {
|
||||||
this._onBatchProgressUpdate.fire({
|
this._onBatchProgressUpdate.fire({
|
||||||
|
|
|
||||||
|
|
@ -188,6 +188,7 @@ export class CodeIndexServiceFactory {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a file watcher instance with its required dependencies.
|
* Creates a file watcher instance with its required dependencies.
|
||||||
|
* Passes configurable performance settings for multi-worktree optimization.
|
||||||
*/
|
*/
|
||||||
public createFileWatcher(
|
public createFileWatcher(
|
||||||
context: vscode.ExtensionContext,
|
context: vscode.ExtensionContext,
|
||||||
|
|
@ -197,6 +198,8 @@ export class CodeIndexServiceFactory {
|
||||||
ignoreInstance: Ignore,
|
ignoreInstance: Ignore,
|
||||||
rooIgnoreController?: RooIgnoreController,
|
rooIgnoreController?: RooIgnoreController,
|
||||||
): IFileWatcher {
|
): IFileWatcher {
|
||||||
|
const config = this.configManager.getConfig()
|
||||||
|
|
||||||
// Get the configurable batch size from VSCode settings
|
// Get the configurable batch size from VSCode settings
|
||||||
let batchSize: number
|
let batchSize: number
|
||||||
try {
|
try {
|
||||||
|
|
@ -207,6 +210,12 @@ export class CodeIndexServiceFactory {
|
||||||
// In test environment, vscode.workspace might not be available
|
// In test environment, vscode.workspace might not be available
|
||||||
batchSize = BATCH_SEGMENT_THRESHOLD
|
batchSize = BATCH_SEGMENT_THRESHOLD
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get configurable file watcher performance settings from config manager
|
||||||
|
// These allow users to optimize CPU usage for multi-worktree scenarios
|
||||||
|
const debounceMs = config.fileWatcherDebounceMs
|
||||||
|
const concurrencyLimit = config.fileWatcherConcurrency
|
||||||
|
|
||||||
return new FileWatcher(
|
return new FileWatcher(
|
||||||
this.workspacePath,
|
this.workspacePath,
|
||||||
context,
|
context,
|
||||||
|
|
@ -216,6 +225,8 @@ export class CodeIndexServiceFactory {
|
||||||
ignoreInstance,
|
ignoreInstance,
|
||||||
rooIgnoreController,
|
rooIgnoreController,
|
||||||
batchSize,
|
batchSize,
|
||||||
|
debounceMs,
|
||||||
|
concurrencyLimit,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue