diff options
| author | JaeHo Jang <[email protected]> | 2025-08-22 03:21:04 +0900 |
|---|---|---|
| committer | GitHub <[email protected]> | 2025-08-21 18:21:04 +0000 |
| commit | 1e5ead6960d531c51593be25c8665e4e8f118562 (patch) | |
| tree | ad77be43cb289ab7a3705e8abd423432eb1ac161 /packages/core/src/utils/memoryDiscovery.ts | |
| parent | 714b3dab73bb2a6e0f3c66ac8142db9ea7cc5fc7 (diff) | |
perf(core): parallelize memory discovery file operations performance gain (#5751)
Co-authored-by: Jacob Richman <[email protected]>
Diffstat (limited to 'packages/core/src/utils/memoryDiscovery.ts')
| -rw-r--r-- | packages/core/src/utils/memoryDiscovery.ts | 120 |
1 files changed, 83 insertions, 37 deletions
diff --git a/packages/core/src/utils/memoryDiscovery.ts b/packages/core/src/utils/memoryDiscovery.ts index d3c24baf..d2eff39c 100644 --- a/packages/core/src/utils/memoryDiscovery.ts +++ b/packages/core/src/utils/memoryDiscovery.ts @@ -96,19 +96,41 @@ async function getGeminiMdFilePathsInternal( ...includeDirectoriesToReadGemini, currentWorkingDirectory, ]); - const paths = []; - for (const dir of dirs) { - const pathsByDir = await getGeminiMdFilePathsInternalForEachDir( - dir, - userHomePath, - debugMode, - fileService, - extensionContextFilePaths, - fileFilteringOptions, - maxDirs, + + // Process directories in parallel with concurrency limit to prevent EMFILE errors + const CONCURRENT_LIMIT = 10; + const dirsArray = Array.from(dirs); + const pathsArrays: string[][] = []; + + for (let i = 0; i < dirsArray.length; i += CONCURRENT_LIMIT) { + const batch = dirsArray.slice(i, i + CONCURRENT_LIMIT); + const batchPromises = batch.map((dir) => + getGeminiMdFilePathsInternalForEachDir( + dir, + userHomePath, + debugMode, + fileService, + extensionContextFilePaths, + fileFilteringOptions, + maxDirs, + ), ); - paths.push(...pathsByDir); + + const batchResults = await Promise.allSettled(batchPromises); + + for (const result of batchResults) { + if (result.status === 'fulfilled') { + pathsArrays.push(result.value); + } else { + const error = result.reason; + const message = error instanceof Error ? error.message : String(error); + logger.error(`Error discovering files in directory: ${message}`); + // Continue processing other directories + } + } } + + const paths = pathsArrays.flat(); return Array.from(new Set<string>(paths)); } @@ -226,39 +248,63 @@ async function readGeminiMdFiles( debugMode: boolean, importFormat: 'flat' | 'tree' = 'tree', ): Promise<GeminiFileContent[]> { + // Process files in parallel with concurrency limit to prevent EMFILE errors + const CONCURRENT_LIMIT = 20; // Higher limit for file reads as they're typically faster const results: GeminiFileContent[] = []; - for (const filePath of filePaths) { - try { - const content = await fs.readFile(filePath, 'utf-8'); - // Process imports in the content - const processedResult = await processImports( - content, - path.dirname(filePath), - debugMode, - undefined, - undefined, - importFormat, - ); + for (let i = 0; i < filePaths.length; i += CONCURRENT_LIMIT) { + const batch = filePaths.slice(i, i + CONCURRENT_LIMIT); + const batchPromises = batch.map( + async (filePath): Promise<GeminiFileContent> => { + try { + const content = await fs.readFile(filePath, 'utf-8'); - results.push({ filePath, content: processedResult.content }); - if (debugMode) - logger.debug( - `Successfully read and processed imports: ${filePath} (Length: ${processedResult.content.length})`, - ); - } catch (error: unknown) { - const isTestEnv = - process.env['NODE_ENV'] === 'test' || process.env['VITEST']; - if (!isTestEnv) { + // Process imports in the content + const processedResult = await processImports( + content, + path.dirname(filePath), + debugMode, + undefined, + undefined, + importFormat, + ); + if (debugMode) + logger.debug( + `Successfully read and processed imports: ${filePath} (Length: ${processedResult.content.length})`, + ); + + return { filePath, content: processedResult.content }; + } catch (error: unknown) { + const isTestEnv = + process.env['NODE_ENV'] === 'test' || process.env['VITEST']; + if (!isTestEnv) { + const message = + error instanceof Error ? error.message : String(error); + logger.warn( + `Warning: Could not read ${getAllGeminiMdFilenames()} file at ${filePath}. Error: ${message}`, + ); + } + if (debugMode) logger.debug(`Failed to read: ${filePath}`); + return { filePath, content: null }; // Still include it with null content + } + }, + ); + + const batchResults = await Promise.allSettled(batchPromises); + + for (const result of batchResults) { + if (result.status === 'fulfilled') { + results.push(result.value); + } else { + // This case shouldn't happen since we catch all errors above, + // but handle it for completeness + const error = result.reason; const message = error instanceof Error ? error.message : String(error); - logger.warn( - `Warning: Could not read ${getAllGeminiMdFilenames()} file at ${filePath}. Error: ${message}`, - ); + logger.error(`Unexpected error processing file: ${message}`); } - results.push({ filePath, content: null }); // Still include it with null content - if (debugMode) logger.debug(`Failed to read: ${filePath}`); } } + return results; } |
