Skip to content

Commit bc2a267

Browse files
authored
Merge pull request #5048 from Widthdom/codex/large-codebase-indexing-performance-20260809
Speed up large-codebase indexing
2 parents d016f29 + 8f17f7a commit bc2a267

39 files changed

Lines changed: 3329 additions & 203 deletions

File tree

DEVELOPER_GUIDE.md

Lines changed: 103 additions & 19 deletions
Large diffs are not rendered by default.

TESTING_GUIDE.md

Lines changed: 16 additions & 8 deletions
Large diffs are not rendered by default.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
category: changed
3+
affected:
4+
- src/CodeIndex/Cli/IndexCommandRunner.FullScan.cs
5+
- src/CodeIndex/Cli/IndexCommandRunner.FullScan.Readiness.cs
6+
- src/CodeIndex/Cli/IndexCommandRunner.Update.cs
7+
- src/CodeIndex/Cli/IndexCommandRunner.Update.Readiness.cs
8+
- src/CodeIndex/Mcp/McpToolHandlers.Indexing.Execution.cs
9+
- src/CodeIndex/Database/DbWriter.TypeScriptAugmentations.cs
10+
---
11+
12+
## English
13+
14+
- **Large TypeScript indexes avoid refreshing the reference graph twice** — CLI full scans, scoped updates, and MCP indexing now let a planned declaration-augmentation rebuild perform the single graph-finalization pass while keeping candidate-index staging active through that pass. Empty results still finalize deletions or an inherited graph pass, marker-only validation skips a whole-graph scan, late validation failures fall back before partial readiness, and `--memory-trace` attributes the graph sample after the augmentation work. Authoritative scans without TypeScript stamp the contract without rebuilding augmentation rows.
15+
16+
## 日本語
17+
18+
- **大規模 TypeScript index で reference graph を2回 refresh しないようになりました** — CLI full scan、scoped update、MCP indexing は、candidate index の遅延を維持したまま予定された declaration augmentation rebuild に1回だけの graph finalization を担当させます。結果が空でも削除または引き継いだgraph passは確定し、marker検証だけなら全graph走査を省きます。late validation failureではpartial readiness前にfallbackし、`--memory-trace` の graph sample はaugmentation作業後へ帰属させます。TypeScriptのないauthoritative scanはaugmentation rowを再構築せずcontractをstampします。
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
category: changed
3+
affected:
4+
- src/CodeIndex/Cli/IndexCommandRunner.Update.cs
5+
- src/CodeIndex/Cli/IndexCommandRunner.Update.SecondaryIndexPreflight.cs
6+
- src/CodeIndex/Database/ReferenceSecondaryIndexSql.cs
7+
- src/CodeIndex/Database/ReferenceSecondaryIndexBulkLoadGuard.cs
8+
- src/CodeIndex/Database/DbContext.SchemaInitialization.cs
9+
---
10+
11+
## English
12+
13+
- **Large reference graphs no longer maintain the reverse candidate-symbol index row by row during candidate materialization** — Fresh, rebuilt, and high-churn full-graph indexing keeps this query-only index during raw reference persistence and drops it only when a graph refresh will actually repopulate candidates, including a TypeScript augmentation-owned pass. A filtered stat preflight skips reference and hotspot secondary-index staging when high-cardinality scoped targets are unchanged or only a sparse subset is expected to mutate indexed state; staging begins only when that estimate crosses the existing bulk threshold. Marker-only work also avoids rebuilding the candidate index. The candidate primary key remains available and success, cancellation, and recovery still converge on the identical final schema.
14+
15+
## 日本語
16+
17+
- **大規模 reference graph の candidate 構築中に candidate-symbol reverse index を行ごとに保守しなくなりました** — fresh、rebuild、および高 churn の full-graph indexing は、この query 専用 index を raw reference persistence 中は維持し、TypeScript augmentation が担当する pass を含め、graph refresh が candidate を実際に再構築するときだけ外します。filtered stat preflight は高 cardinality scoped target が全て unchanged、または indexed state を変更し得る target が疎な場合に reference / hotspot secondary-index staging を省き、その見積もり件数が既存 bulk threshold を超えた場合だけ staging を開始します。marker-only work も candidate index の再構築を避けます。candidate primary key を維持したまま、成功・cancellation・recovery の全経路で同一の最終 schema に収束します。
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
category: changed
3+
affected:
4+
- src/CodeIndex/Cli/IndexCommandRunner.FullScan.ExtractionPipeline.cs
5+
- src/CodeIndex/Cli/IndexCommandRunner.FullScan.ExtractionWorkers.cs
6+
- src/CodeIndex/Indexer/Scanning/FileIndexer.cs
7+
---
8+
9+
## English
10+
11+
- **Parallel full scans start large tail files earlier** — extraction now skips metadata probes with a single worker or when all work fits in the first worker wave; otherwise it checks only a fixed-size suffix of at most 64 work items and starts known in-limit files largest-first, reducing the final worker-wave tail without adding a repository-wide metadata pass.
12+
13+
## 日本語
14+
15+
- **parallel full scanが末尾の大きなfileを早く開始します** — workerが1つ、または全workが最初のworker waveに収まる場合はmetadata probeを省き、それ以外では最大64件の固定長suffixだけを確認してsize取得済みで上限内のfileを大きい順に開始するため、repository全体のmetadata passを増やさず最終worker waveの長い尾を短縮します。
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
category: changed
3+
affected:
4+
- src/CodeIndex/Indexer/Symbols/SymbolExtractor.ExtractionPhases.cs
5+
- src/CodeIndex/Indexer/Symbols/SymbolExtractor.JavaScriptScopes.cs
6+
---
7+
8+
## English
9+
10+
- **JavaScript and TypeScript symbol extraction reuses its sanitized snapshot** — module, supplemental-symbol, and private-scope analysis now share one column-preserving snapshot, avoiding a duplicate full-file lexical pass on scope-heavy files while retaining the flat-file pre-scan fast path.
11+
12+
## 日本語
13+
14+
- **JavaScript / TypeScript の symbol extraction が sanitized snapshot を再利用します** — module、supplemental symbol、private-scope 解析は列位置を保つ snapshot を共有し、flat file の pre-scan fast path を維持しながら scope の多い file で重複していた全file lexical passを避けます。
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
category: changed
3+
affected:
4+
- src/CodeIndex/Database/HotspotReferenceAggregateSql.cs
5+
- src/CodeIndex/Database/DbWriter.HotspotReferenceRefreshScope.cs
6+
- src/CodeIndex/Database/DbContext.SchemaInitialization.cs
7+
- src/CodeIndex/Database/DbContext.ReadMigrations.cs
8+
- src/CodeIndex/Cli/IndexCommandRunner.FullScan.cs
9+
- src/CodeIndex/Cli/IndexCommandRunner.Update.cs
10+
- src/CodeIndex/Mcp/McpToolHandlers.Indexing.Execution.cs
11+
---
12+
13+
## English
14+
15+
- **Large indexing runs rebuild hotspot aggregate query indexes once instead of maintaining four trees row by row** — Full scans, rebuilds, high-churn scoped updates, and MCP bulk indexing now stage the cross-language hotspot indexes inside the existing aggregate refresh transaction when at least 64 dirty files account for most aggregate rows, including an empty pre-refresh aggregate during fresh/rebuild work. An exact probe bounded by dirty-row cardinality keeps the indexes for small or highly skewed updates, and success, cancellation, and failure preserve the same ready state and final schema.
16+
17+
## 日本語
18+
19+
- **大規模 indexing では hotspot aggregate の query index 4本を行ごとに保守せず、最後に1回だけ再構築するようになりました** — full scan、rebuild、高 churn の scoped update、MCP bulk indexing は、64件以上のdirty fileがaggregate rowの大半を占める場合に既存のaggregate refresh transaction内で全言語共通のhotspot indexを遅延し、fresh / rebuildでrefresh前aggregateが空の場合も対象にします。dirty-row cardinalityで上限を設けたexact probeにより小規模または偏りの大きいupdateではindexを維持し、成功・cancellation・失敗の各経路で同じready stateと最終schemaを保ちます。
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
category: changed
3+
affected:
4+
- src/CodeIndex/Database/DbWriter.Fts.cs
5+
---
6+
7+
## English
8+
9+
- **Large FTS rebuilds avoid repeated automatic segment merges before their final optimize** — The standard and trigram indexes now disable routine automerge only around each transactional rebuild, restore their prior settings before commit, and retain crisis merging as a safety valve. Cancellation and failure roll back the configuration together with the rebuild.
10+
11+
## 日本語
12+
13+
- **大規模 FTS rebuild では最終 optimize 前の自動 segment merge を繰り返さないようになりました** — standard / trigram index は各 transaction 内の rebuild 中だけ通常 automerge を無効化し、commit 前に以前の設定へ戻します。crisis merge は安全弁として維持し、cancellation / failure 時は設定と rebuild を一緒に rollback します。

