Skip to content

Commit 2dd1e66

Browse files
committed
Add fast path for non-DI scenarios
1 parent b1b5e91 commit 2dd1e66

22 files changed

Lines changed: 519 additions & 176 deletions

.github/scripts/Update-BenchmarkDocs.ps1

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -89,16 +89,28 @@ foreach ($row in $csv) {
8989
}
9090
}
9191

92-
# Sort each group by implementation order
93-
$groupKeys = @($groups.Keys)
94-
foreach ($key in $groupKeys) {
95-
$groups[$key] = $groups[$key] | Sort-Object {
96-
$method = $_.Method
97-
for ($i = 0; $i -lt $implOrder.Count; $i++) {
98-
if ($method -match "^$($implOrder[$i])_") { return $i }
92+
# Helper function to parse Mean value to nanoseconds for sorting
93+
function Parse-MeanToNs {
94+
param([string]$Value)
95+
if ($Value -match '^([\d,.]+)\s*(ns|us|μs|ms|s)$') {
96+
$num = [double]($Matches[1] -replace ',', '')
97+
$unit = $Matches[2]
98+
switch ($unit) {
99+
'ns' { return $num }
100+
'us' { return $num * 1000 }
101+
'μs' { return $num * 1000 }
102+
'ms' { return $num * 1000000 }
103+
's' { return $num * 1000000000 }
104+
default { return $num }
99105
}
100-
return 999
101106
}
107+
return [double]::MaxValue
108+
}
109+
110+
# Sort each group by Mean (best performance first)
111+
$groupKeys = @($groups.Keys)
112+
foreach ($key in $groupKeys) {
113+
$groups[$key] = $groups[$key] | Sort-Object { Parse-MeanToNs $_.Mean }
102114
}
103115

104116
# Helper function to format allocated bytes with thousand separators

.github/workflows/benchmarks.yml

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,6 @@ name: Benchmarks
22

33
on:
44
workflow_dispatch:
5-
schedule:
6-
# Run daily at 2:00 AM UTC
7-
- cron: '0 2 * * *'
8-
push:
9-
branches:
10-
- main
11-
paths:
12-
- '.github/scripts/Update-BenchmarkDocs.ps1'
13-
- '.github/workflows/benchmarks.yml'
145

156
jobs:
167
benchmark:

benchmarks/Foundatio.Mediator.Benchmarks/CoreBenchmarks.cs

Lines changed: 54 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -31,19 +31,22 @@ public class CoreBenchmarks
3131
private readonly FoundatioCommandHandler _directCommandHandler = new();
3232
private readonly FoundatioQueryHandler _directQueryHandler = new();
3333
private readonly FoundatioEventHandler _directEventHandler = new();
34-
private FoundatioQueryWithDependenciesHandler _directQueryWithDependenciesHandler = null!;
34+
private readonly FoundatioCreateOrderHandler _directCreateOrderHandler = new();
35+
private readonly FoundatioOrderCreatedHandler1 _directOrderCreatedHandler1 = new();
36+
private readonly FoundatioOrderCreatedHandler2 _directOrderCreatedHandler2 = new();
37+
private FoundatioFullQueryHandler _directFullQueryHandler = null!;
3538

3639
private readonly PingCommand _pingCommand = new("test-123");
3740
private readonly GetOrder _getOrder = new(42);
38-
private readonly GetOrderWithDependencies _getOrderWithDependencies = new(42);
41+
private readonly GetFullQuery _getFullQuery = new(42);
3942
private readonly GetCachedOrder _getCachedOrder = new(42);
4043
private readonly UserRegisteredEvent _userRegisteredEvent = new("User-456", "test@example.com");
4144
private readonly CreateOrder _createOrder = new(123, 99.99m);
4245

4346
// MediatorNet-specific message types (uses different interfaces)
4447
private readonly MediatorNetPingCommand _mediatorNetPingCommand = new("test-123");
4548
private readonly MediatorNetGetOrder _mediatorNetGetOrder = new(42);
46-
private readonly MediatorNetGetOrderWithDependencies _mediatorNetGetOrderWithDependencies = new(42);
49+
private readonly MediatorNetGetFullQuery _mediatorNetGetFullQuery = new(42);
4750
private readonly MediatorNetUserRegisteredEvent _mediatorNetUserRegisteredEvent = new("User-456", "test@example.com");
4851
private readonly MediatorNetCreateOrder _mediatorNetCreateOrder = new(123, 99.99m);
4952
private readonly MediatorNetGetCachedOrder _mediatorNetGetCachedOrder = new(42);
@@ -58,8 +61,8 @@ public void Setup()
5861
_foundatioServices = foundatioServices.BuildServiceProvider();
5962
_foundatioMediator = _foundatioServices.GetRequiredService<Foundatio.Mediator.IMediator>();
6063

61-
// Create direct handler with DI
62-
_directQueryWithDependenciesHandler = new FoundatioQueryWithDependenciesHandler(
64+
// Create direct handler with DI for FullQuery baseline
65+
_directFullQueryHandler = new FoundatioFullQueryHandler(
6366
_foundatioServices.GetRequiredService<IOrderService>());
6467

6568
// Setup MediatR
@@ -68,8 +71,8 @@ public void Setup()
6871
mediatrServices.AddMediatR(cfg =>
6972
{
7073
cfg.RegisterServicesFromAssemblyContaining<CoreBenchmarks>();
71-
// Register timing behavior for GetOrderWithDependencies to match Foundatio's middleware
72-
cfg.AddBehavior<MediatR.IPipelineBehavior<GetOrderWithDependencies, Order>, Handlers.MediatR.TimingBehavior<GetOrderWithDependencies, Order>>();
74+
// Register timing behavior for GetFullQuery to match Foundatio's middleware
75+
cfg.AddBehavior<MediatR.IPipelineBehavior<GetFullQuery, Order>, Handlers.MediatR.TimingBehavior<GetFullQuery, Order>>();
7376
// Register short-circuit behavior for GetCachedOrder
7477
cfg.AddBehavior<MediatR.IPipelineBehavior<GetCachedOrder, Order>, Handlers.MediatR.ShortCircuitBehavior>();
7578
// Use parallel notification publisher to match Foundatio/MassTransit behavior
@@ -87,7 +90,7 @@ public void Setup()
8790
cfg.AddConsumer<Handlers.MassTransit.MassTransitQueryConsumer>();
8891
cfg.AddConsumer<Handlers.MassTransit.MassTransitEventConsumer>();
8992
cfg.AddConsumer<Handlers.MassTransit.MassTransitEventConsumer2>();
90-
cfg.AddConsumer<Handlers.MassTransit.MassTransitQueryWithDependenciesConsumer>();
93+
cfg.AddConsumer<Handlers.MassTransit.MassTransitFullQueryConsumer>();
9194
cfg.AddConsumer<Handlers.MassTransit.MassTransitCreateOrderConsumer>();
9295
cfg.AddConsumer<Handlers.MassTransit.MassTransitOrderCreatedConsumer1>();
9396
cfg.AddConsumer<Handlers.MassTransit.MassTransitOrderCreatedConsumer2>();
@@ -122,7 +125,7 @@ public void Setup()
122125
.IncludeType<Handlers.Wolverine.WolverineQueryHandler>()
123126
.IncludeType<Handlers.Wolverine.WolverineEventHandler>()
124127
.IncludeType<Handlers.Wolverine.WolverineEventHandler2>()
125-
.IncludeType<Handlers.Wolverine.WolverineQueryWithDependenciesHandler>()
128+
.IncludeType<Handlers.Wolverine.WolverineFullQueryHandler>()
126129
.IncludeType<Handlers.Wolverine.WolverineCreateOrderHandler>()
127130
.IncludeType<Handlers.Wolverine.WolverineOrderCreatedHandler1>()
128131
.IncludeType<Handlers.Wolverine.WolverineOrderCreatedHandler2>()
@@ -166,16 +169,11 @@ public async Task<Order> Direct_Query()
166169
}
167170

168171
[Benchmark]
169-
public async Task Direct_Event()
172+
public async Task Direct_Publish()
170173
{
171174
await _directEventHandler.HandleAsync(_userRegisteredEvent);
172175
}
173176

174-
[Benchmark]
175-
public async Task<Order> Direct_QueryWithDependencies()
176-
{
177-
return await _directQueryWithDependenciesHandler.HandleAsync(_getOrderWithDependencies);
178-
}
179177

180178
// Scenario 1: InvokeAsync without response (Command)
181179
[Benchmark]
@@ -272,40 +270,65 @@ public async Task MediatorNet_Publish()
272270
await _mediatorNetMediator.Publish(_mediatorNetUserRegisteredEvent);
273271
}
274272

275-
// Scenario 4: InvokeAsync<T> with DI (Query with dependency injection)
273+
// Scenario 4: InvokeAsync<T> with DI (Query with dependency injection and middleware)
274+
[Benchmark]
275+
public async Task<Order> Direct_FullQuery()
276+
{
277+
// Simulate TimingMiddleware.Before
278+
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
279+
try
280+
{
281+
return await _directFullQueryHandler.HandleAsync(_getFullQuery);
282+
}
283+
finally
284+
{
285+
// Simulate TimingMiddleware.Finally
286+
stopwatch.Stop();
287+
}
288+
}
289+
276290
[Benchmark]
277291
public async Task<Order> Foundatio_FullQuery()
278292
{
279-
return await _foundatioMediator.InvokeAsync<Order>(_getOrderWithDependencies);
293+
return await _foundatioMediator.InvokeAsync<Order>(_getFullQuery);
280294
}
281295

282296
[Benchmark]
283297
public async Task<Order> MediatR_FullQuery()
284298
{
285-
return await _mediatrMediator.Send(_getOrderWithDependencies);
299+
return await _mediatrMediator.Send(_getFullQuery);
286300
}
287301

288302
[Benchmark]
289303
public async Task<Order> MassTransit_FullQuery()
290304
{
291-
var client = _masstransitMediator.CreateRequestClient<GetOrderWithDependencies>();
292-
var response = await client.GetResponse<Order>(_getOrderWithDependencies);
305+
var client = _masstransitMediator.CreateRequestClient<GetFullQuery>();
306+
var response = await client.GetResponse<Order>(_getFullQuery);
293307
return response.Message;
294308
}
295309

296310
[Benchmark]
297311
public async Task<Order?> Wolverine_FullQuery()
298312
{
299-
return await _wolverineBus.InvokeAsync<Order>(_getOrderWithDependencies);
313+
return await _wolverineBus.InvokeAsync<Order>(_getFullQuery);
300314
}
301315

302316
[Benchmark]
303317
public async Task<Order> MediatorNet_FullQuery()
304318
{
305-
return await _mediatorNetMediator.Send(_mediatorNetGetOrderWithDependencies);
319+
return await _mediatorNetMediator.Send(_mediatorNetGetFullQuery);
306320
}
307321

308322
// Scenario 5: Cascading messages - invoke returns result and auto-publishes events to multiple handlers
323+
[Benchmark]
324+
public async Task<Order> Direct_CascadingMessages()
325+
{
326+
var (order, evt) = await _directCreateOrderHandler.HandleAsync(_createOrder);
327+
await _directOrderCreatedHandler1.HandleAsync(evt);
328+
await _directOrderCreatedHandler2.HandleAsync(evt);
329+
return order;
330+
}
331+
309332
[Benchmark]
310333
public async Task<Order> Foundatio_CascadingMessages()
311334
{
@@ -340,6 +363,15 @@ public async Task<Order> MediatorNet_CascadingMessages()
340363

341364
// Scenario 6: Short-circuit middleware - returns cached result, handler is never invoked
342365
// Tests the cost of short-circuiting via middleware (caching, validation, auth, etc.)
366+
private static readonly Order _cachedOrder = new(999, 49.99m, DateTime.UtcNow);
367+
368+
[Benchmark]
369+
public Task<Order> Direct_ShortCircuit()
370+
{
371+
// Simulate ShortCircuitMiddleware.Before returning cached result
372+
return Task.FromResult(_cachedOrder);
373+
}
374+
343375
[Benchmark]
344376
public async Task<Order> Foundatio_ShortCircuit()
345377
{

benchmarks/Foundatio.Mediator.Benchmarks/FoundatioBenchmarks.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public class FoundatioBenchmarks
1818

1919
private readonly PingCommand _pingCommand = new("test-123");
2020
private readonly GetOrder _getOrder = new(42);
21-
private readonly GetOrderWithDependencies _getOrderWithDependencies = new(42);
21+
private readonly GetFullQuery _getFullQuery = new(42);
2222
private readonly UserRegisteredEvent _userRegisteredEvent = new("User-456", "test@example.com");
2323

2424
[GlobalSetup]
@@ -56,8 +56,8 @@ public async Task Publish()
5656
}
5757

5858
[Benchmark]
59-
public async Task<Order> QueryWithDependencies()
59+
public async Task<Order> FullQuery()
6060
{
61-
return await _foundatioMediator.InvokeAsync<Order>(_getOrderWithDependencies);
61+
return await _foundatioMediator.InvokeAsync<Order>(_getFullQuery);
6262
}
6363
}

benchmarks/Foundatio.Mediator.Benchmarks/Handlers/Foundatio/FoundatioHandlers.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,16 +43,16 @@ public async Task HandleAsync(UserRegisteredEvent notification, CancellationToke
4343
}
4444

4545
// Scenario 4: Query handler with dependency injection
46-
public class FoundatioQueryWithDependenciesHandler
46+
public class FoundatioFullQueryHandler
4747
{
4848
private readonly IOrderService _orderService;
4949

50-
public FoundatioQueryWithDependenciesHandler(IOrderService orderService)
50+
public FoundatioFullQueryHandler(IOrderService orderService)
5151
{
5252
_orderService = orderService;
5353
}
5454

55-
public async Task<Order> HandleAsync(GetOrderWithDependencies query, CancellationToken cancellationToken = default)
55+
public async Task<Order> HandleAsync(GetFullQuery query, CancellationToken cancellationToken = default)
5656
{
5757
return await _orderService.GetOrderAsync(query.Id, cancellationToken);
5858
}

benchmarks/Foundatio.Mediator.Benchmarks/Handlers/MassTransit/MassTransitHandlers.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,16 +44,16 @@ public async Task Consume(ConsumeContext<UserRegisteredEvent> context)
4444
}
4545

4646
// Scenario 4: Query handler with dependency injection
47-
public class MassTransitQueryWithDependenciesConsumer : IConsumer<GetOrderWithDependencies>
47+
public class MassTransitFullQueryConsumer : IConsumer<GetFullQuery>
4848
{
4949
private readonly IOrderService _orderService;
5050

51-
public MassTransitQueryWithDependenciesConsumer(IOrderService orderService)
51+
public MassTransitFullQueryConsumer(IOrderService orderService)
5252
{
5353
_orderService = orderService;
5454
}
5555

56-
public async Task Consume(ConsumeContext<GetOrderWithDependencies> context)
56+
public async Task Consume(ConsumeContext<GetFullQuery> context)
5757
{
5858
var order = await _orderService.GetOrderAsync(context.Message.Id);
5959
await context.RespondAsync(order);

benchmarks/Foundatio.Mediator.Benchmarks/Handlers/MediatR/MediatRHandlers.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,16 +49,16 @@ public async Task Handle(UserRegisteredEvent notification, CancellationToken can
4949

5050
// Scenario 4: Query handler with dependency injection
5151
[FoundatioIgnore]
52-
public class MediatRQueryWithDependenciesHandler : IRequestHandler<GetOrderWithDependencies, Order>
52+
public class MediatRFullQueryHandler : IRequestHandler<GetFullQuery, Order>
5353
{
5454
private readonly IOrderService _orderService;
5555

56-
public MediatRQueryWithDependenciesHandler(IOrderService orderService)
56+
public MediatRFullQueryHandler(IOrderService orderService)
5757
{
5858
_orderService = orderService;
5959
}
6060

61-
public async Task<Order> Handle(GetOrderWithDependencies request, CancellationToken cancellationToken)
61+
public async Task<Order> Handle(GetFullQuery request, CancellationToken cancellationToken)
6262
{
6363
return await _orderService.GetOrderAsync(request.Id, cancellationToken);
6464
}

benchmarks/Foundatio.Mediator.Benchmarks/Handlers/MediatorNet/MediatorNetHandlers.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ public record MediatorNetPingCommand(string Id) : MediatorLib.ICommand;
1313
public record MediatorNetGetOrder(int Id) : MediatorLib.IQuery<Order>;
1414

1515
[FoundatioIgnore]
16-
public record MediatorNetGetOrderWithDependencies(int Id) : MediatorLib.IQuery<Order>;
16+
public record MediatorNetGetFullQuery(int Id) : MediatorLib.IQuery<Order>;
1717

1818
[FoundatioIgnore]
1919
public record MediatorNetUserRegisteredEvent(string UserId, string Email) : MediatorLib.INotification;
@@ -71,16 +71,16 @@ public ValueTask Handle(MediatorNetUserRegisteredEvent notification, Cancellatio
7171

7272
// Scenario 4: Query handler with dependency injection
7373
[FoundatioIgnore]
74-
public class MediatorNetQueryWithDependenciesHandler : MediatorLib.IQueryHandler<MediatorNetGetOrderWithDependencies, Order>
74+
public class MediatorNetFullQueryHandler : MediatorLib.IQueryHandler<MediatorNetGetFullQuery, Order>
7575
{
7676
private readonly IOrderService _orderService;
7777

78-
public MediatorNetQueryWithDependenciesHandler(IOrderService orderService)
78+
public MediatorNetFullQueryHandler(IOrderService orderService)
7979
{
8080
_orderService = orderService;
8181
}
8282

83-
public async ValueTask<Order> Handle(MediatorNetGetOrderWithDependencies query, CancellationToken cancellationToken)
83+
public async ValueTask<Order> Handle(MediatorNetGetFullQuery query, CancellationToken cancellationToken)
8484
{
8585
return await _orderService.GetOrderAsync(query.Id, cancellationToken);
8686
}

benchmarks/Foundatio.Mediator.Benchmarks/Handlers/Wolverine/WolverineHandlers.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,9 @@ public Task Handle(UserRegisteredEvent notification)
4848

4949
// Scenario 4: Query handler with dependency injection
5050
[FoundatioIgnore]
51-
public class WolverineQueryWithDependenciesHandler
51+
public class WolverineFullQueryHandler
5252
{
53-
public Task<Order> Handle(GetOrderWithDependencies query, IOrderService orderService)
53+
public Task<Order> Handle(GetFullQuery query, IOrderService orderService)
5454
{
5555
return orderService.GetOrderAsync(query.Id);
5656
}

benchmarks/Foundatio.Mediator.Benchmarks/Messages/BenchmarkMessages.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ public record PingCommand(string Id) : IRequest;
99
// Scenario 2: Query with return value for InvokeAsync<T>
1010
public record GetOrder(int Id) : IRequest<Order>;
1111

12-
// Scenario 2b: Query with dependency injection
13-
public record GetOrderWithDependencies(int Id) : IRequest<Order>;
12+
// Scenario 4: FullQuery - Query with dependency injection
13+
public record GetFullQuery(int Id) : IRequest<Order>;
1414

1515
// Scenario 3: Notification for PublishAsync with multiple handlers
1616
public record UserRegisteredEvent(string UserId, string Email) : MediatR.INotification;

0 commit comments

Comments
 (0)