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
19 changes: 19 additions & 0 deletions Tests/Unit/Core/Aggregators/FileCouplingAggregatorTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using ChangeTrace.Core.Aggregators;
using ChangeTrace.Core.Events.Semantic;
using ChangeTrace.Core.Options;
using Xunit;

namespace ChangeTrace.Tests.Core.Aggregators;
Expand Down Expand Up @@ -43,4 +44,22 @@ public void Process_DoesNotEmitPairsForSmallBundles(int fileCount)

Assert.Equal(0, writer.Count);
}

/// <summary>Process skips bundles that exceed the configured file threshold.</summary>
[Fact]
public void Process_SkipsLargeBundlesAboveConfiguredThreshold()
{
using var writer = new SemanticEventWriter<FileCouplingEvent>();
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);
}
}
15 changes: 13 additions & 2 deletions src/Core/Aggregators/FileCouplingAggregator.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using ChangeTrace.Core.Events.Semantic;
using ChangeTrace.Core.Interfaces;
using ChangeTrace.Core.Options;

namespace ChangeTrace.Core.Aggregators;

Expand All @@ -17,9 +18,14 @@ namespace ChangeTrace.Core.Aggregators;
/// architectural coupling between files in repository over time.
/// </para>
/// </remarks>
internal sealed class FileCouplingAggregator(SemanticEventWriter<FileCouplingEvent> writer)
internal sealed class FileCouplingAggregator(
SemanticEventWriter<FileCouplingEvent> writer,
FileCouplingAggregatorOptions? options = null)
: IEventAggregator<CommitBundleEvent>
{
private readonly FileCouplingAggregatorOptions _options =
options ?? new FileCouplingAggregatorOptions();

/// <summary>
/// Processes single <see cref="CommitBundleEvent"/> and emits
/// <see cref="FileCouplingEvent"/> for all file pairs modified together.
Expand All @@ -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++)
{
Expand All @@ -68,4 +79,4 @@ public void Flush()
{
// nothing buffered
}
}
}
29 changes: 29 additions & 0 deletions src/Core/Events/Semantic/SemanticEventWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,25 @@ public void Write(T evt)
_buffer[_count++] = evt;
}

/// <summary>
/// Ensures the writer has room for an additional number of events without repeated growth.
/// </summary>
/// <param name="additionalCount">The number of additional events expected to be written.</param>
/// <exception cref="ObjectDisposedException">Thrown if the writer has been disposed.</exception>
public void EnsureAdditionalCapacity(int additionalCount)
{
if (_disposed) throw new ObjectDisposedException(nameof(SemanticEventWriter<T>));
if (additionalCount <= 0)
return;

var required = _count + additionalCount;

if (required <= _buffer.Length)
return;

Grow(required);
}

/// <summary>
/// Returns snapshot of the written events as <see cref="ReadOnlyMemory{T}"/>.
/// </summary>
Expand Down Expand Up @@ -90,8 +109,18 @@ public void Dispose()
/// Doubles buffer size when capacity is exceeded.
/// </summary>
private void Grow()
=> Grow(_buffer.Length * 2);

/// <summary>
/// Grows buffer to satisfy a required capacity.
/// </summary>
private void Grow(int requiredCapacity)
{
var newSize = _buffer.Length * 2;

if (newSize < requiredCapacity)
newSize = requiredCapacity;

var newBuffer = ArrayPool<T>.Shared.Rent(newSize);
Array.Copy(_buffer, newBuffer, _count);
ArrayPool<T>.Shared.Return(_buffer, clearArray: false);
Expand Down
12 changes: 12 additions & 0 deletions src/Core/Options/FileCouplingAggregatorOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace ChangeTrace.Core.Options;

/// <summary>
/// Controls how file coupling pairs are generated from commit bundles.
/// </summary>
/// <param name="MaxFilesPerCommit">
/// 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.
/// </param>
internal sealed record FileCouplingAggregatorOptions(
int MaxFilesPerCommit = 24
);
Loading