Add E2E benchmark project and throughput scenarios for Surreal-backed peers
All checks were successful
NuGet Package Publish / nuget (push) Successful in 1m16s

This commit is contained in:
Joseph Doherty
2026-02-22 05:59:38 -05:00
parent bd10914828
commit c06b56172a
5 changed files with 361 additions and 13 deletions

View File

@@ -0,0 +1,135 @@
using BenchmarkDotNet.Attributes;
using System.Net;
using System.Net.Sockets;
using ZB.MOM.WW.CBDDC.Core.Network;
using ZB.MOM.WW.CBDDC.Sample.Console;
namespace ZB.MOM.WW.CDBBC.E2E.Benchmark.Tests;
[MemoryDiagnoser]
[SimpleJob(launchCount: 1, warmupCount: 1, iterationCount: 3)]
public class E2EThroughputBenchmarks
{
private const int BatchSize = 50;
private BenchmarkPeerNode _nodeA = null!;
private BenchmarkPeerNode _nodeB = null!;
private int _sequence;
[GlobalSetup]
public async Task GlobalSetupAsync()
{
int nodeAPort = GetAvailableTcpPort();
int nodeBPort = GetAvailableTcpPort();
while (nodeBPort == nodeAPort)
nodeBPort = GetAvailableTcpPort();
string clusterToken = Guid.NewGuid().ToString("N");
_nodeA = BenchmarkPeerNode.Create(
"benchmark-node-a",
nodeAPort,
clusterToken,
[
new KnownPeerConfiguration
{
NodeId = "benchmark-node-b",
Host = "127.0.0.1",
Port = nodeBPort
}
]);
_nodeB = BenchmarkPeerNode.Create(
"benchmark-node-b",
nodeBPort,
clusterToken,
[
new KnownPeerConfiguration
{
NodeId = "benchmark-node-a",
Host = "127.0.0.1",
Port = nodeAPort
}
]);
await _nodeA.StartAsync();
await _nodeB.StartAsync();
// Allow initial network loop to settle before measurements.
await Task.Delay(500);
}
[GlobalCleanup]
public Task GlobalCleanupAsync()
{
// Explicit Surreal embedded disposal can race native callbacks in benchmark child processes.
// Benchmarks run out-of-process, so process teardown is used for cleanup stability.
return Task.CompletedTask;
}
[Benchmark(Description = "Local write throughput", OperationsPerInvoke = BatchSize)]
public async Task LocalWriteThroughput()
{
IReadOnlyList<string> userIds = NextUserIds("local");
foreach (string userId in userIds)
await _nodeA.UpsertUserAsync(CreateUser(userId));
}
[Benchmark(Description = "Cross-node replicated throughput", OperationsPerInvoke = BatchSize)]
public async Task ReplicatedWriteThroughput()
{
IReadOnlyList<string> userIds = NextUserIds("replicated");
foreach (string userId in userIds)
await _nodeA.UpsertUserAsync(CreateUser(userId));
await WaitForReplicationAsync(userIds, TimeSpan.FromSeconds(30));
}
private IReadOnlyList<string> NextUserIds(string prefix)
{
int start = Interlocked.Add(ref _sequence, BatchSize) - BatchSize;
string[] ids = new string[BatchSize];
for (var i = 0; i < BatchSize; i++)
ids[i] = $"{prefix}-{start + i:D8}";
return ids;
}
private static User CreateUser(string userId)
{
return new User
{
Id = userId,
Name = $"user-{userId}",
Age = 30,
Address = new Address { City = "BenchmarkCity" }
};
}
private async Task WaitForReplicationAsync(IReadOnlyList<string> userIds, TimeSpan timeout)
{
DateTime deadline = DateTime.UtcNow.Add(timeout);
while (DateTime.UtcNow < deadline)
{
bool allPresent = true;
foreach (string userId in userIds)
if (!_nodeB.ContainsUser(userId))
{
allPresent = false;
break;
}
if (allPresent) return;
await Task.Delay(25);
}
throw new TimeoutException($"Timed out waiting for replication of {userIds.Count} users.");
}
private static int GetAvailableTcpPort()
{
using var listener = new TcpListener(IPAddress.Loopback, 0);
listener.Start();
return ((IPEndPoint)listener.LocalEndpoint).Port;
}
}