diff --git a/src/HotChocolate/Core/benchmarks/Execution.Abstractions.Benchmarks/HotChocolate.Execution.Abstractions.Benchmarks.csproj b/src/HotChocolate/Core/benchmarks/Execution.Abstractions.Benchmarks/HotChocolate.Execution.Abstractions.Benchmarks.csproj new file mode 100644 index 00000000000..be09cc91e25 --- /dev/null +++ b/src/HotChocolate/Core/benchmarks/Execution.Abstractions.Benchmarks/HotChocolate.Execution.Abstractions.Benchmarks.csproj @@ -0,0 +1,22 @@ + + + + Exe + net10.0 + true + Preview + HotChocolate.Execution.Abstractions.Benchmarks + HotChocolate.Execution.Abstractions.Benchmarks + true + false + + + + + + + + + + + diff --git a/src/HotChocolate/Core/benchmarks/Execution.Abstractions.Benchmarks/PathBenchmark.cs b/src/HotChocolate/Core/benchmarks/Execution.Abstractions.Benchmarks/PathBenchmark.cs new file mode 100644 index 00000000000..a3790da1270 --- /dev/null +++ b/src/HotChocolate/Core/benchmarks/Execution.Abstractions.Benchmarks/PathBenchmark.cs @@ -0,0 +1,89 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Jobs; + +namespace HotChocolate.Execution.Abstractions.Benchmarks; + +[MemoryDiagnoser] +[ShortRunJob(RuntimeMoniker.Net10_0)] +public class PathBenchmark +{ + private Path _depth5 = null!; + private Path _depth10 = null!; + private Path _depth20 = null!; + private Path _depth50 = null!; + + [GlobalSetup] + public void Setup() + { + _depth5 = BuildPath(5); + _depth10 = BuildPath(10); + _depth20 = BuildPath(20); + _depth50 = BuildPath(50); + } + + private static Path BuildPath(int depth) + { + var path = Path.Root; + for (var i = 0; i < depth; i++) + { + path = path.Append("field" + i); + if (i % 3 == 2) + { + path = path.Append(i); + } + } + return path; + } + + // --- Print benchmarks --- + + [Benchmark] + public string Print_Depth5() => _depth5.Print(); + + [Benchmark] + public string Print_Depth10() => _depth10.Print(); + + [Benchmark] + public string Print_Depth20() => _depth20.Print(); + + [Benchmark] + public string Print_Depth50() => _depth50.Print(); + + // --- ToList benchmarks --- + + [Benchmark] + public IReadOnlyList ToList_Depth5() => _depth5.ToList(); + + [Benchmark] + public IReadOnlyList ToList_Depth10() => _depth10.ToList(); + + [Benchmark] + public IReadOnlyList ToList_Depth20() => _depth20.ToList(); + + [Benchmark] + public IReadOnlyList ToList_Depth50() => _depth50.ToList(); + + // --- EnumerateSegments benchmarks --- + + [Benchmark] + public int EnumerateSegments_Depth10() + { + var count = 0; + foreach (var _ in _depth10.EnumerateSegments()) + { + count++; + } + return count; + } + + [Benchmark] + public int EnumerateSegments_Depth50() + { + var count = 0; + foreach (var _ in _depth50.EnumerateSegments()) + { + count++; + } + return count; + } +} diff --git a/src/HotChocolate/Core/benchmarks/Execution.Abstractions.Benchmarks/Program.cs b/src/HotChocolate/Core/benchmarks/Execution.Abstractions.Benchmarks/Program.cs new file mode 100644 index 00000000000..727a3c31032 --- /dev/null +++ b/src/HotChocolate/Core/benchmarks/Execution.Abstractions.Benchmarks/Program.cs @@ -0,0 +1,4 @@ +using BenchmarkDotNet.Running; +using HotChocolate.Execution.Abstractions.Benchmarks; + +BenchmarkSwitcher.FromAssembly(typeof(PathBenchmark).Assembly).Run(args); diff --git a/src/HotChocolate/Core/src/Execution.Abstractions/Path.cs b/src/HotChocolate/Core/src/Execution.Abstractions/Path.cs index 2cdb1bca0b1..506e38d3417 100644 --- a/src/HotChocolate/Core/src/Execution.Abstractions/Path.cs +++ b/src/HotChocolate/Core/src/Execution.Abstractions/Path.cs @@ -1,5 +1,3 @@ -using System.Text; - namespace HotChocolate; /// @@ -136,7 +134,8 @@ public string Print() return "/"; } - var sb = new StringBuilder(); + // On first pass we calculate the total length + var totalLength = 0; var current = this; while (current is not RootPathSegment) @@ -144,15 +143,11 @@ public string Print() switch (current) { case IndexerPathSegment indexer: - var numberValue = indexer.Index.ToString(); - sb.Insert(0, '['); - sb.Insert(1, numberValue); - sb.Insert(1 + numberValue.Length, ']'); + totalLength += 2 + CountDigits(indexer.Index); // '[' + digits + ']' break; case NamePathSegment name: - sb.Insert(0, '/'); - sb.Insert(1, name.Name); + totalLength += 1 + name.Name.Length; // '/' + name break; default: @@ -162,7 +157,60 @@ public string Print() current = current.Parent; } - return sb.ToString(); + // On second pass we fill from right to left using string.Create + return string.Create(totalLength, this, static (span, path) => + { + var pos = span.Length; + var current = path; + + while (current is not RootPathSegment) + { + switch (current) + { + case IndexerPathSegment indexer: + span[--pos] = ']'; + var idx = indexer.Index; + if (idx == 0) + { + span[--pos] = '0'; + } + else + { + while (idx > 0) + { + span[--pos] = (char)('0' + (idx % 10)); + idx /= 10; + } + } + span[--pos] = '['; + break; + + case NamePathSegment name: + pos -= name.Name.Length; + name.Name.AsSpan().CopyTo(span[pos..]); + span[--pos] = '/'; + break; + } + + current = current.Parent; + } + }); + + static int CountDigits(int n) + { + if (n == 0) + { + return 1; + } + + var count = 0; + while (n > 0) + { + count++; + n /= 10; + } + return count; + } } /// @@ -178,19 +226,20 @@ public IReadOnlyList ToList() return Array.Empty(); } - var stack = new List(); + var result = new object[Length]; var current = this; + var i = Length - 1; while (!current.IsRoot) { switch (current) { case IndexerPathSegment indexer: - stack.Insert(0, indexer.Index); + result[i--] = indexer.Index; break; case NamePathSegment name: - stack.Insert(0, name.Name); + result[i--] = name.Name; break; default: @@ -200,7 +249,7 @@ public IReadOnlyList ToList() current = current.Parent; } - return stack; + return result; } /// @@ -247,26 +296,31 @@ public void ToList(Span path) } public IEnumerable EnumerateSegments() - => EnumerateSegmentsBackwards().Reverse(); - - private IEnumerable EnumerateSegmentsBackwards() { if (IsRoot) { - yield break; + return []; } + var segments = new Path[Length]; var current = this; + var i = Length - 1; while (!current.IsRoot) { - yield return current; + segments[i--] = current; current = current.Parent; } + + return segments; } - /// Returns a string that represents the current . - /// A string that represents the current . + /// + /// Returns a string that represents the current . + /// + /// + /// A string that represents the current . + /// public override string ToString() => Print(); public virtual bool Equals(Path? other) @@ -323,7 +377,7 @@ public int CompareTo(Path? other) return 1; } - // 1. Align to the same depth + // First we align the paths to the the same depth var a = this; var b = other; @@ -339,14 +393,13 @@ public int CompareTo(Path? other) b = b.Skip(lenB - lenA); } - // 2. Walk aligned segments from root to leaf + // Then we walk aligned segments from root to leaf var cmp = CompareFromRoot(a, b); if (cmp != 0) { return cmp; } - // 3. Same segments → shorter path wins return Length.CompareTo(other.Length); }