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
15 changes: 15 additions & 0 deletions changelog.d/unreleased/1667.internal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
category: internal
issues:
- 1667
affected:
- tests/CodeIndex.Tests/PerformanceTests.cs
---

## English

- **Added CI allocation budgets for extraction hot paths (#1667)** — symbol and reference extraction now have fixed C# fixture allocation checks to catch memory-pressure regressions before indexing slows down.

## 日本語

- **抽出 hot path 向けの CI allocation budget を追加しました (#1667)** — symbol extraction と reference extraction に固定 C# fixture の allocation 検査を加え、indexing が遅くなる前にメモリ負荷の回帰を検出します。
16 changes: 16 additions & 0 deletions changelog.d/unreleased/2740.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
category: fixed
issues:
- 2740
affected:
- src/CodeIndex/Indexer/Symbols/SymbolExtractor.CSharpScanner.cs
- tests/CodeIndex.Tests/SymbolExtractorTests.cs
---

## English

- **C# symbol extraction no longer repeatedly rescans method bodies as field candidates (#2740)** — large C# files such as `SymbolExtractor.JavaScriptTypeScriptSupport.cs` now avoid a quadratic property-header fallback during full indexing.

## 日本語

- **C# シンボル抽出がメソッド本体をフィールド候補として繰り返し再走査しなくなりました (#2740)** — `SymbolExtractor.JavaScriptTypeScriptSupport.cs` のような大きな C# ファイルで、full index 中の property header fallback が二乗的に重くなる経路を避けます。
23 changes: 23 additions & 0 deletions src/CodeIndex/Indexer/Symbols/SymbolExtractor.CSharpScanner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1798,6 +1798,14 @@ private static CSharpPropertyMatchCandidate BuildCSharpPropertyMatchLine(string[

var isPropertyHeaderPrefix = CSharpPropertyHeaderPrefixRegex.IsMatch(matchLine);
var isMethodHeaderPrefix = CSharpMethodHeaderPrefixRegex.IsMatch(matchLine);
if (!isPropertyHeaderPrefix
&& isMethodHeaderPrefix
&& matchLine.IndexOf('(') >= 0
&& (matchLine.IndexOf('{') >= 0 || matchLine.IndexOf(';') >= 0))
{
return new CSharpPropertyMatchCandidate(matchLine, startLineIndex, startLineIndex);
}

if (string.IsNullOrWhiteSpace(matchLine)
|| (!isPropertyHeaderPrefix && !isMethodHeaderPrefix)
|| HasCSharpPropertyAccessorStart(matchLine)
Expand Down Expand Up @@ -1928,6 +1936,7 @@ private static bool IsCSharpNonMemberHeaderLine(string line)
|| trimmed.StartsWith("using ", StringComparison.Ordinal)
|| trimmed.StartsWith("global using ", StringComparison.Ordinal)
|| trimmed.StartsWith("extern alias ", StringComparison.Ordinal)
|| trimmed.StartsWith("var ", StringComparison.Ordinal)
|| trimmed.StartsWith("//", StringComparison.Ordinal);
}

Expand Down Expand Up @@ -1962,6 +1971,12 @@ private static CSharpPropertyMatchCandidate ContinueConfirmedCSharpPropertyMatch
openBraceLineIndex,
openBraceExclusiveEndColumn);
}

if (accessorProbeStatus == CSharpAccessorProbeStatus.Rejected
&& CSharpConfirmedMethodPrefixRegex.IsMatch(normalizedCombined))
{
return new CSharpPropertyMatchCandidate(normalizedCombined, currentLineIndex, currentLineIndex);
}
}

for (int i = currentLineIndex + 1; i < csharpMatchLines.Length; i++)
Expand All @@ -1983,6 +1998,14 @@ private static CSharpPropertyMatchCandidate ContinueConfirmedCSharpPropertyMatch
openBraceExclusiveEndColumn.Value,
i);
accessorProbeStatus = ClassifyCSharpAccessorProbe(accessorProbeBuilder.ToString());
if (accessorProbeStatus == CSharpAccessorProbeStatus.Rejected
&& CSharpConfirmedMethodPrefixRegex.IsMatch(CollapseCSharpGenericTypeWhitespace(builder.ToString())))
{
return new CSharpPropertyMatchCandidate(
CollapseCSharpGenericTypeWhitespace(builder.ToString()),
i,
i);
}
}
else if (accessorProbeBuilder != null
&& accessorProbeStatus == CSharpAccessorProbeStatus.Pending)
Expand Down
52 changes: 52 additions & 0 deletions tests/CodeIndex.Tests/PerformanceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,58 @@ public void ExtractLargeSameLineSymbolFixture_CompletesInReasonableTime()
Assert.Equal(4_000, symbols.Count);
}

