Repository navigation
Expand file tree
/
Copy pathQueuePullBenchmarks.cs
More file actions
202 lines (182 loc) · 6.93 KB
/
Copy pathQueuePullBenchmarks.cs
File metadata and controls
202 lines (182 loc) · 6.93 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Engines;
using BenchmarkDotNet.Order;
using BufferQueue;
using Microsoft.Extensions.DependencyInjection;
namespace RequestBatcher.Benchmarks.InMemory;
/// <summary>
/// Measures draining a prefilled BufferQueue pull consumer without producer, handler,
/// tracing, admission, or request-completion work in the timed region.
/// </summary>
[MemoryDiagnoser]
[RankColumn]
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
[SimpleJob(
RunStrategy.Throughput,
launchCount: 2,
warmupCount: 3,
iterationCount: 8,
invocationCount: 1,
id: "BufferQueuePull")]
public class QueuePullBenchmarks
{
private const int RequestCount = 4_096;
private const string TopicName = "request-batcher-queue-pull-benchmark";
private ServiceProvider? _provider;
private IBufferProducer<QueueItem>? _producer;
private IBufferPullConsumer<QueueItem>? _consumer;
private QueueItem[]? _items;
private int[]? _seen;
private int[] _partitionItems = [];
private int _consumedCount;
private int _batchCount;
private int _maximumBatchSize;
private int _synchronousMoveNextCount;
private int _validatedWaves;
private CancellationTokenSource? _timeout;
/// <summary>
/// Controls the maximum number of items returned by one consumer pull.
/// </summary>
[Params(32, 128)]
public int BatchSize { get; set; }
/// <summary>
/// Controls the number of memory topic partitions assigned to the one consumer.
/// </summary>
[Params(1, 16)]
public int PartitionCount { get; set; }
/// <summary>
/// Creates the real in-memory topic and its single pull consumer outside measurement.
/// </summary>
[GlobalSetup]
public void GlobalSetup()
{
var services = new ServiceCollection();
services.AddBufferQueue(queue => queue.UseMemory(memory =>
memory.AddTopic<QueueItem>(topic =>
{
topic.TopicName = TopicName;
topic.PartitionNumber = PartitionCount;
topic.SegmentSize = Math.Max(16, BatchSize);
topic.UsePartitionKey(static item => item.Id);
})));
_provider = services.BuildServiceProvider();
var queue = _provider.GetRequiredService<IBufferQueue>();
_producer = queue.GetProducer<QueueItem>(TopicName);
_consumer = queue.CreatePullConsumer<QueueItem>(
new BufferPullConsumerOptions
{
TopicName = TopicName,
GroupName = $"queue-pull-benchmark-{Guid.NewGuid():N}",
BatchSize = BatchSize,
AutoCommit = true,
});
_items = Enumerable.Range(0, RequestCount)
.Select(static id => new QueueItem(id))
.ToArray();
_seen = new int[RequestCount];
_partitionItems = new int[PartitionCount];
}
/// <summary>
/// Refills the topic before each measured invocation, outside the timed region.
/// </summary>
[IterationSetup]
public void IterationSetup()
{
Array.Clear(_seen!);
Array.Clear(_partitionItems);
_consumedCount = 0;
_batchCount = 0;
_maximumBatchSize = 0;
_synchronousMoveNextCount = 0;
_timeout = new CancellationTokenSource(TimeSpan.FromSeconds(30));
_producer!.ProduceAsync(_items!.AsMemory()).GetAwaiter().GetResult();
}
/// <summary>
/// Drains all prefilled records by awaiting the consumer's ValueTask pull operation.
/// </summary>
[Benchmark(OperationsPerInvoke = RequestCount)]
public async Task ConsumePrefilledQueueAsync()
{
await using var enumerator = _consumer!
.ConsumeAsync(_timeout!.Token)
.GetAsyncEnumerator(_timeout.Token);
while (_consumedCount < RequestCount)
{
var moveNext = enumerator.MoveNextAsync();
if (moveNext.IsCompletedSuccessfully)
{
_synchronousMoveNextCount++;
}
if (!await moveNext.ConfigureAwait(false))
{
throw new InvalidOperationException("The prefilled queue ended before all records were consumed.");
}
var batchSize = 0;
var partitionResidue = -1;
foreach (var item in enumerator.Current)
{
if ((uint)item.Id >= RequestCount)
{
throw new InvalidOperationException($"Unexpected queue item id: {item.Id}.");
}
_seen![item.Id]++;
if (batchSize == 0)
{
partitionResidue = item.Id % PartitionCount;
}
_consumedCount++;
batchSize++;
}
if (batchSize == 0 || batchSize > BatchSize)
{
throw new InvalidOperationException(
$"Observed invalid pull size {batchSize}; expected 1..{BatchSize}.");
}
_maximumBatchSize = Math.Max(_maximumBatchSize, batchSize);
_partitionItems[partitionResidue] += batchSize;
_batchCount++;
}
}
/// <summary>
/// Validates the result after measurement and reports sync completion separately from timed work.
/// </summary>
[IterationCleanup]
public void IterationCleanup()
{
_timeout!.Dispose();
if (_consumedCount != RequestCount || _seen!.Any(static count => count != 1)
|| _partitionItems.Any(count => count != RequestCount / PartitionCount))
{
throw new InvalidOperationException(
$"Expected {RequestCount} unique records, consumed {_consumedCount}.");
}
if (_batchCount == 0 || _maximumBatchSize > BatchSize)
{
throw new InvalidOperationException(
$"Observed invalid pull batches: count={_batchCount}, " +
$"maximum size={_maximumBatchSize}, limit={BatchSize}.");
}
if (_synchronousMoveNextCount != _batchCount)
{
throw new InvalidOperationException(
$"Expected every prefilled pull to complete synchronously, observed " +
$"{_synchronousMoveNextCount}/{_batchCount} synchronous MoveNextAsync calls.");
}
_validatedWaves++;
}
/// <summary>
/// Releases the in-memory queue after all benchmark launches for this parameter set finish.
/// </summary>
[GlobalCleanup]
public void GlobalCleanup()
{
Console.WriteLine(
$"QueuePull BatchSize={BatchSize}, Partitions={PartitionCount}, " +
$"Batches={_batchCount}, MaxBatch={_maximumBatchSize}, " +
$"SynchronousMoveNext={_synchronousMoveNextCount}/{_batchCount}, " +
$"Requests={_consumedCount}/{RequestCount}, ValidatedWaves={_validatedWaves}");
_provider?.Dispose();
}
private sealed record QueueItem(int Id);
}