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 7fff6cfc73..71fa405941 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; @@ -1515,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); @@ -3184,10 +3185,15 @@ 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 parallelizeExtraction = (options.Rebuild || writer.GetCounts().files == 0) - && !options.SymbolKindFilter.IsActive; + var hasPostExtractionHooks = postExtractionHooks.Hooks.Count > 0; + var parallelizeExtraction = (options.Rebuild || writer.GetCounts().files == 0 || headChangeDetected) + && !options.SymbolKindFilter.IsActive + && !hasPostExtractionHooks; + FullScanExtractionSchedulingForTesting?.Invoke( + parallelizeExtraction, + headChangeDetected ? "head_changed" : null); void StartIndexSpinnerIfNeeded() { diff --git a/tests/CodeIndex.Tests/IndexCommandRunnerTests.cs b/tests/CodeIndex.Tests/IndexCommandRunnerTests.cs index a105341a5a..ae7eb32dad 100644 --- a/tests/CodeIndex.Tests/IndexCommandRunnerTests.cs +++ b/tests/CodeIndex.Tests/IndexCommandRunnerTests.cs @@ -372,6 +372,92 @@ 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_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(); + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + DeleteDirectory(projectRoot); + } + } + [Fact] public void Run_Help_IncludesSymbolKindFilterFlags() {