[Fact]
public void SymbolExtraction_CsharpHotPath_StaysWithinAllocationBudget()
{
var content = BuildCSharpHotPathFixture(typeCount: 120);
_ = SymbolExtractor.Extract(1, "csharp", content);

var allocatedBytes = MeasureAllocatedBytes(() => SymbolExtractor.Extract(1, "csharp", content));

Assert.True(allocatedBytes < 18_000_000, $"Symbol extraction allocated {allocatedBytes:N0} bytes");
}

[Fact]
public void ReferenceExtraction_CsharpHotPath_StaysWithinAllocationBudget()
{
var content = BuildCSharpHotPathFixture(typeCount: 80);
var symbols = SymbolExtractor.Extract(1, "csharp", content);
_ = ReferenceExtractor.Extract(1, "csharp", content, symbols);

var allocatedBytes = MeasureAllocatedBytes(() => ReferenceExtractor.Extract(1, "csharp", content, symbols));

Assert.True(allocatedBytes < 18_000_000, $"Reference extraction allocated {allocatedBytes:N0} bytes");
}

private static long MeasureAllocatedBytes(Action action)
{
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();

var before = GC.GetAllocatedBytesForCurrentThread();
action();
return GC.GetAllocatedBytesForCurrentThread() - before;
}

private static string BuildCSharpHotPathFixture(int typeCount)
{
return string.Join(
"\n",
Enumerable.Range(0, typeCount).Select(i => $$"""
public sealed class Service{{i}}
{
private readonly Dependency{{i}} dependency;
public Service{{i}}(Dependency{{i}} dependency) => this.dependency = dependency;
public Result{{i}} Execute(Request{{i}} request)
{
var value = dependency.Transform(request.Value);
return new Result{{i}}(value);
}
}
"""));
}

public void Dispose()
{
_db.Dispose();
Expand Down
18 changes: 18 additions & 0 deletions tests/CodeIndex.Tests/SymbolExtractorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,24 @@ public interface IAddable<TSelf>
Assert.DoesNotContain(symbols, symbol => symbol.Kind == "function" && symbol.Name.StartsWith("operator", StringComparison.Ordinal));
}

[Fact]
public void Extract_CsharpManyMethods_DoesNotRescanMethodBodiesAsFieldCandidates()
{
var methods = Enumerable.Range(0, 80).Select(i => $$"""
public void M{{i}}()
{
var value = {{i}};
value++;
}
""");
var content = "public class ManyMethods\n{\n" + string.Join('\n', methods) + "\n}";

var symbols = SymbolExtractor.Extract(1, "csharp", content);

Assert.Equal(80, symbols.Count(symbol => symbol.Kind == "function" && symbol.Name.StartsWith("M", StringComparison.Ordinal)));
Assert.DoesNotContain(symbols, symbol => symbol.Kind == "function" && symbol.Signature?.Contains("value++", StringComparison.Ordinal) == true);
}

[Fact]
public void Extract_PythonDataclassField_IndexesFieldAndMetadataKeys()
{
Expand Down
Loading