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
2 changes: 2 additions & 0 deletions TESTING_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -796,6 +796,7 @@ Candidate-ordered parallel-index recovery tests must prove that the fatal result
Focused authoritative-fresh fold-readiness coverage spans `DatabaseTests`, `IndexCommandRunnerTests`, and `McpServerToolsCallTests`: the built-in empty-database CLI/MCP path must consume its claim once, retain the NULL-column verification, skip the stored-value re-fold scan, and produce the same readiness/version/fingerprint/language stamps and Unicode, Markdown, C#, Nim, and TypeScript query results as full validation. Pair it with fail-closed cases for each initially nonempty ownership table (`files`, `symbols`, or `symbol_references`), a wrong or reused claim, an intervening external commit observed through `PRAGMA data_version`, rebuild/update/legacy/public-writer paths, and custom plugins, patterns, or post-extraction hooks; full validation must still reject NULL and stale non-NULL folds. A run-barrier regression must also activate a custom producer and then reload back to built-in-only before readiness: the current producer count returns to zero, but the monotonic mutation generation changes and forces full validation. Unchanged missing-directory and diagnostic-only publications must not change that generation.
A deterministic cancel-after-`BEGIN IMMEDIATE` test must prove that the raw transaction is rolled back and the same writer can immediately start and commit another transaction.
For performance audits, alternate identical repository-scale fresh fixtures, isolate the readiness-finalization interval, and report elapsed time plus `GC.GetAllocatedBytesForCurrentThread`; adoption requires removing row-count-proportional managed allocation without changing rows, stamps, or query results. Keep wall-clock measurements out of blocking CI assertions and remove temporary instrumentation after recording the result.
`SymbolExtractorCSharpRegexProbeTests` warms both C# extraction paths, alternates five samples of three extractions per path, and compares their medians with a fixed 64 KiB noise allowance. Keep the structural metric and symbol-parity assertions as the optimization/correctness proof, and keep the deterministic comparison-boundary test that accepts the allowance exactly but rejects one byte beyond it, so full-suite runtime noise cannot mask a material optimized-path allocation increase (#5244).
`ReferenceExtraction_MaskedMultilinePayloads_StayWithinAllocationBudget` keeps C# raw strings, Java text blocks, and TypeScript template literals from materializing trimmed reference contexts after structural masking has made a line empty.
`ReferenceExtraction_CallFreePaddedLines_DefersContextMaterialization` covers padded reference-free C#, Python, Erlang, and Solidity files across the core, functional, and dedicated extraction loops; keep its blocking `net8.0` allocation ceiling below 10.5 MB and pair it with `ReferenceExtractorTests.Extract_BuiltInReferencePaths_PreserveTrimmedContextAndPhysicalColumn` so deferred contexts remain trimmed while columns stay physical.
`CppHeaderDetection_LargeSample_DoesNotMaterializeLineArrays` keeps bounded C / C++ header-disambiguation samples on span-based line walks instead of allocating a string and array for every sampled line.
Expand Down Expand Up @@ -1961,6 +1962,7 @@ dotnet test --filter "FullyQualifiedName~GitHelperTests"
authoritative-fresh fold readiness の focused coverage は `DatabaseTests`、`IndexCommandRunnerTests`、`McpServerToolsCallTests` で分担します。built-in の empty-database CLI / MCP 経路が claim を一度だけ consume し、NULL column の検証を維持しつつ、保存 value の再 fold scan を省き、full validation と同じ readiness / version / fingerprint / language stamp、および Unicode、Markdown、C#、Nim、TypeScript の query result を生成することを固定します。初期状態で ownership table(`files`、`symbols`、`symbol_references`)のいずれかが非空の場合、owner が異なるか再利用された claim、`PRAGMA data_version` で観測される外部 commit、rebuild / update / legacy / public-writer 経路、custom plugin / pattern / post-extraction hook は fail closed であることも対にし、full validation が NULL と stale な非 NULL fold を引き続き拒否することを確認します。run barrier では custom producer を一度 active にしてから readiness 前に built-in-only へ reload し、最終 producer count が zero に戻っていても monotonic mutation generation の変化で full validation へ戻ることを固定します。状態不変の missing-directory と diagnostic-only publication では generation が変わらないことも確認します。
`BEGIN IMMEDIATE`成功直後のdeterministicなcancel testでは、raw transactionがrollbackされ、同じwriterが直後に別transactionを開始・commitできることを必須とします。
性能監査では、同一の repository-scale fresh fixture を交互に実行し、readiness finalization 区間を分離して、経過時間と `GC.GetAllocatedBytesForCurrentThread` を報告します。row 数に比例する managed allocation を取り除きつつ、row、stamp、query result が変わらないことを採用条件にします。wall-clock 計測は blocking CI assertion にせず、結果を記録したら一時 instrumentation を削除してください。
`SymbolExtractorCSharpRegexProbeTests` は C# extraction の両経路を warm-up し、各経路3回の extraction からなる sample を5回交互に測定して、その中央値を固定の64 KiB noise allowance付きで比較します。構造 metric と symbol parity の assertion を最適化および正しさの証拠として維持し、許容量ちょうどは受理する一方で1 byteでも超えれば拒否する決定論的な比較境界 test も維持してください。これにより、full-suite の runtime noise を除外しながら optimized path の重大な allocation 増加は隠しません (#5244)。
`ReferenceExtraction_MaskedMultilinePayloads_StayWithinAllocationBudget` は、構造マスク後に空行となった C# raw string、Java text block、TypeScript template literal から trim 済み reference context を実体化しないことを固定します。
`CppHeaderDetection_LargeSample_DoesNotMaterializeLineArrays` は、bounded な C / C++ header 判定 sample を span ベースで行走査し、sampled line ごとの string と array を割り当てないことを固定します。
`DelimitedSpanWalking_DenseExtractorLists_DoesNotAllocate` は、repository metadata、application manifest、VHDL、CUDA extraction が共有する single-delimiter walker の trim / remove-empty semantics を allocation-free に固定します。
Expand Down
16 changes: 16 additions & 0 deletions changelog.d/unreleased/5244.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
category: fixed
issues:
- 5244
affected:
- tests/CodeIndex.Tests/SymbolExtractorCSharpRegexProbeTests.cs
- TESTING_GUIDE.md
---

