fix(tools): throttle ripgrep CPU usage with thread limits and concurrency control

- Add --threads=4 flag to all rg invocations (grep and glob)
- Add global semaphore limiting concurrent rg processes to 2
- Reduce grep timeout from 300s to 60s (matches tool description)
- Reduce max output from 10MB to 256KB (prevents excessive memory usage)
- Add output_mode parameter (content/files_with_matches/count)
- Add head_limit parameter for incremental result fetching

Closes #2008

Ref: #674, #1722
This commit is contained in:
JiHongKim98
2026-02-21 03:01:23 +09:00
parent 7fa22aebdf
commit dafdca217b
8 changed files with 117 additions and 11 deletions
+34 -4
View File
@@ -8,14 +8,17 @@ import {
DEFAULT_MAX_COLUMNS,
DEFAULT_TIMEOUT_MS,
DEFAULT_MAX_OUTPUT_BYTES,
DEFAULT_RG_THREADS,
RG_SAFETY_FLAGS,
GREP_SAFETY_FLAGS,
} from "./constants"
import type { GrepOptions, GrepMatch, GrepResult, CountResult } from "./types"
import { rgSemaphore } from "../shared/semaphore"
function buildRgArgs(options: GrepOptions): string[] {
const args: string[] = [
...RG_SAFETY_FLAGS,
`--threads=${Math.min(options.threads ?? DEFAULT_RG_THREADS, DEFAULT_RG_THREADS)}`,
`--max-depth=${Math.min(options.maxDepth ?? DEFAULT_MAX_DEPTH, DEFAULT_MAX_DEPTH)}`,
`--max-filesize=${options.maxFilesize ?? DEFAULT_MAX_FILESIZE}`,
`--max-count=${Math.min(options.maxCount ?? DEFAULT_MAX_COUNT, DEFAULT_MAX_COUNT)}`,
@@ -51,6 +54,12 @@ function buildRgArgs(options: GrepOptions): string[] {
}
}
if (options.outputMode === "files_with_matches") {
args.push("--files-with-matches")
} else if (options.outputMode === "count") {
args.push("--count")
}
return args
}
@@ -130,6 +139,15 @@ function parseCountOutput(output: string): CountResult[] {
}
export async function runRg(options: GrepOptions): Promise<GrepResult> {
await rgSemaphore.acquire()
try {
return await runRgInternal(options)
} finally {
rgSemaphore.release()
}
}
async function runRgInternal(options: GrepOptions): Promise<GrepResult> {
const cli = resolveGrepCli()
const args = buildArgs(options, cli.backend)
const timeout = Math.min(options.timeout ?? DEFAULT_TIMEOUT_MS, DEFAULT_TIMEOUT_MS)
@@ -174,13 +192,16 @@ export async function runRg(options: GrepOptions): Promise<GrepResult> {
}
const matches = parseOutput(outputToProcess)
const filesSearched = new Set(matches.map((m) => m.file)).size
const limited = options.headLimit && options.headLimit > 0
? matches.slice(0, options.headLimit)
: matches
const filesSearched = new Set(limited.map((m) => m.file)).size
return {
matches,
totalMatches: matches.length,
matches: limited,
totalMatches: limited.length,
filesSearched,
truncated,
truncated: truncated || (options.headLimit ? matches.length > options.headLimit : false),
}
} catch (e) {
return {
@@ -194,6 +215,15 @@ export async function runRg(options: GrepOptions): Promise<GrepResult> {
}
export async function runRgCount(options: Omit<GrepOptions, "context">): Promise<CountResult[]> {
await rgSemaphore.acquire()
try {
return await runRgCountInternal(options)
} finally {
rgSemaphore.release()
}
}
async function runRgCountInternal(options: Omit<GrepOptions, "context">): Promise<CountResult[]> {
const cli = resolveGrepCli()
const args = buildArgs({ ...options, context: 0 }, cli.backend)