Skip to content
Draft
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
18 changes: 18 additions & 0 deletions DiffPlex.Benchmarks/DiffPlex.Benchmarks.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">

<ItemGroup>
<ProjectReference Include="..\DiffPlex\DiffPlex.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.13.12" />
</ItemGroup>

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

</Project>
227 changes: 227 additions & 0 deletions DiffPlex.Benchmarks/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Order;
using BenchmarkDotNet.Running;
using DiffPlex.Chunkers;
using DiffPlex.DiffBuilder;
using DiffPlex.DiffBuilder.Model;
using DiffPlex.Model;

namespace DiffPlex.Benchmarks;

internal static class Program
{
public static void Main(string[] args) => BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);
}

[MemoryDiagnoser]
[SimpleJob(RuntimeMoniker.Net80, launchCount: 1, warmupCount: 1, iterationCount: 3)]
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
public class CoreDiffBenchmarks
{
private static readonly char[] WordSeparators = { ' ', '\t', '.', '(', ')', '{', '}', ',', '!', '?', ';' };

private readonly Differ differ = new();
private readonly InlineDiffBuilder inlineDiffBuilder;
private readonly SideBySideDiffBuilder sideBySideDiffBuilder;
private BenchmarkInput input = null!;

public CoreDiffBenchmarks()
{
inlineDiffBuilder = new InlineDiffBuilder(differ);
sideBySideDiffBuilder = new SideBySideDiffBuilder(differ);
}

[Params(InputScenario.Identical, InputScenario.ManySmallChanges, InputScenario.FewLargeChanges, InputScenario.CompletelyDifferent, InputScenario.LongCommonSubsequence)]
public InputScenario Scenario { get; set; }

[Params(InputSize.Small, InputSize.Large)]
public InputSize Size { get; set; }

[GlobalSetup]
public void GlobalSetup()
{
input = BenchmarkInput.Create(Scenario, Size);
}

[Benchmark(Baseline = true)]
public DiffResult CreateDiffs_LineChunker()
{
return differ.CreateDiffs(input.OldText, input.NewText, ignoreWhiteSpace: false, ignoreCase: false, LineChunker.Instance);
}

[Benchmark]
public DiffResult CreateLineDiffs()
{
return differ.CreateLineDiffs(input.OldText, input.NewText, ignoreWhitespace: false);
}

[Benchmark]
public DiffResult CreateWordDiffs()
{
return differ.CreateWordDiffs(input.OldText, input.NewText, ignoreWhitespace: false, WordSeparators);
}

[Benchmark]
public DiffResult CreateCharacterDiffs()
{
return differ.CreateCharacterDiffs(input.CharacterOldText, input.CharacterNewText, ignoreWhitespace: false);
}

[Benchmark]
public DiffPaneModel BuildInlineDiffModel()
{
return inlineDiffBuilder.BuildDiffModel(input.OldText, input.NewText, ignoreWhitespace: false, ignoreCase: false, LineChunker.Instance);
}

[Benchmark]
public SideBySideDiffModel BuildSideBySideDiffModel()
{
return sideBySideDiffBuilder.BuildDiffModel(input.OldText, input.NewText, ignoreWhitespace: false);
}
}

public enum InputScenario
{
Identical,
ManySmallChanges,
FewLargeChanges,
CompletelyDifferent,
LongCommonSubsequence
}

public enum InputSize
{
Small,
Large
}

