Skip to content

Commit cf308c3

Browse files
committed
Various benchmark updates
1 parent a8238e6 commit cf308c3

14 files changed

Lines changed: 429 additions & 225 deletions

File tree

benchmarks/Foundatio.Mediator.Benchmarks/CoreBenchmarks.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,12 @@ public void Setup()
9595
cfg.AddConsumer<Handlers.MassTransit.MassTransitOrderCreatedConsumer1>();
9696
cfg.AddConsumer<Handlers.MassTransit.MassTransitOrderCreatedConsumer2>();
9797
cfg.AddConsumer<Handlers.MassTransit.MassTransitShortCircuitConsumer>();
98+
// Register timing filter for GetFullQuery and short-circuit filter for GetCachedOrder
99+
cfg.ConfigureMediator((context, mcfg) =>
100+
{
101+
mcfg.UseConsumeFilter(typeof(Handlers.MassTransit.MassTransitTimingFilter<>), context);
102+
mcfg.UseConsumeFilter(typeof(Handlers.MassTransit.MassTransitShortCircuitFilter<>), context);
103+
});
98104
});
99105
_masstransitServices = masstransitServices.BuildServiceProvider();
100106
_masstransitMediator = _masstransitServices.GetRequiredService<MassTransit.Mediator.IMediator>();
@@ -104,6 +110,8 @@ public void Setup()
104110
mediatorNetServices.AddSingleton<IOrderService, OrderService>();
105111
// Register short-circuit behavior before AddMediator so it's available in the pipeline
106112
mediatorNetServices.AddSingleton<MediatorLib.IPipelineBehavior<MediatorNetGetCachedOrder, Order>, MediatorNetShortCircuitBehavior>();
113+
// Register timing behavior for GetFullQuery to match Foundatio's middleware
114+
mediatorNetServices.AddSingleton<MediatorLib.IPipelineBehavior<MediatorNetGetFullQuery, Order>, MediatorNetTimingBehavior>();
107115
mediatorNetServices.AddMediator(options =>
108116
{
109117
options.ServiceLifetime = ServiceLifetime.Singleton;
@@ -132,6 +140,8 @@ public void Setup()
132140
.IncludeType<Handlers.Wolverine.WolverineShortCircuitHandler>();
133141
// Register short-circuit middleware for GetCachedOrder
134142
opts.Policies.ForMessagesOfType<GetCachedOrder>().AddMiddleware(typeof(Handlers.Wolverine.WolverineShortCircuitMiddleware));
143+
// Register timing middleware for GetFullQuery to match Foundatio's middleware
144+
opts.Policies.ForMessagesOfType<GetFullQuery>().AddMiddleware(typeof(Handlers.Wolverine.WolverineTimingMiddleware));
135145
})
136146
.Build();
137147
_wolverineHost.Start();
@@ -323,7 +333,7 @@ public async Task<Order> MediatorNet_FullQuery()
323333
[Benchmark]
324334
public async Task<Order> Direct_CascadingMessages()
325335
{
326-
var (order, evt) = _directCreateOrderHandler.HandleAsync(_createOrder);
336+
var (order, evt) = await _directCreateOrderHandler.HandleAsync(_createOrder);
327337
await _directOrderCreatedHandler1.HandleAsync(evt);
328338
await _directOrderCreatedHandler2.HandleAsync(evt);
329339
return order;
Lines changed: 56 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Diagnostics;
12
using Foundatio.Mediator.Benchmarks.Messages;
23
using Foundatio.Mediator.Benchmarks.Services;
34

@@ -7,10 +8,10 @@ namespace Foundatio.Mediator.Benchmarks.Handlers.Foundatio;
78
[Handler]
89
public class FoundatioCommandHandler
910
{
10-
public Task HandleAsync(PingCommand command, CancellationToken cancellationToken = default)
11+
public ValueTask HandleAsync(PingCommand command, CancellationToken cancellationToken = default)
1112
{
12-
// Simulate minimal work - no async state machine
13-
return Task.CompletedTask;
13+
// Simulate minimal work
14+
return default;
1415
}
1516
}
1617

@@ -20,28 +21,28 @@ public class FoundatioQueryHandler
2021
{
2122
public ValueTask<Order> HandleAsync(GetOrder query, CancellationToken cancellationToken = default)
2223
{
23-
return new ValueTask<Order>(new Order(query.Id, 99.99m, DateTime.UtcNow));
24+
return ValueTask.FromResult(new Order(query.Id, 99.99m, DateTime.UtcNow));
2425
}
2526
}
2627

2728
// Scenario 3: Event handlers (PublishAsync with multiple handlers)
2829
[Handler]
2930
public class FoundatioEventHandler
3031
{
31-
public Task HandleAsync(UserRegisteredEvent notification, CancellationToken cancellationToken = default)
32+
public ValueTask HandleAsync(UserRegisteredEvent notification, CancellationToken cancellationToken = default)
3233
{
33-
// Simulate minimal event handling work - returns completed task with no allocation
34-
return Task.CompletedTask;
34+
// Simulate minimal event handling work
35+
return default;
3536
}
3637
}
3738

3839
[Handler]
3940
public class FoundatioSecondEventHandler
4041
{
41-
public Task HandleAsync(UserRegisteredEvent notification, CancellationToken cancellationToken = default)
42+
public ValueTask HandleAsync(UserRegisteredEvent notification, CancellationToken cancellationToken = default)
4243
{
4344
// Second handler listening for the same event
44-
return Task.CompletedTask;
45+
return default;
4546
}
4647
}
4748

@@ -56,52 +57,86 @@ public FoundatioFullQueryHandler(IOrderService orderService)
5657
_orderService = orderService;
5758
}
5859

