Skip to content

Commit 6628587

Browse files
committed
perf: add reproducible benchmark suite
Add BenchmarkDotNet coverage for seven processor paths across completed and yielding selectors at 1k, 10k, and 100k items. Document filtering and revision comparisons, and exclude the non-packable benchmark project from release packaging.
1 parent adde5f0 commit 6628587

7 files changed

Lines changed: 190 additions & 0 deletions

File tree

EnumerableAsyncProcessor.Pipeline/Modules/PackProjectsModule.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ private bool GetProjectsPredicate(File file, IModuleContext context)
4949
}
5050

5151
if (path.Contains("Tests", StringComparison.OrdinalIgnoreCase)
52+
|| path.Contains("Benchmarks", StringComparison.OrdinalIgnoreCase)
5253
|| path.Contains("Pipeline", StringComparison.OrdinalIgnoreCase)
5354
|| path.Contains("Example", StringComparison.OrdinalIgnoreCase))
5455
{

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ Process Multiple Asynchronous Tasks in Various Ways - One at a time / Batched /
1212
Install via Nuget
1313
`Install-Package EnumerableAsyncProcessor`
1414

15+
## Performance benchmarks
16+
17+
See the [benchmark guide](benchmarks/README.md) to run the BenchmarkDotNet suite, filter scenarios, and compare revisions.
18+
1519
### Supported frameworks
1620

1721
Version 4 requires .NET 8 or later. The package targets and tests `net8.0`, `net9.0`, and `net10.0`.

TomLonghurst.EnumerableAsyncProcessor.sln

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnumerableAsyncProcessor.Ex
88
EndProject
99
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnumerableAsyncProcessor.Pipeline", "EnumerableAsyncProcessor.Pipeline\EnumerableAsyncProcessor.Pipeline.csproj", "{522C01BC-E86F-4C53-BD96-1EBA4129CE53}"
1010
EndProject
11+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnumerableAsyncProcessor.Benchmarks", "benchmarks\EnumerableAsyncProcessor.Benchmarks.csproj", "{B6D06B46-12E2-4D85-BACE-8BCE0A3A4EB8}"
12+
EndProject
1113
Global
1214
GlobalSection(SolutionConfigurationPlatforms) = preSolution
1315
Debug|Any CPU = Debug|Any CPU
@@ -30,5 +32,9 @@ Global
3032
{522C01BC-E86F-4C53-BD96-1EBA4129CE53}.Debug|Any CPU.Build.0 = Debug|Any CPU
3133
{522C01BC-E86F-4C53-BD96-1EBA4129CE53}.Release|Any CPU.ActiveCfg = Release|Any CPU
3234
{522C01BC-E86F-4C53-BD96-1EBA4129CE53}.Release|Any CPU.Build.0 = Release|Any CPU
35+
{B6D06B46-12E2-4D85-BACE-8BCE0A3A4EB8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
36+
{B6D06B46-12E2-4D85-BACE-8BCE0A3A4EB8}.Debug|Any CPU.Build.0 = Debug|Any CPU
37+
{B6D06B46-12E2-4D85-BACE-8BCE0A3A4EB8}.Release|Any CPU.ActiveCfg = Release|Any CPU
38+
{B6D06B46-12E2-4D85-BACE-8BCE0A3A4EB8}.Release|Any CPU.Build.0 = Release|Any CPU
3339
EndGlobalSection
3440
EndGlobal
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net10.0</TargetFramework>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<Nullable>enable</Nullable>
8+
<IsPackable>false</IsPackable>
9+
</PropertyGroup>
10+
11+
<ItemGroup>
12+
<ProjectReference Include="..\EnumerableAsyncProcessor\EnumerableAsyncProcessor.csproj" />
13+
<PackageReference Include="BenchmarkDotNet" Version="0.15.8" />
14+
</ItemGroup>
15+
16+
</Project>

benchmarks/ProcessorBenchmarks.cs

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
using BenchmarkDotNet.Attributes;
2+
using EnumerableAsyncProcessor.Extensions;
3+
4+
namespace EnumerableAsyncProcessor.Benchmarks;
5+
6+
[MemoryDiagnoser]
7+
public class ProcessorBenchmarks
8+
{
9+
private const int Concurrency = 64;
10+
private int[] _items = null!;
11+
private Func<int, Task> _processItemAsync = null!;
12+
private Func<int, Task<int>> _transformItemAsync = null!;
13+
14+
public enum SelectorWorkload
15+
{
16+
CompletedTask,
17+
TaskYield
18+
}
19+
20+
[Params(1_000, 10_000, 100_000)]
21+
public int ItemCount { get; set; }
22+
23+
[Params(SelectorWorkload.CompletedTask, SelectorWorkload.TaskYield)]
24+
public SelectorWorkload Workload { get; set; }
25+
26+
[GlobalSetup]
27+
public void Setup()
28+
{
29+
_items = Enumerable.Range(0, ItemCount).ToArray();
30+
_processItemAsync = Workload == SelectorWorkload.CompletedTask
31+
? ProcessCompletedItemAsync
32+
: ProcessWithYieldAsync;
33+
_transformItemAsync = Workload == SelectorWorkload.CompletedTask
34+
? TransformCompletedItemAsync
35+
: TransformWithYieldAsync;
36+
}
37+
38+
[Benchmark(Baseline = true)]
39+
public async Task UnboundedParallel()
40+
{
41+
await using var processor = _items
42+
.ForEachAsync(_processItemAsync)
43+
.ProcessInParallel();
44+
45+
await processor.WaitAsync();
46+
}
47+
48+
[Benchmark]
49+
public async Task ThrottledParallel()
50+
{
51+
await using var processor = _items
52+
.ForEachAsync(_processItemAsync)
53+
.ProcessInParallel(maxConcurrency: Concurrency);
54+
55+
await processor.WaitAsync();
56+
}
57+
58+
[Benchmark]
59+
public async Task RateLimitedParallel()
60+
{
61+
await using var processor = _items
62+
.ForEachAsync(_processItemAsync)
63+
.ProcessInParallel(Concurrency);
64+
65+
await processor.WaitAsync();
66+
}
67+
68+
[Benchmark]
69+
public async Task TimedRateLimitedParallel()
70+
{
71+
await using var processor = _items
72+
.ForEachAsync(_processItemAsync)
73+
.ProcessInParallel(Concurrency, TimeSpan.Zero);
74+
75+
await processor.WaitAsync();
76+
}
77+
78+
[Benchmark]
79+
public async Task Batch()
80+
{
81+
await using var processor = _items
82+
.ForEachAsync(_processItemAsync)
83+
.ProcessInBatches(Concurrency);
84+
85+
await processor.WaitAsync();
86+
}
87+
88+
[Benchmark]
89+
public async Task OneAtATime()
90+
{
91+
await using var processor = _items
92+
.ForEachAsync(_processItemAsync)
93+
.ProcessOneAtATime();
94+
95+
await processor.WaitAsync();
96+
}
97+
98+
[Benchmark]
99+
public async Task<int> ResultStreaming()
100+
{
101+
await using var processor = _items
102+
.SelectAsync(_transformItemAsync)
103+
.ProcessInParallel(maxConcurrency: Concurrency);
104+
105+
var checksum = 0;
106+
await foreach (var result in processor.GetResultsAsyncEnumerable())
107+
{
108+
checksum = unchecked(checksum + result);
109+
}
110+
111+
return checksum;
112+
}
113+
114+
private static Task ProcessCompletedItemAsync(int _)
115+
{
116+
return Task.CompletedTask;
117+
}
118+
119+
private static Task<int> TransformCompletedItemAsync(int item)
120+
{
121+
return Task.FromResult(item);
122+
}
123+
124+
private static async Task ProcessWithYieldAsync(int _)
125+
{
126+
await Task.Yield();
127+
}
128+
129+
private static async Task<int> TransformWithYieldAsync(int item)
130+
{
131+
await Task.Yield();
132+
return item;
133+
}
134+
}

benchmarks/Program.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
using BenchmarkDotNet.Running;
2+
3+
BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);

benchmarks/README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Benchmarks
2+
3+
The suite measures processor coordination overhead and allocations with completed-task and `Task.Yield` selectors at 1,000, 10,000, and 100,000 items.
4+
The timed scenario uses a zero-duration window so the results isolate coordination overhead rather than wall-clock waiting.
5+
6+
Run every benchmark from the repository root:
7+
8+
```shell
9+
dotnet run -c Release --project benchmarks
10+
```
11+
12+
`BenchmarkSwitcher` forwards BenchmarkDotNet command-line options. List or filter cases before a focused comparison:
13+
14+
```shell
15+
dotnet run -c Release --project benchmarks -- --list flat
16+
dotnet run -c Release --project benchmarks -- --filter "*ThrottledParallel*"
17+
```
18+
19+
`UnboundedParallel` is the in-process baseline for ratio columns. To compare code revisions, run the same filter in separate clean worktrees and give each run a distinct artifacts directory:
20+
21+
```shell
22+
dotnet run -c Release --project benchmarks -- --filter "*ResultStreaming*" --artifacts artifacts/baseline
23+
dotnet run -c Release --project benchmarks -- --filter "*ResultStreaming*" --artifacts artifacts/candidate
24+
```
25+
26+
Use Release builds on the same idle machine and compare the generated Markdown or CSV reports under each artifacts directory.

0 commit comments

Comments
 (0)