internal sealed record BenchmarkInput(string OldText, string NewText, string CharacterOldText, string CharacterNewText)
{
public static BenchmarkInput Create(InputScenario scenario, InputSize size)
{
int lineCount = size == InputSize.Small ? 25 : 1_500;
int characterCount = size == InputSize.Small ? 250 : 1_000;

var oldLines = CreateLines(lineCount, "old");
var newLines = scenario switch
{
InputScenario.Identical => oldLines.ToArray(),
InputScenario.ManySmallChanges => ManySmallChanges(oldLines),
InputScenario.FewLargeChanges => FewLargeChanges(oldLines),
InputScenario.CompletelyDifferent => CreateLines(lineCount, "new"),
InputScenario.LongCommonSubsequence => LongCommonSubsequence(oldLines),
_ => throw new ArgumentOutOfRangeException(nameof(scenario), scenario, null)
};

string oldText = string.Join('\n', oldLines);
string newText = string.Join('\n', newLines);

string characterOldText = CreateCharacterText(characterCount, "abcdefghij");
string characterNewText = scenario switch
{
InputScenario.Identical => characterOldText,
InputScenario.ManySmallChanges => ReplaceEvery(characterOldText, every: 19, 'Z'),
InputScenario.FewLargeChanges => ReplaceRange(characterOldText, characterCount / 3, characterCount / 5, 'X'),
InputScenario.CompletelyDifferent => CreateCharacterText(characterCount, "ZYXWVUTSRQ"),
// Keep the character variant bounded while still preserving a long common subsequence around an insertion.
InputScenario.LongCommonSubsequence => characterOldText.Insert(characterCount / 2, CreateCharacterText(characterCount / 10, "lcs")),
_ => throw new ArgumentOutOfRangeException(nameof(scenario), scenario, null)
};

return new BenchmarkInput(oldText, newText, characterOldText, characterNewText);
}

private static string[] CreateLines(int lineCount, string prefix)
{
var lines = new string[lineCount];
for (int i = 0; i < lines.Length; i++)
{
lines[i] = $"{prefix} line {i:D5}: common-token-{i % 17} value-{(i * 31) % 997} words for diffing";
}

return lines;
}

private static string[] ManySmallChanges(string[] oldLines)
{
var newLines = oldLines.ToArray();
for (int i = 4; i < newLines.Length; i += 10)
{
newLines[i] = newLines[i] + " changed";
}

return newLines;
}

private static string[] FewLargeChanges(string[] oldLines)
{
var newLines = oldLines.ToArray();
int blockLength = Math.Max(3, oldLines.Length / 5);
int start = Math.Max(0, (oldLines.Length - blockLength) / 2);

for (int i = 0; i < blockLength; i++)
{
newLines[start + i] = $"replacement block line {i:D5}: updated content with different tokens {i % 11}";
}

return newLines;
}

private static string[] LongCommonSubsequence(string[] oldLines)
{
var newLines = new List<string>(oldLines.Length + Math.Max(1, oldLines.Length / 20));
for (int i = 0; i < oldLines.Length; i++)
{
if (i % 50 == 0)
{
newLines.Add($"inserted anchor line {i:D5}: preserves a long common subsequence around edits");
}

if (i % 75 != 0)
{
newLines.Add(oldLines[i]);
}
}

return newLines.ToArray();
}

private static string CreateCharacterText(int characterCount, string pattern)
{
if (characterCount <= 0)
{
return string.Empty;
}

var builder = new System.Text.StringBuilder(characterCount);
while (builder.Length < characterCount)
{
builder.Append(pattern);
}

return builder.ToString(0, characterCount);
}

private static string ReplaceEvery(string text, int every, char replacement)
{
var chars = text.ToCharArray();
for (int i = every - 1; i < chars.Length; i += every)
{
chars[i] = replacement;
}

return new string(chars);
}

private static string ReplaceRange(string text, int start, int length, char replacement)
{
var chars = text.ToCharArray();
for (int i = start; i < Math.Min(chars.Length, start + length); i++)
{
chars[i] = replacement;
}

return new string(chars);
}
}
18 changes: 18 additions & 0 deletions DiffPlex.Benchmarks/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# DiffPlex benchmarks

This project contains BenchmarkDotNet benchmarks for the core DiffPlex diffing APIs and builders. The scenarios cover identical inputs, many small edits, a few large edits, completely different inputs, and inputs with long common subsequences at small and large sizes.

Run the full suite from this directory:

```console
dotnet run -c Release
```

Useful shorter commands while iterating:

```console
dotnet run -c Release -- --filter "*CreateLineDiffs*"
dotnet run -c Release -- --filter "*Large*" --exporters github csv
```

