From c180989b1c8fa9aceecf80ebcccef2aab362c00b Mon Sep 17 00:00:00 2001 From: Widthdom Date: Sun, 24 May 2026 20:42:53 +0900 Subject: [PATCH 1/3] Fix full scan extraction after HEAD changes (#2605) --- changelog.d/unreleased/2605.fixed.md | 16 ++++++++ src/CodeIndex/Cli/IndexCommandRunner.cs | 6 ++- .../IndexCommandRunnerTests.cs | 41 +++++++++++++++++++ 3 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 changelog.d/unreleased/2605.fixed.md diff --git a/changelog.d/unreleased/2605.fixed.md b/changelog.d/unreleased/2605.fixed.md new file mode 100644 index 0000000000..be68237c67 --- /dev/null +++ b/changelog.d/unreleased/2605.fixed.md @@ -0,0 +1,16 @@ +--- +category: fixed +issues: + - 2605 +affected: + - src/CodeIndex/Cli/IndexCommandRunner.cs + - tests/CodeIndex.Tests/IndexCommandRunnerTests.cs +--- + +## English + +- **Full scans after a Git HEAD change parallelize extraction (#2605)** — `cdidx index` now uses the parallel extraction path when the existing index was stamped from a different commit, preventing branch-refresh full scans from spending long single-threaded stretches on large C# test files before they can finish or report normal interruption diagnostics. + +## 日本語 + +- **Git HEAD 変更後の full scan で抽出を並列化しました (#2605)** — 既存 index が別 commit で stamp されている場合、`cdidx index` は parallel extraction 経路を使うようになり、branch refresh 後の full scan が大きな C# test file で長時間直列処理に留まって完了や通常の中断診断へ進めなくなる問題を防ぎます。 diff --git a/src/CodeIndex/Cli/IndexCommandRunner.cs b/src/CodeIndex/Cli/IndexCommandRunner.cs index c0bd7bc8f9..31942c2921 100644 --- a/src/CodeIndex/Cli/IndexCommandRunner.cs +++ b/src/CodeIndex/Cli/IndexCommandRunner.cs @@ -28,6 +28,7 @@ private sealed record ScanCheckpoint( IReadOnlyList Directories); internal static Action? FullScanWritePhaseStartedForTesting { get; set; } + internal static Action? FullScanExtractionSchedulingForTesting { get; set; } internal static Action? HotspotFamilyUpdateRestampReadyForCommitForTesting { get; set; } internal static Func IsInputRedirectedForTesting { get; set; } = () => Console.IsInputRedirected; internal static Func ReadLineForTesting { get; set; } = Console.ReadLine; @@ -3175,8 +3176,11 @@ void ThrowIfFullScanCancelled(int filesProcessed, int? filesTotal) Task? jsonHeartbeatTask = null; var postExtractionHooks = PostExtractionHookRunner.DiscoverDefault(); var extractionParallelism = Math.Max(1, options.Parallelism); - var parallelizeExtraction = (options.Rebuild || writer.GetCounts().files == 0) + var parallelizeExtraction = (options.Rebuild || writer.GetCounts().files == 0 || headChangeDetected) && !options.SymbolKindFilter.IsActive; + FullScanExtractionSchedulingForTesting?.Invoke( + parallelizeExtraction, + headChangeDetected ? "head_changed" : null); void StartIndexSpinnerIfNeeded() { diff --git a/tests/CodeIndex.Tests/IndexCommandRunnerTests.cs b/tests/CodeIndex.Tests/IndexCommandRunnerTests.cs index 07c12b05c9..b6060bfc28 100644 --- a/tests/CodeIndex.Tests/IndexCommandRunnerTests.cs +++ b/tests/CodeIndex.Tests/IndexCommandRunnerTests.cs @@ -372,6 +372,47 @@ def helper(): } } + [Fact] + public void Run_FullScanAfterHeadChange_ParallelizesExtraction() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_head_changed_parallel_extract"); + bool? parallelized = null; + string? reason = null; + try + { + RunGit(projectRoot, "init"); + File.WriteAllText(Path.Combine(projectRoot, "app.cs"), "public class App { public void Run() { } }\n"); + RunGit(projectRoot, "add", "app.cs"); + RunGit(projectRoot, "commit", "-m", "initial"); + + var (initialExitCode, _) = RunAndCaptureJson([projectRoot, "--json"]); + Assert.Equal(CommandExitCodes.Success, initialExitCode); + + File.AppendAllText(Path.Combine(projectRoot, "app.cs"), "public class Next { public void Run() { } }\n"); + RunGit(projectRoot, "add", "app.cs"); + RunGit(projectRoot, "commit", "-m", "next"); + + IndexCommandRunner.FullScanExtractionSchedulingForTesting = (enabled, why) => + { + parallelized = enabled; + reason = why; + }; + + var (refreshExitCode, refreshJson) = RunAndCaptureJson([projectRoot, "--json"]); + + Assert.Equal(CommandExitCodes.Success, refreshExitCode); + Assert.Equal("success", refreshJson.GetProperty("status").GetString()); + Assert.True(parallelized); + Assert.Equal("head_changed", reason); + } + finally + { + IndexCommandRunner.FullScanExtractionSchedulingForTesting = null; + SqliteConnection.ClearAllPools(); + DeleteDirectory(projectRoot); + } + } + [Fact] public void Run_Help_IncludesSymbolKindFilterFlags() { From eff7b72f7669df7cd474f92a59b04bf29686d9f7 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Sun, 24 May 2026 20:49:47 +0900 Subject: [PATCH 2/3] Preserve hook ordering during head-change scans (#2605) --- src/CodeIndex/Cli/IndexCommandRunner.cs | 4 +- .../IndexCommandRunnerTests.cs | 42 +++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/src/CodeIndex/Cli/IndexCommandRunner.cs b/src/CodeIndex/Cli/IndexCommandRunner.cs index 8e9cb55113..c8675cc8e5 100644 --- a/src/CodeIndex/Cli/IndexCommandRunner.cs +++ b/src/CodeIndex/Cli/IndexCommandRunner.cs @@ -3187,8 +3187,10 @@ void ThrowIfFullScanCancelled(int filesProcessed, int? filesTotal) Task? jsonHeartbeatTask = null; var postExtractionHooks = PostExtractionHookRunner.DiscoverDefault(); var extractionParallelism = Math.Max(1, options.Parallelism); + var hasPostExtractionHooks = postExtractionHooks.Hooks.Count > 0; var parallelizeExtraction = (options.Rebuild || writer.GetCounts().files == 0 || headChangeDetected) - && !options.SymbolKindFilter.IsActive; + && !options.SymbolKindFilter.IsActive + && !hasPostExtractionHooks; FullScanExtractionSchedulingForTesting?.Invoke( parallelizeExtraction, headChangeDetected ? "head_changed" : null); diff --git a/tests/CodeIndex.Tests/IndexCommandRunnerTests.cs b/tests/CodeIndex.Tests/IndexCommandRunnerTests.cs index c6110df6a5..59756b6243 100644 --- a/tests/CodeIndex.Tests/IndexCommandRunnerTests.cs +++ b/tests/CodeIndex.Tests/IndexCommandRunnerTests.cs @@ -413,6 +413,48 @@ public void Run_FullScanAfterHeadChange_ParallelizesExtraction() } } + [Fact] + public void Run_FullScanAfterHeadChange_WithPostExtractionHooksKeepsSequentialReferences() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_head_changed_hooks_sequential"); + bool? parallelized = null; + var originalHooksDir = Environment.GetEnvironmentVariable("CDIDX_HOOKS_DIR"); + try + { + var hooksDir = Path.Combine(projectRoot, "hooks"); + Directory.CreateDirectory(hooksDir); + File.Copy(typeof(SamplePostExtractionHook).Assembly.Location, Path.Combine(hooksDir, "CodeIndex.Tests.dll")); + Environment.SetEnvironmentVariable("CDIDX_HOOKS_DIR", hooksDir); + + RunGit(projectRoot, "init"); + File.WriteAllText(Path.Combine(projectRoot, "app.cs"), "public class App { public void Run() { } }\n"); + RunGit(projectRoot, "add", "app.cs"); + RunGit(projectRoot, "commit", "-m", "initial"); + + var (initialExitCode, _) = RunAndCaptureJson([projectRoot, "--json"]); + Assert.Equal(CommandExitCodes.Success, initialExitCode); + + File.AppendAllText(Path.Combine(projectRoot, "app.cs"), "public class Next { public void Run() { } }\n"); + RunGit(projectRoot, "add", "app.cs"); + RunGit(projectRoot, "commit", "-m", "next"); + + IndexCommandRunner.FullScanExtractionSchedulingForTesting = (enabled, _) => parallelized = enabled; + + var (refreshExitCode, refreshJson) = RunAndCaptureJson([projectRoot, "--json"]); + + Assert.Equal(CommandExitCodes.Success, refreshExitCode); + Assert.Contains(refreshJson.GetProperty("status").GetString(), ["success", "partial"]); + Assert.False(parallelized); + } + finally + { + Environment.SetEnvironmentVariable("CDIDX_HOOKS_DIR", originalHooksDir); + IndexCommandRunner.FullScanExtractionSchedulingForTesting = null; + SqliteConnection.ClearAllPools(); + DeleteDirectory(projectRoot); + } + } + [Fact] public void Run_Help_IncludesSymbolKindFilterFlags() { From a3ddc98895a23e18c2ac0d9f3802b2509d1c7c2a Mon Sep 17 00:00:00 2001 From: Widthdom Date: Sun, 24 May 2026 22:47:50 +0900 Subject: [PATCH 3/3] Dispose hook runners during indexing (#2605) --- src/CodeIndex/Cli/IndexCommandRunner.cs | 4 ++-- tests/CodeIndex.Tests/IndexCommandRunnerTests.cs | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/CodeIndex/Cli/IndexCommandRunner.cs b/src/CodeIndex/Cli/IndexCommandRunner.cs index c8675cc8e5..71fa405941 100644 --- a/src/CodeIndex/Cli/IndexCommandRunner.cs +++ b/src/CodeIndex/Cli/IndexCommandRunner.cs @@ -1516,7 +1516,7 @@ private static int RunUpdateMode( var ftsMutated = false; var purgedRefs = 0; var supportedGraphLanguages = ReferenceExtractor.GetSupportedLanguages(); - var postExtractionHooks = PostExtractionHookRunner.DiscoverDefault(); + using var postExtractionHooks = PostExtractionHookRunner.DiscoverDefault(); var currentFoldVersion = NameFold.Version.ToString(System.Globalization.CultureInfo.InvariantCulture); var currentFoldFingerprint = NameFold.Fingerprint(); var currentCSharpSymbolNameContractVersion = DbContext.CSharpSymbolNameContractVersion.ToString(System.Globalization.CultureInfo.InvariantCulture); @@ -3185,7 +3185,7 @@ void ThrowIfFullScanCancelled(int filesProcessed, int? filesTotal) string? currentJsonIndexFile = null; CancellationTokenSource? jsonHeartbeatCts = null; Task? jsonHeartbeatTask = null; - var postExtractionHooks = PostExtractionHookRunner.DiscoverDefault(); + using var postExtractionHooks = PostExtractionHookRunner.DiscoverDefault(); var extractionParallelism = Math.Max(1, options.Parallelism); var hasPostExtractionHooks = postExtractionHooks.Hooks.Count > 0; var parallelizeExtraction = (options.Rebuild || writer.GetCounts().files == 0 || headChangeDetected) diff --git a/tests/CodeIndex.Tests/IndexCommandRunnerTests.cs b/tests/CodeIndex.Tests/IndexCommandRunnerTests.cs index 59756b6243..ae7eb32dad 100644 --- a/tests/CodeIndex.Tests/IndexCommandRunnerTests.cs +++ b/tests/CodeIndex.Tests/IndexCommandRunnerTests.cs @@ -451,6 +451,9 @@ public void Run_FullScanAfterHeadChange_WithPostExtractionHooksKeepsSequentialRe Environment.SetEnvironmentVariable("CDIDX_HOOKS_DIR", originalHooksDir); IndexCommandRunner.FullScanExtractionSchedulingForTesting = null; SqliteConnection.ClearAllPools(); + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); DeleteDirectory(projectRoot); } }