Skip to content

Commit 3c9e734

Browse files
committed
Update benchmarks
1 parent 88c22a4 commit 3c9e734

File tree

17 files changed

+315
-740
lines changed

17 files changed

+315
-740
lines changed

.github/copilot-instructions.md

Lines changed: 2 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,6 @@ Build a fast, convention-based C# mediator library using incremental source gene
104104

105105
* Supported variants:
106106

107-
* `Publish(message, CancellationToken = default)`
108107
* `PublishAsync(message, CancellationToken = default)`
109108
* **Zero to many handlers** per message
110109
* All handlers are called inline
@@ -115,7 +114,8 @@ Build a fast, convention-based C# mediator library using incremental source gene
115114
* If one handler throws:
116115

117116
* All others still run
118-
* First exception is thrown after all complete
117+
* If one handler throws, the exception is rethrown after all handlers complete
118+
* If multiple handlers throw, an `AggregateException` is thrown with all exceptions
119119

120120
---
121121

@@ -191,42 +191,3 @@ Build a fast, convention-based C# mediator library using incremental source gene
191191
* Middleware phases
192192
* Tuple/cascading message flow
193193
* Source generation metadata (discovery, errors)
194-
195-
---
196-
197-
## ✅ Implementation Checklist
198-
199-
### ✅ Core
200-
201-
* [x] Discover handlers using naming convention
202-
* [ ] Generate static `HandleAsync` method per handler
203-
* [x] Emit compile-time diagnostics for invalid handlers
204-
* [ ] Generate and register `HandlerRegistration` per handler
205-
206-
### ✅ Dispatch
207-
208-
* [x] `Invoke(...)` and `InvokeAsync(...)` variants
209-
* [x] `Publish(...)` and `PublishAsync(...)` variants
210-
* [ ] Match handler return type to expected `TResponse`
211-
* [ ] Publish any extra tuple values as cascading messages
212-
* [ ] Wait for cascading messages before returning from `Invoke`
213-
214-
### 🧠 Middleware
215-
216-
* [ ] Execute `Before` / `After` / `Finally` logic
217-
* [ ] Support returning `HandlerResult` from `Before`
218-
* [ ] Pass `Before` return into `After`/`Finally`
219-
* [ ] Inject message, token, and DI services into middleware
220-
221-
### ⚙️ Runtime
222-
223-
* [ ] Register `HandlerRegistration` keyed by fully qualified message name
224-
* [ ] Use `GetServices<HandlerRegistration>()` to dispatch
225-
* [ ] Add config for publish mode: sequential vs parallel
226-
227-
### 🧪 Testing & Debugging
228-
229-
* [x] Enable `EmitCompilerGeneratedFiles` for source inspection
230-
* [x] Validate generator behavior with method DI only
231-
* [ ] Add tests for tuple returns and cascading publish
232-
* [x] Benchmark vs MediatR, Wolverine, etc.

Foundatio.Mediator.sln

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleSample", "samples\Co
2121
EndProject
2222
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Foundatio.Mediator.Benchmarks", "benchmarks\Foundatio.Mediator.Benchmarks\Foundatio.Mediator.Benchmarks.csproj", "{8C9D0E1F-AD6F-4CD0-BE9F-9A2B3C7D8E0F}"
2323
EndProject
24-
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionFolder1", "SolutionFolder1", "{AD86A443-DD0F-4265-A27A-F7BF758B8DC7}"
24+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{AD86A443-DD0F-4265-A27A-F7BF758B8DC7}"
2525
ProjectSection(SolutionItems) = preProject
2626
build\common.props = build\common.props
27+
.github\copilot-instructions.md = .github\copilot-instructions.md
2728
EndProjectSection
2829
EndProject
2930
Global

README.md

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -184,27 +184,46 @@ Foundatio.Mediator delivers exceptional performance, getting remarkably close to
184184

185185
| Method | Mean | Error | StdDev | Gen0 | Allocated | vs Direct |
186186
|-------------------------------|-------------|-----------|-----------|--------|-----------|-----------|
187-
| **DirectPingCommandAsync** | **8.4 ns** | 0.18 ns | 0.16 ns | **-** | **0 B** | baseline |
188-
| **FoundatioPingCommandAsync** | **17.2 ns** | 0.12 ns | 0.11 ns | **-** | **0 B** | **2.05x** |
189-
| MediatRPingCommandAsync | 52.9 ns | 1.00 ns | 0.78 ns | 0.0038 | 192 B | 6.32x |
190-
| MassTransitPingCommandAsync | 1,549.5 ns | 19.3 ns | 16.1 ns | 0.0839 | 4216 B | 185x |
187+
| **Direct_Command** | **8.33 ns** | 0.17 ns | 0.24 ns | **-** | **0 B** | baseline |
188+
| **Foundatio_Command** | **17.93 ns** | 0.36 ns | 0.34 ns | **-** | **0 B** | **2.15x** |
189+
| MediatR_Command | 54.81 ns | 1.12 ns | 1.77 ns | 0.0038 | 192 B | 6.58x |
190+
| MassTransit_Command | 1,585.85 ns | 19.82 ns | 17.57 ns | 0.0839 | 4232 B | 190.4x |
191191