BenchmarkDotNet writes detailed reports under `BenchmarkDotNet.Artifacts/results` by default. Committed before/after summaries for this optimization effort are tracked in `../benchmarks/RESULTS.md`.
18 changes: 18 additions & 0 deletions DiffPlex.sln
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DiffPlex.App", "DiffPlex.Ap
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DiffPlex.Blazor", "DiffPlex.Blazor\DiffPlex.Blazor.csproj", "{B1234567-89AB-CDEF-0123-456789ABCDEF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DiffPlex.Benchmarks", "DiffPlex.Benchmarks\DiffPlex.Benchmarks.csproj", "{65C36D68-4F3F-43E9-A98A-C37F9634358B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -196,6 +198,22 @@ Global
{B1234567-89AB-CDEF-0123-456789ABCDEF}.Release|arm64.ActiveCfg = Release|Any CPU
{B1234567-89AB-CDEF-0123-456789ABCDEF}.Release|x64.ActiveCfg = Release|Any CPU
{B1234567-89AB-CDEF-0123-456789ABCDEF}.Release|x86.ActiveCfg = Release|Any CPU
{65C36D68-4F3F-43E9-A98A-C37F9634358B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{65C36D68-4F3F-43E9-A98A-C37F9634358B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{65C36D68-4F3F-43E9-A98A-C37F9634358B}.Debug|arm64.ActiveCfg = Debug|Any CPU
{65C36D68-4F3F-43E9-A98A-C37F9634358B}.Debug|arm64.Build.0 = Debug|Any CPU
{65C36D68-4F3F-43E9-A98A-C37F9634358B}.Debug|x64.ActiveCfg = Debug|Any CPU
{65C36D68-4F3F-43E9-A98A-C37F9634358B}.Debug|x64.Build.0 = Debug|Any CPU
{65C36D68-4F3F-43E9-A98A-C37F9634358B}.Debug|x86.ActiveCfg = Debug|Any CPU
{65C36D68-4F3F-43E9-A98A-C37F9634358B}.Debug|x86.Build.0 = Debug|Any CPU
{65C36D68-4F3F-43E9-A98A-C37F9634358B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{65C36D68-4F3F-43E9-A98A-C37F9634358B}.Release|Any CPU.Build.0 = Release|Any CPU
{65C36D68-4F3F-43E9-A98A-C37F9634358B}.Release|arm64.ActiveCfg = Release|Any CPU
{65C36D68-4F3F-43E9-A98A-C37F9634358B}.Release|arm64.Build.0 = Release|Any CPU
{65C36D68-4F3F-43E9-A98A-C37F9634358B}.Release|x64.ActiveCfg = Release|Any CPU
{65C36D68-4F3F-43E9-A98A-C37F9634358B}.Release|x64.Build.0 = Release|Any CPU
{65C36D68-4F3F-43E9-A98A-C37F9634358B}.Release|x86.ActiveCfg = Release|Any CPU
{65C36D68-4F3F-43E9-A98A-C37F9634358B}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
3 changes: 1 addition & 2 deletions DiffPlex/DiffBuilder/InlineDiffBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,7 @@ public DiffPaneModel BuildDiffModel(string oldText, string newText)

public DiffPaneModel BuildDiffModel(string oldText, string newText, bool ignoreWhitespace)
{
var chunker = new LineChunker();
return BuildDiffModel(oldText, newText, ignoreWhitespace, false, chunker);
return BuildDiffModel(oldText, newText, ignoreWhitespace, false, LineChunker.Instance);
}

public DiffPaneModel BuildDiffModel(string oldText, string newText, bool ignoreWhitespace, bool ignoreCase, IChunker chunker)
Expand Down
14 changes: 12 additions & 2 deletions DiffPlex/DiffBuilder/Model/DiffPaneModel.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.Collections.Generic;
using System.Linq;

namespace DiffPlex.DiffBuilder.Model
{
Expand All @@ -9,7 +8,18 @@ public class DiffPaneModel

public bool HasDifferences
{
get { return Lines.Any(x => x.Type != ChangeType.Unchanged); }
get
{
for (int i = 0; i < Lines.Count; i++)
{
if (Lines[i].Type != ChangeType.Unchanged)
{
return true;
}
}

return false;
}
}

public DiffPaneModel()
Expand Down
Loading
Loading