59-
public async Task<Order> HandleAsync(GetFullQuery query, CancellationToken cancellationToken = default)
60+
public ValueTask<Order> HandleAsync(GetFullQuery query, CancellationToken cancellationToken = default)
6061
{
61-
return await _orderService.GetOrderAsync(query.Id, cancellationToken);
62+
return _orderService.GetOrderAsync(query.Id, cancellationToken);
6263
}
6364
}
6465

6566
// Scenario 5: Cascading messages - returns tuple with result + events that auto-publish
6667
[Handler]
6768
public class FoundatioCreateOrderHandler
6869
{
69-
public (Order order, OrderCreatedEvent evt) HandleAsync(CreateOrder command, CancellationToken cancellationToken = default)
70+
public ValueTask<(Order order, OrderCreatedEvent evt)> HandleAsync(CreateOrder command, CancellationToken cancellationToken = default)
7071
{
71-
// No async state machine needed
7272
var order = new Order(1, command.Amount, DateTime.UtcNow);
73-
return (order, new OrderCreatedEvent(order.Id, command.CustomerId));
73+
return ValueTask.FromResult((order, new OrderCreatedEvent(order.Id, command.CustomerId)));
7474
}
7575
}
7676

7777
// Handlers for the cascaded OrderCreatedEvent
7878
[Handler]
7979
public class FoundatioFirstOrderCreatedHandler
8080
{
81-
public Task HandleAsync(OrderCreatedEvent notification, CancellationToken cancellationToken = default)
81+
public ValueTask HandleAsync(OrderCreatedEvent notification, CancellationToken cancellationToken = default)
8282
{
83-
// First handler for order created event - no async state machine
84-
return Task.CompletedTask;
83+
// First handler for order created event
84+
return default;
8585
}
8686
}
8787

8888
[Handler]
8989
public class FoundatioSecondOrderCreatedHandler
9090
{
91-
public Task HandleAsync(OrderCreatedEvent notification, CancellationToken cancellationToken = default)
91+
public ValueTask HandleAsync(OrderCreatedEvent notification, CancellationToken cancellationToken = default)
9292
{
93-
// Second handler for order created event - no async state machine
94-
return Task.CompletedTask;
93+
// Second handler for order created event
94+
return default;
9595
}
9696
}
9797

9898
// Scenario 6: Short-circuit handler (never actually called due to ShortCircuitMiddleware)
9999
[Handler]
100100
public class FoundatioShortCircuitHandler
101101
{
102-
public Task<Order> HandleAsync(GetCachedOrder query, CancellationToken cancellationToken = default)
102+
public ValueTask<Order> HandleAsync(GetCachedOrder query, CancellationToken cancellationToken = default)
103103
{
104104
// This should never be called - middleware short-circuits before reaching handler
105105
throw new InvalidOperationException("Short-circuit middleware should have prevented this call");
106106
}
107107
}
108+
109+
/// <summary>
110+
/// Simple timing middleware for benchmarking - simulates real-world logging/timing middleware.
111+
/// Only applies to GetFullQuery (FullQuery benchmark).
112+
/// </summary>
113+
[Middleware]
114+
public static class TimingMiddleware
115+
{
116+
public static Stopwatch Before(GetFullQuery message, HandlerExecutionInfo info)
117+
{
118+
return Stopwatch.StartNew();
119+
}
120+
121+
public static void Finally(GetFullQuery message, Stopwatch? stopwatch, HandlerExecutionInfo info)
122+
{
123+
stopwatch?.Stop();
124+
// In real middleware, you'd log here - we just stop the timer for the benchmark
125+
}
126+
}
127+
128+
/// <summary>
129+
/// Short-circuit middleware that immediately returns a cached result without calling the handler.
130+
/// This demonstrates middleware returning early (cache hit, validation success with cached result, etc.)
131+
/// </summary>
132+
[Middleware]
133+
public class ShortCircuitMiddleware
134+
{
135+
private static readonly Order _cachedOrder = new(999, 49.99m, DateTime.UtcNow);
136+
137+
public ValueTask<HandlerResult> BeforeAsync(GetCachedOrder message)
138+
{
139+
// Always short-circuit with cached result - simulates cache hit scenario
140+
return ValueTask.FromResult(HandlerResult.ShortCircuit(_cachedOrder));
141+
}
142+
}

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