192192
### Queries (Request/Response)
193193

194194
| Method | Mean | Error | StdDev | Gen0 | Allocated | vs Direct |
195195
|-------------------------------|-------------|-----------|-----------|--------|-----------|-----------|
196-
| **DirectGreetingQueryAsync** | **17.9 ns** | 0.39 ns | 0.35 ns | 0.0038 | **192 B** | baseline |
197-
| **FoundatioGreetingQueryAsync** | **31.8 ns** | 0.59 ns | 0.66 ns | 0.0052 | **264 B** | **1.78x** |
198-
| MediatRGreetingQueryAsync | 62.3 ns | 1.27 ns | 1.46 ns | 0.0076 | 384 B | 3.48x |
199-
| MassTransitGreetingQueryAsync | 6,192.6 ns | 123.5 ns | 192.2 ns | 0.2518 | 12792 B | 346x |
196+
| **Direct_Query** | **32.12 ns** | 0.50 ns | 0.47 ns | 0.0038 | **192 B** | baseline |
197+
| **Foundatio_Query** | **46.36 ns** | 0.94 ns | 0.84 ns | 0.0052 | **264 B** | **1.44x** |
198+
| MediatR_Query | 81.40 ns | 1.32 ns | 1.23 ns | 0.0076 | 384 B | 2.53x |
199+
| MassTransit_Query | 6,354.47 ns | 125.37 ns | 195.19 ns | 0.2518 | 12784 B | 197.8x |
200+
201+
### Events (Publish/Subscribe)
202+
203+
| Method | Mean | Error | StdDev | Gen0 | Allocated | vs Direct |
204+
|-------------------------------|-------------|-----------|-----------|--------|-----------|-----------|
205+
| **Direct_Event** | **8.12 ns** | 0.18 ns | 0.36 ns | **-** | **0 B** | baseline |
206+
| **Foundatio_Publish** | **121.57 ns**| 0.80 ns | 0.71 ns | 0.0134 | **672 B** | **15.0x** |
207+
| MediatR_Publish | 59.29 ns | 1.13 ns | 1.59 ns | 0.0057 | 288 B | 7.30x |
208+
| MassTransit_Publish | 1,697.53 ns | 13.97 ns | 13.06 ns | 0.0877 | 4448 B | 209.0x |
209+
210+
### Dependency Injection Overhead
211+
212+
| Method | Mean | Error | StdDev | Gen0 | Allocated | vs No DI |
213+
|---------------------------------------|-------------|-----------|-----------|--------|-----------|-----------|
214+
| **Direct_QueryWithDependencies** | **39.24 ns** | 0.81 ns | 1.28 ns | 0.0052 | **264 B** | baseline |
215+
| **Foundatio_QueryWithDependencies** | **53.30 ns** | 1.05 ns | 1.37 ns | 0.0067 | **336 B** | **1.36x** |
216+
| MediatR_QueryWithDependencies | 79.97 ns | 0.54 ns | 0.51 ns | 0.0091 | 456 B | 2.04x |
217+
| MassTransit_QueryWithDependencies | 5,397.69 ns | 61.05 ns | 50.98 ns | 0.2518 | 12857 B | 137.6x |
200218

201219
### 🎯 Key Performance Insights
202220

203221
- **🚀 Near-Optimal Performance**: Only slight overhead vs direct method calls
204-
- **⚡ Foundatio vs MediatR**: **3.08x faster** for commands, **1.96x faster** for queries
205-
- ** Foundatio vs MassTransit**: **90x faster** for commands, **195x faster** for queries
222+
- **⚡ Foundatio vs MediatR**: **3.06x faster** for commands, **1.76x faster** for queries
223+
- **🎯 Foundatio vs MassTransit**: **88x faster** for commands, **137x faster** for queries
206224
- **💾 Zero Allocation Commands**: Fire-and-forget operations have no GC pressure
207-
- **🎪 Minimal Memory Overhead**: Very efficient memory usage across all scenarios
225+
- **🔥 Minimal DI Overhead**: Only 36% performance cost for dependency injection
226+
- **📡 Efficient Publishing**: Event publishing scales well with multiple handlers
208227

