Skip to content

Commit 6730bf1

Browse files
authored
feat: add JobOrderPolicy option to sort jobs numeric order (#2770)
1 parent fe3b31f commit 6730bf1

File tree

5 files changed

+258
-8
lines changed

5 files changed

+258
-8
lines changed

src/BenchmarkDotNet/Attributes/OrdererAttribute.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@ public class OrdererAttribute : Attribute, IConfigSource
99
{
1010
public OrdererAttribute(
1111
SummaryOrderPolicy summaryOrderPolicy = SummaryOrderPolicy.Default,
12-
MethodOrderPolicy methodOrderPolicy = MethodOrderPolicy.Declared)
12+
MethodOrderPolicy methodOrderPolicy = MethodOrderPolicy.Declared,
13+
JobOrderPolicy jobOrderPolicy = JobOrderPolicy.Default)
1314
{
14-
Config = ManualConfig.CreateEmpty().WithOrderer(new DefaultOrderer(summaryOrderPolicy, methodOrderPolicy));
15+
Config = ManualConfig.CreateEmpty().WithOrderer(new DefaultOrderer(summaryOrderPolicy, methodOrderPolicy, jobOrderPolicy));
1516
}
1617

1718
public IConfig Config { get; }

src/BenchmarkDotNet/Jobs/JobComparer.cs

Lines changed: 89 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,23 @@
1-
using System;
1+
using BenchmarkDotNet.Characteristics;
2+
using BenchmarkDotNet.Order;
3+
using System;
24
using System.Collections.Generic;
3-
using BenchmarkDotNet.Characteristics;
45

56
namespace BenchmarkDotNet.Jobs
67
{
78
internal class JobComparer : IComparer<Job>, IEqualityComparer<Job>
89
{
9-
public static readonly JobComparer Instance = new JobComparer();
10+
private readonly IComparer<string> Comparer;
11+
12+
public static readonly JobComparer Instance = new JobComparer(JobOrderPolicy.Default);
13+
public static readonly JobComparer Numeric = new JobComparer(JobOrderPolicy.Numeric);
14+
15+
public JobComparer(JobOrderPolicy jobOrderPolicy = JobOrderPolicy.Default)
16+
{
17+
Comparer = jobOrderPolicy == JobOrderPolicy.Default
18+
? StringComparer.Ordinal
19+
: new NumericStringComparer(); // TODO: Use `StringComparer.Create(CultureInfo.InvariantCulture, CompareOptions.NumericOrdering)` for .NET10 or greater.
20+
}
1021

1122
public int Compare(Job x, Job y)
1223
{
@@ -39,7 +50,7 @@ public int Compare(Job x, Job y)
3950
continue;
4051
}
4152

42-
int compare = string.CompareOrdinal(
53+
int compare = Comparer.Compare(
4354
presenter.ToPresentation(x, characteristic),
4455
presenter.ToPresentation(y, characteristic));
4556
if (compare != 0)
@@ -52,5 +63,79 @@ public int Compare(Job x, Job y)
5263
public bool Equals(Job x, Job y) => Compare(x, y) == 0;
5364

5465
public int GetHashCode(Job obj) => obj.Id.GetHashCode();
66+
67+
internal class NumericStringComparer : IComparer<string>
68+
{
69+
public int Compare(string? x, string? y)
70+
{
71+
if (ReferenceEquals(x, y)) return 0;
72+
if (x == null) return -1;
73+
if (y == null) return 1;
74+
75+
ReadOnlySpan<char> spanX = x.AsSpan();
76+
ReadOnlySpan<char> spanY = y.AsSpan();
77+
78+
int i = 0, j = 0;
79+
80+
while (i < spanX.Length && j < spanY.Length)
81+
{
82+
char cx = spanX[i];
83+
char cy = spanY[j];
84+
85+
if (!char.IsDigit(cx) || !char.IsDigit(cy))
86+
{
87+
int cmp = cx.CompareTo(cy);
88+
if (cmp != 0)
89+
return cmp;
90+
91+
i++;
92+
j++;
93+
continue;
94+
}
95+
96+
int ixStart = i;
97+
int iyStart = j;
98+
99+
// Skip leading zeros
100+
while (ixStart < spanX.Length && spanX[ixStart] == '0') ixStart++;
101+
while (iyStart < spanY.Length && spanY[iyStart] == '0') iyStart++;
102+
103+
int ix = ixStart;
104+
int iy = iyStart;
105+
106+
// Skip digits
107+
while (ix < spanX.Length && char.IsDigit(spanX[ix])) ix++;
108+
while (iy < spanY.Length && char.IsDigit(spanY[iy])) iy++;
109+
110+
int lenX = ix - ixStart;
111+
int lenY = iy - iyStart;
112+
113+
// Compare by digits length
114+
if (lenX != lenY)
115+
return lenX.CompareTo(lenY);
116+
117+
// Compare digits
118+
for (int k = 0; k < lenX; k++)
119+
{
120+
int cmp = spanX[ixStart + k].CompareTo(spanY[iyStart + k]);
121+
if (cmp != 0)
122+
return cmp;
123+
}
124+
125+
// Compare by leading zeros
126+
int leadingZerosX = ixStart - i;
127+
int leadingZerosY = iyStart - j;
128+
if (leadingZerosX != leadingZerosY)
129+
return 0; // Leading zero differences are ignored (`CompareOptions.NumericOrdering` behavior of .NET)
130+
131+
// Move to the next character after the digits
132+
i = ix;
133+
j = iy;
134+
}
135+
136+
// Compare remaining chars
137+
return (spanX.Length - i).CompareTo(spanY.Length - j);
138+
}
139+
}
55140
}
56141
}

src/BenchmarkDotNet/Order/DefaultOrderer.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,22 @@ public class DefaultOrderer : IOrderer
1919

2020
private readonly IComparer<string[]> categoryComparer = CategoryComparer.Instance;
2121
private readonly IComparer<ParameterInstances> paramsComparer = ParameterComparer.Instance;
22-
private readonly IComparer<Job> jobComparer = JobComparer.Instance;
22+
private readonly IComparer<Job> jobComparer;
2323
private readonly IComparer<Descriptor> targetComparer;
2424

2525
public SummaryOrderPolicy SummaryOrderPolicy { get; }
2626
public MethodOrderPolicy MethodOrderPolicy { get; }
2727

2828
public DefaultOrderer(
2929
SummaryOrderPolicy summaryOrderPolicy = SummaryOrderPolicy.Default,
30-
MethodOrderPolicy methodOrderPolicy = MethodOrderPolicy.Declared)
30+
MethodOrderPolicy methodOrderPolicy = MethodOrderPolicy.Declared,
31+
JobOrderPolicy jobOrderPolicy = JobOrderPolicy.Default)
3132
{
3233
SummaryOrderPolicy = summaryOrderPolicy;
3334
MethodOrderPolicy = methodOrderPolicy;
35+
jobComparer = jobOrderPolicy == JobOrderPolicy.Default
36+
? JobComparer.Instance
37+
: JobComparer.Numeric;
3438
targetComparer = new DescriptorComparer(methodOrderPolicy);
3539
}
3640

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
namespace BenchmarkDotNet.Order;
2+
3+
public enum JobOrderPolicy
4+
{
5+
/// <summary>
6+
/// Compare job characteristics in ordinal order.
7+
/// </summary>
8+
Default,
9+
10+
/// <summary>
11+
/// Compare job characteristics in numeric order.
12+
/// </summary>
13+
Numeric,
14+
}
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
using BenchmarkDotNet.Environments;
2+
using BenchmarkDotNet.Jobs;
3+
using BenchmarkDotNet.Toolchains;
4+
using BenchmarkDotNet.Toolchains.CsProj;
5+
using System.Linq;
6+
using Xunit;
7+
8+
namespace BenchmarkDotNet.Tests.Order;
9+
10+
public class JobOrderTests
11+
{
12+
[Fact]
13+
public void TestJobOrders_ByJobId()
14+
{
15+
// Arrange
16+
Job[] jobs =
17+
[
18+
Job.Dry.WithToolchain(CsProjCoreToolchain.NetCoreApp80)
19+
.WithRuntime(CoreRuntime.Core80)
20+
.WithId("v1.4.1"),
21+
Job.Dry.WithToolchain(CsProjCoreToolchain.NetCoreApp90)
22+
.WithRuntime(CoreRuntime.Core90)
23+
.WithId("v1.4.10"),
24+
Job.Dry.WithToolchain(CsProjCoreToolchain.NetCoreApp10_0)
25+
.WithRuntime(CoreRuntime.Core10_0)
26+
.WithId("v1.4.2"),
27+
];
28+
29+
// Verify jobs are sorted by JobId's ordinal order.
30+
{
31+
// Act
32+
var comparer = JobComparer.Instance;
33+
var results = jobs.OrderBy(x => x, comparer)
34+
.Select(x => x.Job.Id)
35+
.ToArray();
36+
37+
// Assert
38+
Assert.Equal(["v1.4.1", "v1.4.10", "v1.4.2"], results);
39+
}
40+
41+
// Verify jobs are sorted by JobId's numeric order.
42+
{
43+
// Act
44+
var comparer = JobComparer.Numeric;
45+
var results = jobs.OrderBy(d => d, comparer)
46+
.Select(x => x.Job.Id)
47+
.ToArray();
48+
// Assert
49+
Assert.Equal(["v1.4.1", "v1.4.2", "v1.4.10"], results);
50+
}
51+
}
52+
53+
[Fact]
54+
public void TestJobOrders_ByRuntime()
55+
{
56+
// Arrange
57+
Job[] jobs =
58+
[
59+
Job.Dry.WithToolchain(CsProjCoreToolchain.NetCoreApp10_0)
60+
.WithRuntime(CoreRuntime.Core80),
61+
Job.Dry.WithToolchain(CsProjCoreToolchain.NetCoreApp90)
62+
.WithRuntime(CoreRuntime.Core90),
63+
Job.Dry.WithToolchain(CsProjCoreToolchain.NetCoreApp80)
64+
.WithRuntime(CoreRuntime.Core10_0),
65+
];
66+
67+
// Act
68+
// Verify jobs are sorted by Runtime's numeric order.
69+
var results = jobs.OrderBy(d => d, JobComparer.Numeric)
70+
.Select(x => x.Job.Environment.GetRuntime().Name)
71+
.ToArray();
72+
73+
// Assert
74+
var expected = new[]
75+
{
76+
CoreRuntime.Core80.Name,
77+
CoreRuntime.Core90.Name,
78+
CoreRuntime.Core10_0.Name
79+
};
80+
Assert.Equal(expected, results);
81+
}
82+
83+
[Fact]
84+
public void TestJobOrders_ByToolchain()
85+
{
86+
// Arrange
87+
Job[] jobs =
88+
[
89+
Job.Dry.WithToolchain(CsProjCoreToolchain.NetCoreApp10_0),
90+
Job.Dry.WithToolchain(CsProjCoreToolchain.NetCoreApp90),
91+
Job.Dry.WithToolchain(CsProjCoreToolchain.NetCoreApp80),
92+
];
93+
94+
// Act
95+
// Verify jobs are sorted by Toolchain's numeric order.
96+
var results = jobs.OrderBy(d => d, JobComparer.Numeric)
97+
.Select(x => x.Job.GetToolchain().Name)
98+
.ToArray();
99+
100+
// Assert
101+
var expected = new[]
102+
{
103+
CsProjCoreToolchain.NetCoreApp80.Name,
104+
CsProjCoreToolchain.NetCoreApp90.Name,
105+
CsProjCoreToolchain.NetCoreApp10_0.Name,
106+
};
107+
Assert.Equal(expected, results);
108+
}
109+
110+
[Theory]
111+
[InlineData("item1", "item1", 0)]
112+
[InlineData("item123", "item123", 0)]
113+
// Compare different values
114+
[InlineData("item1", "item2", -1)]
115+
[InlineData("item2", "item1", 1)]
116+
[InlineData("item2", "item10", -1)]
117+
[InlineData("item10", "item2", 1)]
118+
[InlineData("item1a", "item1b", -1)]
119+
[InlineData("item1b", "item1a", 1)]
120+
[InlineData("item", "item1", -1)]
121+
[InlineData("item10", "item", 1)]
122+
[InlineData(".NET 8", ".NET 10", -1)]
123+
[InlineData(".NET 10", ".NET 8", 1)]
124+
[InlineData("v1.4.1", "v1.4.10", -1)]
125+
[InlineData("v1.4.10", "v1.4.2", 1)]
126+
// Compare zero paddeed numeric string.
127+
[InlineData("item01", "item1", 0)]
128+
[InlineData("item001", "item1", 0)]
129+
[InlineData("item1", "item001", 0)]
130+
[InlineData("item1", "item01", 0)]
131+
[InlineData("item9", "item09", 0)]
132+
[InlineData(".NET 08", ".NET 10", -1)]
133+
[InlineData(".NET 10", ".NET 08", 1)]
134+
// Arguments that contains null
135+
[InlineData(null, "a", -1)]
136+
[InlineData("a", null, 1)]
137+
[InlineData(null, null, 0)]
138+
public void TestNumericComparer(string? a, string? b, int expectedSign)
139+
{
140+
int result = new JobComparer.NumericStringComparer().Compare(a, b);
141+
Assert.Equal(expectedSign, NormalizeSign(result));
142+
143+
static int NormalizeSign(int value)
144+
=> value == 0 ? 0 : value < 0 ? -1 : 1;
145+
}
146+
}

0 commit comments

Comments
 (0)