Lines changed: 63 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,39 +7,38 @@ namespace Foundatio.Mediator.Benchmarks.Handlers.MassTransit;
77
// Scenario 1: Command handler (InvokeAsync without response)
88
public class MassTransitCommandConsumer : IConsumer<PingCommand>
99
{
10-
public async Task Consume(ConsumeContext<PingCommand> context)
10+
public Task Consume(ConsumeContext<PingCommand> context)
1111
{
1212
// Simulate minimal work
13-
await Task.CompletedTask;
13+
return Task.CompletedTask;
1414
}
1515
}
1616

1717
// Scenario 2: Query handler (InvokeAsync<T>) - No DI for baseline comparison
1818
public class MassTransitQueryConsumer : IConsumer<GetOrder>
1919
{
20-
public async Task Consume(ConsumeContext<GetOrder> context)
20+
public Task Consume(ConsumeContext<GetOrder> context)
2121
{
22-
await Task.CompletedTask;
23-
await context.RespondAsync(new Order(context.Message.Id, 99.99m, DateTime.UtcNow));
22+
return context.RespondAsync(new Order(context.Message.Id, 99.99m, DateTime.UtcNow));
2423
}
2524
}
2625

2726
// Scenario 3: Event handlers (PublishAsync with multiple handlers)
2827
public class MassTransitEventConsumer : IConsumer<UserRegisteredEvent>
2928
{
30-
public async Task Consume(ConsumeContext<UserRegisteredEvent> context)
29+
public Task Consume(ConsumeContext<UserRegisteredEvent> context)
3130
{
3231
// Simulate minimal event handling work
33-
await Task.CompletedTask;
32+
return Task.CompletedTask;
3433
}
3534
}
3635

3736
public class MassTransitEventConsumer2 : IConsumer<UserRegisteredEvent>
3837
{
39-
public async Task Consume(ConsumeContext<UserRegisteredEvent> context)
38+
public Task Consume(ConsumeContext<UserRegisteredEvent> context)
4039
{
4140
// Second handler listening for the same event
42-
await Task.CompletedTask;
41+
return Task.CompletedTask;
4342
}
4443
}
4544

@@ -74,32 +73,77 @@ public async Task Consume(ConsumeContext<CreateOrder> context)
7473
// Handlers for the cascaded OrderCreatedEvent
7574
public class MassTransitOrderCreatedConsumer1 : IConsumer<OrderCreatedEvent>
7675
{
77-
public async Task Consume(ConsumeContext<OrderCreatedEvent> context)
76+
public Task Consume(ConsumeContext<OrderCreatedEvent> context)
7877
{
7978
// First handler for order created event
80-
await Task.CompletedTask;
79+
return Task.CompletedTask;
8180
}
8281
}
8382

8483
public class MassTransitOrderCreatedConsumer2 : IConsumer<OrderCreatedEvent>
8584
{
86-
public async Task Consume(ConsumeContext<OrderCreatedEvent> context)
85+
public Task Consume(ConsumeContext<OrderCreatedEvent> context)
8786
{
8887
// Second handler for order created event
89-
await Task.CompletedTask;
88+
return Task.CompletedTask;
9089
}
9190
}
9291

