perf: Phase 1 JetStream async file publish optimizations

- Add cached state properties (LastSeq, MessageCount, TotalBytes, FirstSeq)
  to IStreamStore/FileStore/MemStore — eliminates GetStateAsync on publish path
- Add Capture(StreamHandle, ...) overload to StreamManager — eliminates
  double FindBySubject lookup (once in JetStreamPublisher, once in Capture)
- Remove _messageIndexes dictionary from FileStore write path — all lookups
  now use _messages directly, saving ~48B allocation per message
- Add JetStreamPubAckFormatter for hand-rolled UTF-8 success ack formatting —
  avoids JsonSerializer overhead on the hot publish path
- Switch flush loop to exponential backoff (1→2→4→8ms) matching Go server
This commit is contained in:
Joseph Doherty
2026-03-13 15:09:21 -04:00
parent 82cc3ec841
commit 7404ecdb0e
7 changed files with 106 additions and 30 deletions

View File

@@ -1399,8 +1399,19 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable
// Go reference: server/jetstream.go — jsPubAckResponse sent to reply.
if (replyTo != null)
{
var ackData = JsonSerializer.SerializeToUtf8Bytes(pubAck, s_jetStreamJsonOptions);
ProcessMessage(replyTo, null, default, ackData, sender);
if (JetStream.Publish.JetStreamPubAckFormatter.IsSimpleSuccess(pubAck))
{
// Fast path: hand-rolled UTF-8 formatter avoids JsonSerializer overhead.
Span<byte> ackBuf = stackalloc byte[256];
var ackLen = JetStream.Publish.JetStreamPubAckFormatter.FormatSuccess(ackBuf, pubAck.Stream, pubAck.Seq);
ProcessMessage(replyTo, null, default, ackBuf[..ackLen].ToArray(), sender);
}
else
{
var ackData = JsonSerializer.SerializeToUtf8Bytes(pubAck, s_jetStreamJsonOptions);
ProcessMessage(replyTo, null, default, ackData, sender);
}
return;
}
}