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
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using ModularPipelines.Context;
using ModularPipelines.DotNet.Extensions;
using ModularPipelines.DotNet.Options;
using ModularPipelines.Git.Extensions;
using ModularPipelines.Models;
using ModularPipelines.Modules;
using ModularPipelines.Options;

namespace EnumerableAsyncProcessor.Pipeline.Modules;

public class BuildBenchmarkProjectsModule : Module<List<CommandResult>>
{
protected override async Task<List<CommandResult>?> ExecuteAsync(
IModuleContext context,
CancellationToken cancellationToken)
{
var results = new List<CommandResult>();
var executionOptions = new CommandExecutionOptions
{
ThrowOnNonZeroExitCode = true,
};

foreach (var benchmarkProjectFile in context
.Git().RootDirectory
.GetFiles(file => file.Path.EndsWith(".csproj", StringComparison.OrdinalIgnoreCase)
&& file.Path.Contains("Benchmarks", StringComparison.OrdinalIgnoreCase)))
{
results.Add(await context.DotNet().Build(new DotNetBuildOptions
{
ProjectSolution = benchmarkProjectFile.Path,
Configuration = "Release",
}, executionOptions: executionOptions, cancellationToken: cancellationToken).ConfigureAwait(false));
}

return results;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ namespace EnumerableAsyncProcessor.Pipeline.Modules;
[DependsOn<PackageFilesRemovalModule>]
[DependsOn<NugetVersionGeneratorModule>]
[DependsOn<RunUnitTestsModule>]
[DependsOn<BuildBenchmarkProjectsModule>]
[DependsOn<BuildExampleProjectsModule>]
public class PackProjectsModule : Module<List<CommandResult>>
{
Expand Down Expand Up @@ -49,6 +50,7 @@ private bool GetProjectsPredicate(File file, IModuleContext context)
}

if (path.Contains("Tests", StringComparison.OrdinalIgnoreCase)
|| path.Contains("Benchmarks", StringComparison.OrdinalIgnoreCase)
|| path.Contains("Pipeline", StringComparison.OrdinalIgnoreCase)
|| path.Contains("Example", StringComparison.OrdinalIgnoreCase))
{
Expand Down
3 changes: 2 additions & 1 deletion EnumerableAsyncProcessor.Pipeline/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
builder.AddModule<UploadPackagesToNugetModule>();
}

builder.AddModule<BuildExampleProjectsModule>()
builder.AddModule<BuildBenchmarkProjectsModule>()
.AddModule<BuildExampleProjectsModule>()
.AddModule<RunUnitTestsModule>()
.AddModule<NugetVersionGeneratorModule>()
.AddModule<PackProjectsModule>()
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ Process Multiple Asynchronous Tasks in Various Ways - One at a time / Batched /
Install via Nuget
`Install-Package EnumerableAsyncProcessor`

## Performance benchmarks

See the [benchmark guide](benchmarks/README.md) to run the BenchmarkDotNet suite, filter scenarios, and compare revisions.

### Supported frameworks

Version 4 requires .NET 8 or later. The package targets and tests `net8.0`, `net9.0`, and `net10.0`.
Expand Down
6 changes: 6 additions & 0 deletions TomLonghurst.EnumerableAsyncProcessor.sln
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnumerableAsyncProcessor.Ex
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnumerableAsyncProcessor.Pipeline", "EnumerableAsyncProcessor.Pipeline\EnumerableAsyncProcessor.Pipeline.csproj", "{522C01BC-E86F-4C53-BD96-1EBA4129CE53}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnumerableAsyncProcessor.Benchmarks", "benchmarks\EnumerableAsyncProcessor.Benchmarks.csproj", "{B6D06B46-12E2-4D85-BACE-8BCE0A3A4EB8}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -30,5 +32,9 @@ Global
{522C01BC-E86F-4C53-BD96-1EBA4129CE53}.Debug|Any CPU.Build.0 = Debug|Any CPU
{522C01BC-E86F-4C53-BD96-1EBA4129CE53}.Release|Any CPU.ActiveCfg = Release|Any CPU
{522C01BC-E86F-4C53-BD96-1EBA4129CE53}.Release|Any CPU.Build.0 = Release|Any CPU
{B6D06B46-12E2-4D85-BACE-8BCE0A3A4EB8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B6D06B46-12E2-4D85-BACE-8BCE0A3A4EB8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B6D06B46-12E2-4D85-BACE-8BCE0A3A4EB8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B6D06B46-12E2-4D85-BACE-8BCE0A3A4EB8}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
16 changes: 16 additions & 0 deletions benchmarks/EnumerableAsyncProcessor.Benchmarks.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
Comment thread
thomhurst marked this conversation as resolved.

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\EnumerableAsyncProcessor\EnumerableAsyncProcessor.csproj" />
<PackageReference Include="BenchmarkDotNet" Version="0.15.8" />
</ItemGroup>

</Project>
134 changes: 134 additions & 0 deletions benchmarks/ProcessorBenchmarks.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
using BenchmarkDotNet.Attributes;
using EnumerableAsyncProcessor.Extensions;

namespace EnumerableAsyncProcessor.Benchmarks;

[MemoryDiagnoser]
public class ProcessorBenchmarks
{
private const int Concurrency = 64;
private int[] _items = null!;
private Func<int, Task> _processItemAsync = null!;
private Func<int, Task<int>> _transformItemAsync = null!;

public enum SelectorWorkload
{
CompletedTask,
TaskYield
}

[Params(1_000, 10_000, 100_000)]
public int ItemCount { get; set; }

[Params(SelectorWorkload.CompletedTask, SelectorWorkload.TaskYield)]
public SelectorWorkload Workload { get; set; }

[GlobalSetup]
public void Setup()
{
_items = Enumerable.Range(0, ItemCount).ToArray();
_processItemAsync = Workload == SelectorWorkload.CompletedTask
? ProcessCompletedItemAsync
: ProcessWithYieldAsync;
_transformItemAsync = Workload == SelectorWorkload.CompletedTask
? TransformCompletedItemAsync
: TransformWithYieldAsync;
}

[Benchmark(Baseline = true)]
public async Task UnboundedParallel()
{
await using var processor = _items
.ForEachAsync(_processItemAsync)
.ProcessInParallel();

await processor.WaitAsync();
}

[Benchmark]
public async Task ThrottledParallel()
{
await using var processor = _items
.ForEachAsync(_processItemAsync)
.ProcessInParallel(maxConcurrency: Concurrency);

await processor.WaitAsync();
}

[Benchmark]
public async Task RateLimitedParallel()
{
await using var processor = _items
.ForEachAsync(_processItemAsync)
.ProcessInParallel(Concurrency);

await processor.WaitAsync();
}

[Benchmark]
public async Task TimedRateLimitedParallel()
{
await using var processor = _items
.ForEachAsync(_processItemAsync)
.ProcessInParallel(Concurrency, TimeSpan.Zero);

await processor.WaitAsync();
}

[Benchmark]
public async Task Batch()
{
await using var processor = _items
.ForEachAsync(_processItemAsync)
.ProcessInBatches(Concurrency);

await processor.WaitAsync();
}

[Benchmark]
public async Task OneAtATime()
{
await using var processor = _items
.ForEachAsync(_processItemAsync)
.ProcessOneAtATime();

await processor.WaitAsync();
}

[Benchmark]
public async Task<int> ResultStreaming()
{
await using var processor = _items
.SelectAsync(_transformItemAsync)
.ProcessInParallel(maxConcurrency: Concurrency);

var checksum = 0;
await foreach (var result in processor.GetResultsAsyncEnumerable())
{
checksum = unchecked(checksum + result);
}

return checksum;
}

private static Task ProcessCompletedItemAsync(int _)
{
return Task.CompletedTask;
}

private static Task<int> TransformCompletedItemAsync(int item)
{
return Task.FromResult(item);
}

private static async Task ProcessWithYieldAsync(int _)
{
await Task.Yield();
}

private static async Task<int> TransformWithYieldAsync(int item)
{
await Task.Yield();
return item;
}
}
3 changes: 3 additions & 0 deletions benchmarks/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
using BenchmarkDotNet.Running;

BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);
26 changes: 26 additions & 0 deletions benchmarks/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Benchmarks

The suite measures processor coordination overhead and allocations with completed-task and `Task.Yield` selectors at 1,000, 10,000, and 100,000 items.
The timed scenario uses a zero-duration window so the results isolate coordination overhead rather than wall-clock waiting.

Run every benchmark from the repository root:

```shell
dotnet run -c Release --project benchmarks
```

`BenchmarkSwitcher` forwards BenchmarkDotNet command-line options. List or filter cases before a focused comparison:

```shell
dotnet run -c Release --project benchmarks -- --list flat
dotnet run -c Release --project benchmarks -- --filter "*ThrottledParallel*"
```

`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:

```shell
dotnet run -c Release --project benchmarks -- --filter "*ResultStreaming*" --artifacts artifacts/baseline
dotnet run -c Release --project benchmarks -- --filter "*ResultStreaming*" --artifacts artifacts/candidate
```

Use Release builds on the same idle machine and compare the generated Markdown or CSV reports under each artifacts directory.
Loading