src/CodeIndex/Cli/IndexCommandRunner.FullScan.ExtractionPipeline.cs

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,13 @@ private readonly record struct FullScanExtractionScheduling(
113113
bool Parallelize,
114114
string? Reason);
115115

116+
private readonly record struct FullScanExtractionTailCandidate(
117+
int WorkOrdinal,
118+
long? Length);
119+
120+
private const int FullScanExtractionTailWorkerWaves = 4;
121+
internal const int MaxFullScanExtractionTailProbeCount = 64;
122+
116123
private static FullScanExtractionPipelineResult
117124
RunFullScanExtractionPipeline(
118125
FullScanExtractionPipelineContext context)
@@ -170,6 +177,98 @@ private static FullScanExtractionScheduling
170177
return new FullScanExtractionScheduling(parallelize, reason);
171178
}
172179

180+
internal static int[] BuildFullScanExtractionTailSchedule(
181+
int workItemCount,
182+
int workerCount,
183+
long maxFileSizeBytes,
184+
Func<int, long?> getFileLength,
185+
CancellationToken cancellationToken)
186+
{
187+
ArgumentOutOfRangeException.ThrowIfNegative(workItemCount);
188+
ArgumentOutOfRangeException.ThrowIfLessThan(workerCount, 1);
189+
ArgumentOutOfRangeException.ThrowIfNegative(maxFileSizeBytes);
190+
ArgumentNullException.ThrowIfNull(getFileLength);
191+
cancellationToken.ThrowIfCancellationRequested();
192+
if (workerCount <= 1 || workItemCount <= workerCount)
193+
return [];
194+
195+
// Dynamic claiming balances the main body of the scan, but a large file at the
196+
// input tail otherwise starts in the final worker wave. Probe only a fixed-size
197+
// suffix so the remedy never turns into an all-repository metadata pass.
198+
// 本体はdynamic claimで均等化し、末尾だけを固定上限でsize順にして全件statを避ける。
199+
var workerBound = workerCount >= MaxFullScanExtractionTailProbeCount / FullScanExtractionTailWorkerWaves
200+
? MaxFullScanExtractionTailProbeCount
201+
: workerCount * FullScanExtractionTailWorkerWaves;
202+
var tailCount = Math.Min(
203+
workItemCount,
204+
Math.Min(workerBound, MaxFullScanExtractionTailProbeCount));
205+
var tailStart = workItemCount - tailCount;
206+
var candidates = new FullScanExtractionTailCandidate[tailCount];
207+
for (var tailIndex = 0; tailIndex < tailCount; tailIndex++)
208+
{
209+
cancellationToken.ThrowIfCancellationRequested();
210+
var workOrdinal = tailStart + tailIndex;
211+
long? length;
212+
try
213+
{
214+
length = getFileLength(workOrdinal);
215+
}
216+
catch (Exception ex) when (
217+
ex is IOException
218+
or UnauthorizedAccessException
219+
or NotSupportedException
220+
or ArgumentException
221+
or System.Security.SecurityException)
222+
{
223+
length = null;
224+
}
225+
226+
var eligibleLength = length is >= 0
227+
&& length <= maxFileSizeBytes
228+
? length
229+
: null;
230+
candidates[tailIndex] = new FullScanExtractionTailCandidate(
231+
workOrdinal,
232+
eligibleLength);
233+
}
234+
235+
Array.Sort(
236+
candidates,
237+
static (left, right) =>
238+
{
239+
if (left.Length.HasValue != right.Length.HasValue)
240+
return left.Length.HasValue ? -1 : 1;
241+
242+
if (left.Length.HasValue)
243+
{
244+
var lengthComparison = right.Length.GetValueOrDefault().CompareTo(
245+
left.Length.GetValueOrDefault());
246+
if (lengthComparison != 0)
247+
return lengthComparison;
248+
}
249+
250+
return left.WorkOrdinal.CompareTo(right.WorkOrdinal);
251+
});
252+
253+
var schedule = new int[candidates.Length];
254+
for (var index = 0; index < candidates.Length; index++)
255+
schedule[index] = candidates[index].WorkOrdinal;
256+
return schedule;
257+
}
258+
259+
internal static int ResolveFullScanExtractionFileIndex(
260+
IReadOnlyList<int>? extractionFileIndexes,
261+
int workOrdinal)
262+
=> extractionFileIndexes == null
263+
? workOrdinal
264+
: extractionFileIndexes[workOrdinal];
265+
266+
private static long? ReadFullScanExtractionFileLength(string filePath)
267+
{
268+
var info = new FileInfo(filePath);
269+
return info.Exists ? info.Length : null;
270+
}
271+
173272
private static FullScanExtractionConsumerState
174273
ExecuteFullScanExtractionPipeline(
175274
FullScanExtractionPipelineContext context,
@@ -200,6 +299,18 @@ private static FullScanExtractionConsumerState
200299
new LazyDisposable<SymbolExtractionWorkerClient>(
201300
() => new SymbolExtractionWorkerClient(
202301
context.Options.MaxFileSizeBytes));
302+
var extractionTailSchedule = parallelizeExtraction
303+
? BuildFullScanExtractionTailSchedule(
304+
context.ExtractionWorkItemCount,
305+
extractionWorkerCount,
306+
context.Indexer.MaxFileSizeBytes,
307+
workOrdinal => ReadFullScanExtractionFileLength(
308+
context.FileTargets[
309+
ResolveFullScanExtractionFileIndex(
310+
context.ExtractionFileIndexes,
311+
workOrdinal)].FilePath),
312+
context.CancellationToken)
313+
: [];
203314
var workers = StartFullScanExtractionWorkers(
204315
new FullScanExtractionWorkerContext
205316
{
@@ -213,6 +324,7 @@ private static FullScanExtractionConsumerState
213324
context.ExtractionWorkItemCount,
214325
ExtractionWorkerCount = extractionWorkerCount,
215326
ParallelizeExtraction = parallelizeExtraction,
327+
ExtractionTailSchedule = extractionTailSchedule,
216328
CSharpWorkspace = context.GetCSharpWorkspace(),
217329
CSharpWorkspaceFileSnapshots =
218330
context.GetCSharpWorkspaceFileSnapshots(),

src/CodeIndex/Cli/IndexCommandRunner.FullScan.ExtractionWorkers.cs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ private sealed class FullScanExtractionWorkerContext
1818
internal required int ExtractionWorkItemCount { get; init; }
1919
internal required int ExtractionWorkerCount { get; init; }
2020
internal required bool ParallelizeExtraction { get; init; }
21+
internal required int[] ExtractionTailSchedule { get; init; }
2122
internal required CSharpStaticInterfaceWorkspaceSymbols CSharpWorkspace { get; init; }
2223
internal Dictionary<string, CSharpStaticInterfacePrepass.FileStatSnapshot>? CSharpWorkspaceFileSnapshots { get; init; }
2324
internal required PostExtractionHookRunner PostExtractionHooks { get; init; }
@@ -38,6 +39,7 @@ private static Task[] StartFullScanExtractionWorkers(
3839
var extractionWorkItemCount = context.ExtractionWorkItemCount;
3940
var extractionWorkerCount = context.ExtractionWorkerCount;
4041
var parallelizeExtraction = context.ParallelizeExtraction;
42+
var extractionTailSchedule = context.ExtractionTailSchedule;
4143
var csharpWorkspace = context.CSharpWorkspace;
4244
var csharpWorkspaceFileSnapshots = context.CSharpWorkspaceFileSnapshots;
4345
var postExtractionHooks = context.PostExtractionHooks;
@@ -58,9 +60,13 @@ private static Task[] StartFullScanExtractionWorkers(
5860
if (extractionIndex >= extractionWorkItemCount)
5961
break;
6062

61-
var fileIndex = extractionFileIndexes == null
62-
? extractionIndex
63-
: extractionFileIndexes[extractionIndex];
63+
var tailStart = extractionWorkItemCount - extractionTailSchedule.Length;
64+
var workOrdinal = extractionIndex >= tailStart
65+
? extractionTailSchedule[extractionIndex - tailStart]
66+
: extractionIndex;
67+
var fileIndex = ResolveFullScanExtractionFileIndex(
68+
extractionFileIndexes,
69+
workOrdinal);
6470
var target = fileTargets[fileIndex];
6571
var filePath = target.FilePath;
6672
var relativeFilePath = target.RelativePath;

0 commit comments

Comments
 (0)