## English

- **Stabilized C# regex allocation regression probes under full-suite load (#5244)** — the probes now compare medians from alternating warmed samples and tolerate up to 64 KiB of measurement noise while still rejecting a larger optimized-path allocation increase and preserving structural and symbol-parity coverage.

## 日本語

- **full-suite 負荷時の C# regex allocation 回帰プローブを安定化しました (#5244)** — warm-up 後に交互取得した sample の中央値を比較し、64 KiB までの測定ノイズを許容する一方、それを超える optimized path の allocation 増加は引き続き拒否し、構造および symbol parity の coverage も維持します。
104 changes: 76 additions & 28 deletions tests/CodeIndex.Tests/SymbolExtractorCSharpRegexProbeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ namespace CodeIndex.Tests;

public sealed class SymbolExtractorCSharpRegexProbeTests
{
private const int AllocationSampleCount = 5;
private const long AllocationNoiseAllowanceBytes = 64 * 1024;

private static readonly PropertyInfo[] SymbolProperties = typeof(SymbolRecord)
.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
.Where(property => property.CanRead && property.GetIndexParameters().Length == 0)
Expand Down Expand Up @@ -226,16 +229,9 @@ internal class Cache{{index}}
}
"""));

_ = Extract(content, applyOptimizations: false, out _);
_ = Extract(content, applyOptimizations: true, out _);

var baselineAllocatedBytes = MeasureAllocatedBytes(content, applyOptimizations: false);
var optimizedAllocatedBytes = MeasureAllocatedBytes(content, applyOptimizations: true);

Assert.True(
optimizedAllocatedBytes < baselineAllocatedBytes,
$"Expected wrapped lookup caching to allocate less: "
+ $"optimized={optimizedAllocatedBytes:N0}, baseline={baselineAllocatedBytes:N0} bytes.");
AssertNoMaterialAllocationRegression(
"wrapped lookup caching",
MeasureRepresentativeAllocatedBytes(content));
}

[Fact]
Expand Down Expand Up @@ -268,16 +264,9 @@ internal class Lexed{{index}}
Assert.Equal(0, baselineMetrics.LineStartStateReuseCount);
Assert.Equal(content.Split('\n').Length, optimizedMetrics.LineStartStateReuseCount);

_ = Extract(content, applyOptimizations: false, out _);
_ = Extract(content, applyOptimizations: true, out _);

var baselineAllocatedBytes = MeasureAllocatedBytes(content, applyOptimizations: false);
var optimizedAllocatedBytes = MeasureAllocatedBytes(content, applyOptimizations: true);

Assert.True(
optimizedAllocatedBytes < baselineAllocatedBytes,
$"Expected initial lexer-state reuse to allocate less: "
+ $"optimized={optimizedAllocatedBytes:N0}, baseline={baselineAllocatedBytes:N0} bytes.");
AssertNoMaterialAllocationRegression(
"initial lexer-state reuse",
MeasureRepresentativeAllocatedBytes(content));
}

[Fact]
Expand Down Expand Up @@ -317,15 +306,24 @@ public void Run(int value) {
optimizedMetrics.PropertyHeaderRegexAttemptCount
< baselineMetrics.PropertyHeaderRegexAttemptCount);

_ = Extract(content, applyOptimizations: false, out _);
_ = Extract(content, applyOptimizations: true, out _);
var baselineAllocatedBytes = MeasureAllocatedBytes(content, applyOptimizations: false);
var optimizedAllocatedBytes = MeasureAllocatedBytes(content, applyOptimizations: true);
AssertNoMaterialAllocationRegression(
"property structural gating",
MeasureRepresentativeAllocatedBytes(content));
}

Assert.True(
optimizedAllocatedBytes < baselineAllocatedBytes,
$"Expected property structural gating to allocate less: "
+ $"optimized={optimizedAllocatedBytes:N0}, baseline={baselineAllocatedBytes:N0} bytes.");
[Fact]
public void AllocationRegressionComparison_UsesMedianNoiseAllowanceAndRejectsMaterialIncrease_Issue5244()
{
Assert.Equal(300, SelectMedian([500, 100, 300, 200, 400]));
Assert.False(IsMaterialAllocationRegression(
baselineAllocatedBytes: 1_000_000,
optimizedAllocatedBytes: 1_000_000 + AllocationNoiseAllowanceBytes));
Assert.True(IsMaterialAllocationRegression(
baselineAllocatedBytes: 1_000_000,
optimizedAllocatedBytes: 1_000_001 + AllocationNoiseAllowanceBytes));
Assert.False(IsMaterialAllocationRegression(
baselineAllocatedBytes: 1_000_000,
optimizedAllocatedBytes: 900_000));
}

[Theory]
Expand Down Expand Up @@ -363,6 +361,56 @@ private static long MeasureAllocatedBytes(string content, bool applyOptimization
return GC.GetAllocatedBytesForCurrentThread() - allocatedBefore;
}

private static (long Baseline, long Optimized) MeasureRepresentativeAllocatedBytes(string content)
{
_ = Extract(content, applyOptimizations: false, out _);
_ = Extract(content, applyOptimizations: true, out _);

var baselineSamples = new long[AllocationSampleCount];
var optimizedSamples = new long[AllocationSampleCount];
for (var sampleIndex = 0; sampleIndex < AllocationSampleCount; sampleIndex++)
{
if ((sampleIndex & 1) == 0)
{
baselineSamples[sampleIndex] = MeasureAllocatedBytes(content, applyOptimizations: false);
optimizedSamples[sampleIndex] = MeasureAllocatedBytes(content, applyOptimizations: true);
}
else
{
optimizedSamples[sampleIndex] = MeasureAllocatedBytes(content, applyOptimizations: true);
baselineSamples[sampleIndex] = MeasureAllocatedBytes(content, applyOptimizations: false);
}
}

return (SelectMedian(baselineSamples), SelectMedian(optimizedSamples));
}

private static long SelectMedian(long[] samples)
{
Array.Sort(samples);
return samples[samples.Length / 2];
}

private static bool IsMaterialAllocationRegression(
long baselineAllocatedBytes,
long optimizedAllocatedBytes) =>
optimizedAllocatedBytes > baselineAllocatedBytes
&& optimizedAllocatedBytes - baselineAllocatedBytes > AllocationNoiseAllowanceBytes;

private static void AssertNoMaterialAllocationRegression(
string optimization,
(long Baseline, long Optimized) allocatedBytes)
{
var increase = Math.Max(0, allocatedBytes.Optimized - allocatedBytes.Baseline);
Assert.False(
IsMaterialAllocationRegression(allocatedBytes.Baseline, allocatedBytes.Optimized),
$"Expected {optimization} not to materially increase allocations: "
+ $"optimized median={allocatedBytes.Optimized:N0}, "
+ $"baseline median={allocatedBytes.Baseline:N0}, "
+ $"increase={increase:N0}, "
+ $"noise allowance={AllocationNoiseAllowanceBytes:N0} bytes.");
}

private static void AssertSymbolsEqual(
IReadOnlyList<SymbolRecord> expected,
IReadOnlyList<SymbolRecord> actual)
Expand Down
Loading