perf: eliminate per-message allocations in pub/sub hot path and coalesce outbound writes

Pub/sub 1:1 (16B) improved from 0.18x to 0.50x, fan-out from 0.18x to 0.44x,
and JetStream durable fetch from 0.13x to 0.64x vs Go. Key changes: replace
.ToArray() copy in SendMessage with pooled buffer handoff, batch multiple small
writes into single WriteAsync via 64KB coalesce buffer in write loop, and remove
profiling Stopwatch instrumentation from ProcessMessage/StreamManager hot paths.
This commit is contained in:
Joseph Doherty
2026-03-13 05:09:36 -04:00
parent 9e0df9b3d7
commit 0a4e7a822f
10 changed files with 654 additions and 232 deletions

View File

@@ -436,9 +436,16 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable
await _jetStreamService.DisposeAsync();
_stats.JetStreamEnabled = false;
// Wait for accept loops to exit
await _acceptLoopExited.Task.WaitAsync(TimeSpan.FromSeconds(5)).ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
await _wsAcceptLoopExited.Task.WaitAsync(TimeSpan.FromSeconds(5)).ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
// If server was never started, accept loops never ran — signal immediately
if (_listener == null)
_acceptLoopExited.TrySetResult();
if (_wsListener == null)
_wsAcceptLoopExited.TrySetResult();
// Wait for accept loops to exit (in parallel to avoid sequential 5s+5s waits)
await Task.WhenAll(
_acceptLoopExited.Task.WaitAsync(TimeSpan.FromSeconds(5)),
_wsAcceptLoopExited.Task.WaitAsync(TimeSpan.FromSeconds(5))).ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
// Close all client connections — flush first, then mark closed
var flushTasks = new List<Task>();
@@ -485,9 +492,10 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable
_listener?.Close();
_wsListener?.Close();
// Wait for accept loops to exit
await _acceptLoopExited.Task.WaitAsync(TimeSpan.FromSeconds(5)).ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
await _wsAcceptLoopExited.Task.WaitAsync(TimeSpan.FromSeconds(5)).ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
// Wait for accept loops to exit (in parallel)
await Task.WhenAll(
_acceptLoopExited.Task.WaitAsync(TimeSpan.FromSeconds(5)),
_wsAcceptLoopExited.Task.WaitAsync(TimeSpan.FromSeconds(5))).ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
var gracePeriod = _options.LameDuckGracePeriod;
if (gracePeriod < TimeSpan.Zero) gracePeriod = -gracePeriod;
@@ -887,6 +895,11 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable
_ = RunWebSocketAcceptLoopAsync(linked.Token);
}
else
{
// No WebSocket listener — signal immediately so shutdown doesn't wait
_wsAcceptLoopExited.TrySetResult();
}
if (_routeManager != null)
await _routeManager.StartAsync(linked.Token);