mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
fix: optimize memory usage for Swift project analysis
- Add Swift project detection utility to identify Swift/iOS projects - Implement reduced file limits (max 100 files) for Swift projects to prevent OOM - Optimize ripgrep buffer processing with smaller chunks (64KB) for better memory management - Add streaming improvements to process file listings more efficiently - Add comprehensive tests for Swift project detection This fixes the out-of-memory crashes when analyzing large Swift projects by limiting the number of files loaded into memory at once. Fixes #7345
This commit is contained in:
parent
8e4c0ae99d
commit
553311ae42
4 changed files with 233 additions and 2 deletions
|
|
@ -17,6 +17,7 @@ import { TerminalRegistry } from "../../integrations/terminal/TerminalRegistry"
|
|||
import { Terminal } from "../../integrations/terminal/Terminal"
|
||||
import { arePathsEqual } from "../../utils/path"
|
||||
import { formatResponse } from "../prompts/responses"
|
||||
import { getProjectFileLimit } from "../../utils/projectDetection"
|
||||
|
||||
import { Task } from "../task/Task"
|
||||
import { formatReminderSection } from "./reminder"
|
||||
|
|
@ -252,7 +253,15 @@ export async function getEnvironmentDetails(cline: Task, includeFileDetails: boo
|
|||
if (maxFiles === 0) {
|
||||
details += "(Workspace files context disabled. Use list_files to explore if needed.)"
|
||||
} else {
|
||||
const [files, didHitLimit] = await listFiles(cline.cwd, true, maxFiles)
|
||||
// Apply project-specific file limits to prevent memory issues
|
||||
const effectiveLimit = await getProjectFileLimit(cline.cwd, maxFiles)
|
||||
|
||||
// If we're using a reduced limit for a Swift project, add a note
|
||||
if (effectiveLimit < maxFiles) {
|
||||
details += `\n(Note: File listing limited to ${effectiveLimit} files for Swift project memory optimization)`
|
||||
}
|
||||
|
||||
const [files, didHitLimit] = await listFiles(cline.cwd, true, effectiveLimit)
|
||||
const { showRooIgnoredFiles = true } = state ?? {}
|
||||
|
||||
const result = formatResponse.formatFilesList(
|
||||
|
|
|
|||
|
|
@ -627,6 +627,9 @@ async function execRipgrep(rgPath: string, args: string[], limit: number): Promi
|
|||
let output = ""
|
||||
let results: string[] = []
|
||||
|
||||
// Use a smaller buffer size to process data more frequently and reduce memory usage
|
||||
const MAX_BUFFER_SIZE = 1024 * 64 // 64KB buffer instead of unlimited
|
||||
|
||||
// Set timeout to avoid hanging
|
||||
const timeoutId = setTimeout(() => {
|
||||
rgProcess.kill()
|
||||
|
|
@ -637,7 +640,11 @@ async function execRipgrep(rgPath: string, args: string[], limit: number): Promi
|
|||
// Process stdout data as it comes in
|
||||
rgProcess.stdout.on("data", (data) => {
|
||||
output += data.toString()
|
||||
processRipgrepOutput()
|
||||
|
||||
// Process output more frequently to avoid large memory buffers
|
||||
if (output.length > MAX_BUFFER_SIZE || output.split("\n").length > 100) {
|
||||
processRipgrepOutput()
|
||||
}
|
||||
|
||||
// Kill the process if we've reached the limit
|
||||
if (results.length >= limit) {
|
||||
|
|
@ -691,6 +698,8 @@ async function execRipgrep(rgPath: string, args: string[], limit: number): Promi
|
|||
// Keep the relative path as returned by ripgrep
|
||||
results.push(line)
|
||||
} else if (results.length >= limit) {
|
||||
// Clear the output buffer when we hit the limit to free memory
|
||||
output = ""
|
||||
break
|
||||
}
|
||||
}
|
||||
|
|
|
|||
127
src/utils/__tests__/projectDetection.spec.ts
Normal file
127
src/utils/__tests__/projectDetection.spec.ts
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
|
||||
import * as fs from "fs"
|
||||
import * as path from "path"
|
||||
import { isSwiftProject, getProjectFileLimit } from "../projectDetection"
|
||||
|
||||
vi.mock("fs", () => ({
|
||||
promises: {
|
||||
readdir: vi.fn(),
|
||||
stat: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
describe("projectDetection", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
describe("isSwiftProject", () => {
|
||||
it("should detect Swift project with Package.swift", async () => {
|
||||
vi.mocked(fs.promises.readdir).mockResolvedValue(["Package.swift", "Sources", "Tests"] as any)
|
||||
|
||||
const result = await isSwiftProject("/test/project")
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
|
||||
it("should detect Swift project with .xcodeproj", async () => {
|
||||
vi.mocked(fs.promises.readdir).mockResolvedValue(["MyApp.xcodeproj", "MyApp", "MyAppTests"] as any)
|
||||
|
||||
const result = await isSwiftProject("/test/project")
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
|
||||
it("should detect Swift project with .xcworkspace", async () => {
|
||||
vi.mocked(fs.promises.readdir).mockResolvedValue(["MyApp.xcworkspace", "MyApp.xcodeproj", "Pods"] as any)
|
||||
|
||||
const result = await isSwiftProject("/test/project")
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
|
||||
it("should detect Swift project with Podfile", async () => {
|
||||
vi.mocked(fs.promises.readdir).mockResolvedValue(["Podfile", "Podfile.lock", "MyApp"] as any)
|
||||
|
||||
const result = await isSwiftProject("/test/project")
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
|
||||
it("should detect Swift project with Cartfile", async () => {
|
||||
vi.mocked(fs.promises.readdir).mockResolvedValue(["Cartfile", "Cartfile.resolved", "MyApp"] as any)
|
||||
|
||||
const result = await isSwiftProject("/test/project")
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
|
||||
it("should detect Swift project with .swift files in root", async () => {
|
||||
vi.mocked(fs.promises.readdir).mockResolvedValue(["main.swift", "AppDelegate.swift", "README.md"] as any)
|
||||
|
||||
const result = await isSwiftProject("/test/project")
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
|
||||
it("should detect Swift project with Sources directory containing Swift files", async () => {
|
||||
vi.mocked(fs.promises.readdir).mockResolvedValueOnce(["Sources", "Tests", "README.md"] as any)
|
||||
|
||||
vi.mocked(fs.promises.stat).mockResolvedValue({
|
||||
isDirectory: () => true,
|
||||
} as any)
|
||||
|
||||
vi.mocked(fs.promises.readdir).mockResolvedValueOnce(["App.swift", "Model.swift"] as any)
|
||||
|
||||
const result = await isSwiftProject("/test/project")
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
|
||||
it("should not detect non-Swift project", async () => {
|
||||
vi.mocked(fs.promises.readdir).mockResolvedValue([
|
||||
"package.json",
|
||||
"node_modules",
|
||||
"src",
|
||||
"README.md",
|
||||
] as any)
|
||||
|
||||
const result = await isSwiftProject("/test/project")
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
|
||||
it("should handle errors gracefully", async () => {
|
||||
vi.mocked(fs.promises.readdir).mockRejectedValue(new Error("Permission denied"))
|
||||
|
||||
const result = await isSwiftProject("/test/project")
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("getProjectFileLimit", () => {
|
||||
it("should return reduced limit for Swift projects", async () => {
|
||||
vi.mocked(fs.promises.readdir).mockResolvedValue(["Package.swift", "Sources"] as any)
|
||||
|
||||
const result = await getProjectFileLimit("/test/project", 200)
|
||||
expect(result).toBe(100)
|
||||
})
|
||||
|
||||
it("should return reduced limit not exceeding 100 for Swift projects", async () => {
|
||||
vi.mocked(fs.promises.readdir).mockResolvedValue(["MyApp.xcodeproj"] as any)
|
||||
|
||||
const result = await getProjectFileLimit("/test/project", 500)
|
||||
expect(result).toBe(100)
|
||||
})
|
||||
|
||||
it("should return default limit for non-Swift projects", async () => {
|
||||
vi.mocked(fs.promises.readdir).mockResolvedValue(["package.json", "src"] as any)
|
||||
|
||||
const result = await getProjectFileLimit("/test/project", 200)
|
||||
expect(result).toBe(200)
|
||||
})
|
||||
|
||||
it("should return smaller default if it's less than 100 for Swift projects", async () => {
|
||||
vi.mocked(fs.promises.readdir).mockResolvedValue(["Package.swift"] as any)
|
||||
|
||||
const result = await getProjectFileLimit("/test/project", 50)
|
||||
expect(result).toBe(50)
|
||||
})
|
||||
})
|
||||
})
|
||||
86
src/utils/projectDetection.ts
Normal file
86
src/utils/projectDetection.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
import * as fs from "fs"
|
||||
import * as path from "path"
|
||||
|
||||
/**
|
||||
* Detects if a directory contains a Swift project
|
||||
* @param dirPath - The directory path to check
|
||||
* @returns true if it's a Swift project, false otherwise
|
||||
*/
|
||||
export async function isSwiftProject(dirPath: string): Promise<boolean> {
|
||||
try {
|
||||
// Check for common Swift project indicators
|
||||
const swiftIndicators = [
|
||||
"Package.swift", // Swift Package Manager
|
||||
"*.xcodeproj", // Xcode project
|
||||
"*.xcworkspace", // Xcode workspace
|
||||
"Podfile", // CocoaPods
|
||||
"Cartfile", // Carthage
|
||||
]
|
||||
|
||||
const entries = await fs.promises.readdir(dirPath)
|
||||
|
||||
for (const entry of entries) {
|
||||
// Check for exact matches
|
||||
if (swiftIndicators.includes(entry)) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check for pattern matches (e.g., *.xcodeproj)
|
||||
for (const indicator of swiftIndicators) {
|
||||
if (indicator.includes("*")) {
|
||||
const pattern = indicator.replace("*", "")
|
||||
if (entry.endsWith(pattern)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also check if there are .swift files in the root directory
|
||||
const swiftFiles = entries.filter((entry) => entry.endsWith(".swift"))
|
||||
if (swiftFiles.length > 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check for iOS/macOS specific directories
|
||||
const iosIndicators = ["Sources", "Tests", "UITests"]
|
||||
for (const indicator of iosIndicators) {
|
||||
const indicatorPath = path.join(dirPath, indicator)
|
||||
try {
|
||||
const stats = await fs.promises.stat(indicatorPath)
|
||||
if (stats.isDirectory()) {
|
||||
// Check if this directory contains Swift files
|
||||
const subEntries = await fs.promises.readdir(indicatorPath)
|
||||
const hasSwiftFiles = subEntries.some((entry) => entry.endsWith(".swift"))
|
||||
if (hasSwiftFiles) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Directory doesn't exist, continue checking
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
} catch (error) {
|
||||
console.error(`Error detecting Swift project: ${error}`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the recommended file limit for a project based on its type
|
||||
* @param dirPath - The directory path to check
|
||||
* @param defaultLimit - The default limit to use
|
||||
* @returns The recommended file limit
|
||||
*/
|
||||
export async function getProjectFileLimit(dirPath: string, defaultLimit: number): Promise<number> {
|
||||
// For Swift projects, use a more conservative limit to prevent memory issues
|
||||
if (await isSwiftProject(dirPath)) {
|
||||
// Swift projects often have large dependency trees and generated files
|
||||
// Use a smaller limit to prevent memory exhaustion
|
||||
return Math.min(defaultLimit, 100)
|
||||
}
|
||||
|
||||
return defaultLimit
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue