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 +);