Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions changelog.d/unreleased/2605.fixed.md
Original file line number Diff line number Diff line change
@@ -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 で長時間直列処理に留まって完了や通常の中断診断へ進めなくなる問題を防ぎます。
14 changes: 10 additions & 4 deletions src/CodeIndex/Cli/IndexCommandRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ private sealed record ScanCheckpoint(
IReadOnlyList<string> Directories);

internal static Action? FullScanWritePhaseStartedForTesting { get; set; }
internal static Action<bool, string?>? FullScanExtractionSchedulingForTesting { get; set; }
internal static Action? HotspotFamilyUpdateRestampReadyForCommitForTesting { get; set; }
internal static Func<bool> IsInputRedirectedForTesting { get; set; } = () => Console.IsInputRedirected;
internal static Func<string?> ReadLineForTesting { get; set; } = Console.ReadLine;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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()
{
Expand Down
86 changes: 86 additions & 0 deletions tests/CodeIndex.Tests/IndexCommandRunnerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
Loading