209228
*Benchmarks run on .NET 9.0 with BenchmarkDotNet. Results show Foundatio.Mediator achieves its design goal of getting as close as possible to direct method call performance.*
210229

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
using BenchmarkDotNet.Attributes;
2+
using Foundatio.Mediator.Benchmarks.Messages;
3+
using Foundatio.Mediator.Benchmarks.Handlers.Foundatio;
4+
using Foundatio.Mediator.Benchmarks.Services;
5+
using Microsoft.Extensions.DependencyInjection;
6+
using MassTransit;
7+
8+
namespace Foundatio.Mediator.Benchmarks;
9+
10+
[MemoryDiagnoser]
11+
[SimpleJob(BenchmarkDotNet.Jobs.RuntimeMoniker.Net90)]
12+
public class CoreBenchmarks
13+
{
14+
private IServiceProvider _foundatioServices = null!;
15+
private IServiceProvider _mediatrServices = null!;
16+
private IServiceProvider _masstransitServices = null!;
17+
private Foundatio.Mediator.IMediator _foundatioMediator = null!;
18+
private MediatR.IMediator _mediatrMediator = null!;
19+
private MassTransit.Mediator.IMediator _masstransitMediator = null!;
20+
21+
// Direct handler instances for baseline comparison
22+
private readonly FoundatioCommandHandler _directCommandHandler = new();
23+
private readonly FoundatioQueryHandler _directQueryHandler = new();
24+
private readonly FoundatioEventHandler _directEventHandler = new();
25+
private FoundatioQueryWithDependenciesHandler _directQueryWithDependenciesHandler = null!;
26+
27+
private readonly PingCommand _pingCommand = new("test-123");
28+
private readonly GetOrder _getOrder = new(42);
29+
private readonly GetOrderWithDependencies _getOrderWithDependencies = new(42);
30+
private readonly UserRegisteredEvent _userRegisteredEvent = new("User-456", "test@example.com");
31+
32+
[GlobalSetup]
33+
public void Setup()
34+
{
35+
// Setup Foundatio.Mediator
36+
var foundatioServices = new ServiceCollection();
37+
foundatioServices.AddSingleton<IOrderService, OrderService>();
38+
foundatioServices.AddMediator();
39+
_foundatioServices = foundatioServices.BuildServiceProvider();
40+
_foundatioMediator = _foundatioServices.GetRequiredService<Foundatio.Mediator.IMediator>();
41+
42+
// Create direct handler with DI
43+
_directQueryWithDependenciesHandler = new FoundatioQueryWithDependenciesHandler(
44+
_foundatioServices.GetRequiredService<IOrderService>());
45+
46+
// Setup MediatR
47+
var mediatrServices = new ServiceCollection();
48+
mediatrServices.AddSingleton<IOrderService, OrderService>();
49+
mediatrServices.AddMediatR(cfg =>
50+
{
51+
cfg.RegisterServicesFromAssemblyContaining<CoreBenchmarks>();
52+
});
53+
_mediatrServices = mediatrServices.BuildServiceProvider();
54+
_mediatrMediator = _mediatrServices.GetRequiredService<MediatR.IMediator>();
55+
56+
// Setup MassTransit
57+
var masstransitServices = new ServiceCollection();
58+
masstransitServices.AddSingleton<IOrderService, OrderService>();
59+
masstransitServices.AddMediator(cfg =>
60+
{
61+
cfg.AddConsumer<Handlers.MassTransit.MassTransitCommandConsumer>();
62+
cfg.AddConsumer<Handlers.MassTransit.MassTransitQueryConsumer>();
63+
cfg.AddConsumer<Handlers.MassTransit.MassTransitEventConsumer>();
64+
cfg.AddConsumer<Handlers.MassTransit.MassTransitQueryWithDependenciesConsumer>();
65+
});
66+
_masstransitServices = masstransitServices.BuildServiceProvider();
67+
_masstransitMediator = _masstransitServices.GetRequiredService<MassTransit.Mediator.IMediator>();
68+
}
69+
70+
[GlobalCleanup]
71+
public async Task Cleanup()
72+
{
73+
(_foundatioServices as IDisposable)?.Dispose();
74+
(_mediatrServices as IDisposable)?.Dispose();
75+
76+
if (_masstransitServices is IAsyncDisposable asyncDisposable)
77+
await asyncDisposable.DisposeAsync();
78+
else
79+
(_masstransitServices as IDisposable)?.Dispose();
80+
}
81+
82+
// Baseline: Direct method calls (no mediator overhead)
83+
[Benchmark]
84+
public async Task Direct_Command()
85+
{
86+
await _directCommandHandler.HandleAsync(_pingCommand);
87+
}
88+
89+
[Benchmark]
90+
public async Task<Order> Direct_Query()
91+
{
92+
return await _directQueryHandler.HandleAsync(_getOrder);
93+
}
94+
95+
[Benchmark]
96+
public async Task Direct_Event()
97+
{
98+
await _directEventHandler.HandleAsync(_userRegisteredEvent);
99+
}
100+
101+
[Benchmark]
102+
public async Task<Order> Direct_QueryWithDependencies()
103+
{
104+
return await _directQueryWithDependenciesHandler.HandleAsync(_getOrderWithDependencies);
105+
}
106+
107+
// Scenario 1: InvokeAsync without response (Command)
108+
[Benchmark]
109+
public async Task Foundatio_Command()
110+
{
111+
await _foundatioMediator.InvokeAsync(_pingCommand);
112+
}
113+
114+
[Benchmark]
115+
public async Task MediatR_Command()
116+
{
117+
await _mediatrMediator.Send(_pingCommand);
118+
}
119+
120+
[Benchmark]
121+
public async Task MassTransit_Command()
122+
{
123+
await _masstransitMediator.Send(_pingCommand);
124+
}
125+
126+
// Scenario 2: InvokeAsync<T> (Query)
127+
[Benchmark]
128+
public async Task<Order> Foundatio_Query()
129+
{
130+
return await _foundatioMediator.InvokeAsync<Order>(_getOrder);
131+
}
132+
133+
[Benchmark]
134+
public async Task<Order> MediatR_Query()
135+
{
136+
return await _mediatrMediator.Send(_getOrder);
137+
}
138+
139+
[Benchmark]
140+
public async Task<Order> MassTransit_Query()
141+
{
142+
var client = _masstransitMediator.CreateRequestClient<GetOrder>();
143+
var response = await client.GetResponse<Order>(_getOrder);
144+
return response.Message;
145+
}
146+
147+
// Scenario 3: PublishAsync with a single handler
148+
[Benchmark]
149+
public async Task Foundatio_Publish()
150+
{
151+
await _foundatioMediator.PublishAsync(_userRegisteredEvent);
152+
}
153+
154+
[Benchmark]
155+
public async Task MediatR_Publish()
156+
{
157+
await _mediatrMediator.Publish(_userRegisteredEvent);
158+
}
159+
160+
[Benchmark]
161+
public async Task MassTransit_Publish()
162+
{
163+
await _masstransitMediator.Publish(_userRegisteredEvent);
164+
}
165+
166+
// Scenario 4: InvokeAsync<T> with DI (Query with dependency injection)
167+
[Benchmark]
168+
public async Task<Order> Foundatio_QueryWithDependencies()
169+
{
170+
return await _foundatioMediator.InvokeAsync<Order>(_getOrderWithDependencies);
171+
}
172+
173+
[Benchmark]
174+
public async Task<Order> MediatR_QueryWithDependencies()
175+
{
176+
return await _mediatrMediator.Send(_getOrderWithDependencies);
177+
}
178+
179+
[Benchmark]
180+
public async Task<Order> MassTransit_QueryWithDependencies()
181+
{
182+
var client = _masstransitMediator.CreateRequestClient<GetOrderWithDependencies>();
183+
var response = await client.GetResponse<Order>(_getOrderWithDependencies);
184+
return response.Message;
185+
}
186+
}

benchmarks/Foundatio.Mediator.Benchmarks/Foundatio.Mediator.Benchmarks.csproj

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@
2121
<Content Include="$(CompilerGeneratedFilesOutputPath)/**/*.cs" />
2222
</ItemGroup>
2323

24+
<Target Name="CleanCompilerGeneratedFiles" BeforeTargets="CoreGenerateSourceOutput">
25+
<RemoveDir Directories="$(CompilerGeneratedFilesOutputPath)" />
26+
</Target>
27+
2428
<ItemGroup>
2529
<ProjectReference Include="..\..\src\Foundatio.Mediator\Foundatio.Mediator.csproj" />
2630
<ProjectReference Include="..\..\src\Foundatio.Mediator.SourceGenerator\Foundatio.Mediator.SourceGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
@@ -31,6 +35,7 @@
3135
<PackageReference Include="MassTransit" Version="8.5.1" />
3236
<PackageReference Include="MediatR" Version="12.4.1" />
3337
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
38+
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.0" />
3439
</ItemGroup>
3540

3641
</Project>

0 commit comments

Comments
 (0)