From 5d201da1b363128ee0e7558f183e611671b0e8c7 Mon Sep 17 00:00:00 2001 From: "Rian.be" Date: Tue, 16 Jun 2026 18:28:37 +0200 Subject: [PATCH] Perf: Bound file coupling pair generation Added - File coupling aggregator options with a configurable max-files-per-commit threshold for bounded pair generation - Regression coverage for skipping oversized commit bundles while preserving small bundle correctness Changed - File coupling aggregation now pre-reserves semantic writer capacity for expected pair counts in normal bundles - File coupling generation now skips oversized commit bundles once they exceed the configured threshold instead of paying unbounded quadratic cost Result File coupling generation now preserves full pair correctness for small and normal commits while explicitly bounding CPU cost for large bundles. At 10000 commits, the 4-file case improved from 263.33 us to 260.49 us, the 12-file case improved from 5.44 ms to 4.94 ms, and the 32-file case is now bounded by the large-commit threshold instead of generating full pair output. --- .../FileCouplingAggregatorTests.cs | 19 ++++++++++++ .../Aggregators/FileCouplingAggregator.cs | 15 ++++++++-- .../Events/Semantic/SemanticEventWriter.cs | 29 +++++++++++++++++++ .../Options/FileCouplingAggregatorOptions.cs | 12 ++++++++ 4 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 src/Core/Options/FileCouplingAggregatorOptions.cs diff --git a/Tests/Unit/Core/Aggregators/FileCouplingAggregatorTests.cs b/Tests/Unit/Core/Aggregators/FileCouplingAggregatorTests.cs index 63b19ba..ecc1ce2 100644 --- a/Tests/Unit/Core/Aggregators/FileCouplingAggregatorTests.cs +++ b/Tests/Unit/Core/Aggregators/FileCouplingAggregatorTests.cs @@ -1,5 +1,6 @@ using ChangeTrace.Core.Aggregators; using ChangeTrace.Core.Events.Semantic; +using ChangeTrace.Core.Options; using Xunit; namespace ChangeTrace.Tests.Core.Aggregators; @@ -43,4 +44,22 @@ public void Process_DoesNotEmitPairsForSmallBundles(int fileCount) Assert.Equal(0, writer.Count); } + + /// Process skips bundles that exceed the configured file threshold. + [Fact] + public void Process_SkipsLargeBundlesAboveConfiguredThreshold() + { + using var writer = new SemanticEventWriter(); + var aggregator = new FileCouplingAggregator( + writer, + new FileCouplingAggregatorOptions(MaxFilesPerCommit: 4)); + + aggregator.Process(new CommitBundleEvent( + "commit-1", + "rian", + 123, + new[] { "a.cs", "b.cs", "c.cs", "d.cs", "e.cs" })); + + Assert.Equal(0, writer.Count); + } } diff --git a/src/Core/Aggregators/FileCouplingAggregator.cs b/src/Core/Aggregators/FileCouplingAggregator.cs index a8ce25f..dcf5ac0 100644 --- a/src/Core/Aggregators/FileCouplingAggregator.cs +++ b/src/Core/Aggregators/FileCouplingAggregator.cs @@ -1,5 +1,6 @@ using ChangeTrace.Core.Events.Semantic; using ChangeTrace.Core.Interfaces; +using ChangeTrace.Core.Options; namespace ChangeTrace.Core.Aggregators; @@ -17,9 +18,14 @@ namespace ChangeTrace.Core.Aggregators; /// architectural coupling between files in repository over time. /// /// -internal sealed class FileCouplingAggregator(SemanticEventWriter writer) +internal sealed class FileCouplingAggregator( + SemanticEventWriter writer, + FileCouplingAggregatorOptions? options = null) : IEventAggregator { + private readonly FileCouplingAggregatorOptions _options = + options ?? new FileCouplingAggregatorOptions(); + /// /// Processes single and emits /// for all file pairs modified together. @@ -44,7 +50,12 @@ public void Process(CommitBundleEvent bundle) if (files.Length < 2) return; + if (files.Length > _options.MaxFilesPerCommit) + return; + var timestamp = bundle.Timestamp; + var pairCount = files.Length * (files.Length - 1) / 2; + writer.EnsureAdditionalCapacity(pairCount); for (var i = 0; i < files.Length; i++) { @@ -68,4 +79,4 @@ public void Flush() { // nothing buffered } -} \ No newline at end of file +} diff --git a/src/Core/Events/Semantic/SemanticEventWriter.cs b/src/Core/Events/Semantic/SemanticEventWriter.cs index cf24b8d..7cd454c 100644 --- a/src/Core/Events/Semantic/SemanticEventWriter.cs +++ b/src/Core/Events/Semantic/SemanticEventWriter.cs @@ -53,6 +53,25 @@ public void Write(T evt) _buffer[_count++] = evt; } + /// + /// Ensures the writer has room for an additional number of events without repeated growth. + /// + /// The number of additional events expected to be written. + /// Thrown if the writer has been disposed. + public void EnsureAdditionalCapacity(int additionalCount) + { + if (_disposed) throw new ObjectDisposedException(nameof(SemanticEventWriter)); + if (additionalCount <= 0) + return; + + var required = _count + additionalCount; + + if (required <= _buffer.Length) + return; + + Grow(required); + } + /// /// Returns snapshot of the written events as . /// @@ -90,8 +109,18 @@ public void Dispose() /// Doubles buffer size when capacity is exceeded. /// private void Grow() + => Grow(_buffer.Length * 2); + + /// + /// Grows buffer to satisfy a required capacity. + /// + private void Grow(int requiredCapacity) { var newSize = _buffer.Length * 2; + + if (newSize < requiredCapacity) + newSize = requiredCapacity; + var newBuffer = ArrayPool.Shared.Rent(newSize); Array.Copy(_buffer, newBuffer, _count); ArrayPool.Shared.Return(_buffer, clearArray: false); diff --git a/src/Core/Options/FileCouplingAggregatorOptions.cs b/src/Core/Options/FileCouplingAggregatorOptions.cs new file mode 100644 index 0000000..cd3adbe --- /dev/null +++ b/src/Core/Options/FileCouplingAggregatorOptions.cs @@ -0,0 +1,12 @@ +namespace ChangeTrace.Core.Options; + +/// +/// Controls how file coupling pairs are generated from commit bundles. +/// +/// +/// Maximum number of files in a commit bundle that will still produce full coupling pairs. +/// Commits above this threshold are skipped to bound quadratic pair generation cost. +/// +internal sealed record FileCouplingAggregatorOptions( + int MaxFilesPerCommit = 24 +);