93-
// Scenario 6: Short-circuit handler - MassTransit doesn't have separate middleware for in-memory mediator,
94-
// so we simulate short-circuit by returning cached value immediately in the consumer.
95-
// Note: In real MassTransit usage with transport, you'd use a filter to short-circuit.
92+
// Scenario 6: Short-circuit handler - MassTransit uses a filter to short-circuit before reaching the consumer.
9693
public class MassTransitShortCircuitConsumer : IConsumer<GetCachedOrder>
94+
{
95+
public Task Consume(ConsumeContext<GetCachedOrder> context)
96+
{
97+
// This should never be called - filter short-circuits before reaching consumer
98+
throw new InvalidOperationException("Short-circuit filter should have prevented this call");
99+
}
100+
}
101+
102+
// MassTransit short-circuit filter - returns cached value without calling the consumer
103+
// Must be generic for UseConsumeFilter registration, but only short-circuits GetCachedOrder
104+
public class MassTransitShortCircuitFilter<T> : IFilter<ConsumeContext<T>> where T : class
97105
{
98106
private static readonly Order _cachedOrder = new(999, 49.99m, DateTime.UtcNow);
99107

100-
public async Task Consume(ConsumeContext<GetCachedOrder> context)
108+
public async Task Send(ConsumeContext<T> context, IPipe<ConsumeContext<T>> next)
109+
{
110+
// Only short-circuit for GetCachedOrder messages
111+
if (context.Message is GetCachedOrder)
112+
{
113+
// Short-circuit by responding with cached value - never calls next()
114+
await context.RespondAsync(_cachedOrder);
115+
return;
116+
}
117+
118+
// Pass through for all other message types
119+
await next.Send(context);
120+
}
121+
122+
public void Probe(ProbeContext context)
123+
{
124+
context.CreateFilterScope("short-circuit");
125+
}
126+
}
127+
128+
// MassTransit timing filter for FullQuery benchmark (equivalent to Foundatio's TimingMiddleware)
129+
public class MassTransitTimingFilter<T> : IFilter<ConsumeContext<T>> where T : class
130+
{
131+
public async Task Send(ConsumeContext<T> context, IPipe<ConsumeContext<T>> next)
132+
{
133+
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
134+
try
135+
{
136+
await next.Send(context);
137+
}
138+
finally
139+
{
140+
stopwatch.Stop();
141+
// In real middleware, you'd log here - we just stop the timer for the benchmark
142+
}
143+
}
144+
145+
public void Probe(ProbeContext context)
101146
{
102-
// Immediately respond with cached value - simulates cache hit
103-
await context.RespondAsync(_cachedOrder);
147+
context.CreateFilterScope("timing");
104148
}
105149
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,9 @@ public MediatRFullQueryHandler(IOrderService orderService)
5353
_orderService = orderService;
5454
}
5555

56-
public async Task<Order> Handle(GetFullQuery request, CancellationToken cancellationToken)
56+
public Task<Order> Handle(GetFullQuery request, CancellationToken cancellationToken)
5757
{
58-
return await _orderService.GetOrderAsync(request.Id, cancellationToken);
58+
return _orderService.GetOrderAsync(request.Id, cancellationToken).AsTask();
5959
}
6060
}
6161

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

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,9 @@ public MediatorNetFullQueryHandler(IOrderService orderService)
6868
_orderService = orderService;
6969
}
7070

71-
public async ValueTask<Order> Handle(MediatorNetGetFullQuery query, CancellationToken cancellationToken)
71+
public ValueTask<Order> Handle(MediatorNetGetFullQuery query, CancellationToken cancellationToken)
7272
{
73-
return await _orderService.GetOrderAsync(query.Id, cancellationToken);
73+
return _orderService.GetOrderAsync(query.Id, cancellationToken);
7474
}
7575
}
7676

@@ -132,3 +132,21 @@ public ValueTask<Order> Handle(MediatorNetGetCachedOrder message, MediatorLib.Me
132132
return ValueTask.FromResult(_cachedOrder);
133133
}
134134
}
135+
136+
// MediatorNet timing behavior for FullQuery benchmark (equivalent to Foundatio's TimingMiddleware)
137+
public class MediatorNetTimingBehavior : MediatorLib.IPipelineBehavior<MediatorNetGetFullQuery, Order>
138+
{
139+
public async ValueTask<Order> Handle(MediatorNetGetFullQuery message, MediatorLib.MessageHandlerDelegate<MediatorNetGetFullQuery, Order> next, CancellationToken cancellationToken)
140+
{
141+
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
142+
try
143+
{
144+
return await next(message, cancellationToken);
145+
}
146+
finally
147+
{
148+
stopwatch.Stop();
149+
// In real middleware, you'd log here - we just stop the timer for the benchmark
150+
}
151+
}
152+
}

0 commit comments

Comments
 (0)