# NATS.Server.Benchmark.Tests Side-by-side performance comparison of the Go and .NET NATS server implementations. Both servers are launched as child processes on ephemeral ports and exercised with identical workloads using the `NATS.Client.Core` / `NATS.Client.JetStream` NuGet client libraries. ## Prerequisites - .NET 10 SDK - Go toolchain (for Go server comparison; benchmarks still run .NET-only if Go is unavailable) - The Go NATS server source at `golang/nats-server/` (the Go binary is built automatically on first run) ## Running Benchmarks All benchmark tests are tagged with `[Trait("Category", "Benchmark")]`, so a plain `dotnet test` against the solution will **not** run them. Use the `--filter` flag. ```bash # Run all benchmarks dotnet test tests/NATS.Server.Benchmark.Tests --filter "Category=Benchmark" -v normal # Core pub/sub only dotnet test tests/NATS.Server.Benchmark.Tests --filter "Category=Benchmark&FullyQualifiedName~CorePubSub" -v normal # Request/reply only dotnet test tests/NATS.Server.Benchmark.Tests --filter "Category=Benchmark&FullyQualifiedName~RequestReply" -v normal # JetStream only dotnet test tests/NATS.Server.Benchmark.Tests --filter "Category=Benchmark&FullyQualifiedName~JetStream" -v normal # A single benchmark by name dotnet test tests/NATS.Server.Benchmark.Tests --filter "FullyQualifiedName=NATS.Server.Benchmark.Tests.CorePubSub.SinglePublisherThroughputTests.PubNoSub_16B" -v normal ``` Use `-v normal` or `--logger "console;verbosity=detailed"` to see the comparison output in the console. Without verbosity, xUnit suppresses `ITestOutputHelper` output for passing tests. ## Benchmark List | Test Class | Test Method | What It Measures | |---|---|---| | `SinglePublisherThroughputTests` | `PubNoSub_16B` | Publish-only throughput, 16-byte payload | | `SinglePublisherThroughputTests` | `PubNoSub_128B` | Publish-only throughput, 128-byte payload | | `PubSubOneToOneTests` | `PubSub1To1_16B` | 1 publisher, 1 subscriber, 16-byte payload | | `PubSubOneToOneTests` | `PubSub1To1_16KB` | 1 publisher, 1 subscriber, 16 KB payload | | `FanOutTests` | `FanOut1To4_128B` | 1 publisher, 4 subscribers, 128-byte payload | | `MultiPubSubTests` | `MultiPubSub4x4_128B` | 4 publishers, 4 subscribers, 128-byte payload | | `SingleClientLatencyTests` | `RequestReply_SingleClient_128B` | Request/reply round-trip latency, 1 client, 1 service | | `MultiClientLatencyTests` | `RequestReply_10Clients2Services_16B` | Request/reply latency, 10 concurrent clients, 2 queue-group services | | `SyncPublishTests` | `JSSyncPublish_16B_MemoryStore` | JetStream synchronous publish, memory-backed stream | | `AsyncPublishTests` | `JSAsyncPublish_128B_FileStore` | JetStream async batch publish, file-backed stream | | `OrderedConsumerTests` | `JSOrderedConsumer_Throughput` | JetStream ordered ephemeral consumer read throughput | | `DurableConsumerFetchTests` | `JSDurableFetch_Throughput` | JetStream durable consumer fetch-in-batches throughput | ## Output Format Each test writes a side-by-side comparison to xUnit's test output: ``` === Single Publisher (16B) === Go: 2,436,416 msg/s | 37.2 MB/s | 41 ms .NET: 1,425,767 msg/s | 21.8 MB/s | 70 ms Ratio: 0.59x (.NET / Go) ``` Request/reply tests also include latency percentiles: ``` Latency (us): P50 P95 P99 Min Max Go 104.5 124.2 146.2 82.8 1204.5 .NET 134.7 168.0 190.5 91.6 3469.5 ``` If Go is not available, only the .NET result is printed. ## Updating benchmarks_comparison.md After running the full benchmark suite, update `benchmarks_comparison.md` in the repository root with the new numbers: 1. Run all benchmarks with detailed output: ```bash dotnet test tests/NATS.Server.Benchmark.Tests \ --filter "Category=Benchmark" \ -v normal \ --logger "console;verbosity=detailed" 2>&1 | tee /tmp/bench-output.txt ``` 2. Open `/tmp/bench-output.txt` and extract the comparison blocks from the "Standard Output Messages" sections. 3. Update the tables in `benchmarks_comparison.md` with the new msg/s, MB/s, ratio, and latency values. Update the date on the first line and the environment description. 4. Review the Summary table and Key Observations — update assessments if ratios have changed significantly. ## Architecture ``` Infrastructure/ PortAllocator.cs # Ephemeral port allocation (bind to port 0) DotNetServerProcess.cs # Builds + launches NATS.Server.Host GoServerProcess.cs # Builds + launches golang/nats-server CoreServerPairFixture.cs # IAsyncLifetime: Go + .NET servers for core tests JetStreamServerPairFixture # IAsyncLifetime: Go + .NET servers with JetStream Collections.cs # xUnit collection definitions Harness/ BenchmarkResult.cs # Result record (msg/s, MB/s, latencies) LatencyTracker.cs # Pre-allocated sample buffer, percentile math BenchmarkRunner.cs # Warmup + timed measurement loop BenchmarkResultWriter.cs # Formats side-by-side comparison output ``` Server pair fixtures start both servers once per xUnit collection and expose `CreateGoClient()` / `CreateDotNetClient()` factory methods. Test parallelization is disabled at the assembly level (`AssemblyInfo.cs`) to prevent resource contention between collections. ## Notes - The Go binary at `golang/nats-server/nats-server` is built automatically on first run via `go build`. Subsequent runs reuse the cached binary. - If Go is not installed or `golang/nats-server/` does not exist, Go benchmarks are skipped and only .NET results are reported. - JetStream tests create temporary store directories under `$TMPDIR` and clean them up on teardown. - Message counts are intentionally conservative (2K–10K per test) to keep the full suite under 2 minutes while still producing meaningful throughput ratios. For higher-fidelity numbers, increase the counts in individual test methods or the `BenchmarkRunner` configuration.