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:
@@ -28,7 +28,8 @@ public sealed class FileStore : IStreamStore, IAsyncDisposable, IDisposable
|
||||
// In-memory cache: keyed by sequence number. This is the primary data structure
|
||||
// for reads and queries. The blocks are the on-disk persistence layer.
|
||||
private readonly Dictionary<ulong, StoredMessage> _messages = new();
|
||||
private readonly Dictionary<ulong, StoredMessageIndex> _messageIndexes = new();
|
||||
// _messageIndexes removed — all lookups now use _messages directly to avoid
|
||||
// per-message StoredMessageIndex allocation on the write path.
|
||||
private readonly Dictionary<string, ulong> _lastSequenceBySubject = new(StringComparer.Ordinal);
|
||||
|
||||
// Block-based storage: the active (writable) block and sealed blocks.
|
||||
@@ -89,6 +90,12 @@ public sealed class FileStore : IStreamStore, IAsyncDisposable, IDisposable
|
||||
public int BlockCount => _blocks.Count;
|
||||
public bool UsedIndexManifestOnStartup { get; private set; }
|
||||
|
||||
// IStreamStore cached state properties — O(1), maintained incrementally.
|
||||
public ulong LastSeq => _last;
|
||||
public ulong MessageCount => _messageCount;
|
||||
public ulong TotalBytes => _totalBytes;
|
||||
ulong IStreamStore.FirstSeq => _messageCount == 0 ? (_first > 0 ? _first : 0UL) : _firstSeq;
|
||||
|
||||
public FileStore(FileStoreOptions options)
|
||||
{
|
||||
_options = options;
|
||||
@@ -266,7 +273,6 @@ public sealed class FileStore : IStreamStore, IAsyncDisposable, IDisposable
|
||||
public ValueTask RestoreSnapshotAsync(ReadOnlyMemory<byte> snapshot, CancellationToken ct)
|
||||
{
|
||||
_messages.Clear();
|
||||
_messageIndexes.Clear();
|
||||
_lastSequenceBySubject.Clear();
|
||||
_last = 0;
|
||||
_messageCount = 0;
|
||||
@@ -415,7 +421,6 @@ public sealed class FileStore : IStreamStore, IAsyncDisposable, IDisposable
|
||||
{
|
||||
var count = (ulong)_messages.Count;
|
||||
_messages.Clear();
|
||||
_messageIndexes.Clear();
|
||||
_lastSequenceBySubject.Clear();
|
||||
_generation++;
|
||||
_last = 0;
|
||||
@@ -542,7 +547,6 @@ public sealed class FileStore : IStreamStore, IAsyncDisposable, IDisposable
|
||||
{
|
||||
// Truncate to nothing.
|
||||
_messages.Clear();
|
||||
_messageIndexes.Clear();
|
||||
_lastSequenceBySubject.Clear();
|
||||
_generation++;
|
||||
_last = 0;
|
||||
@@ -845,7 +849,6 @@ public sealed class FileStore : IStreamStore, IAsyncDisposable, IDisposable
|
||||
private void TrackMessage(StoredMessage message)
|
||||
{
|
||||
_messages[message.Sequence] = message;
|
||||
_messageIndexes[message.Sequence] = message.ToIndex();
|
||||
_lastSequenceBySubject[message.Subject] = message.Sequence;
|
||||
_messageCount++;
|
||||
_totalBytes += (ulong)(message.RawHeaders.Length + message.Payload.Length);
|
||||
@@ -861,8 +864,6 @@ public sealed class FileStore : IStreamStore, IAsyncDisposable, IDisposable
|
||||
{
|
||||
if (!_messages.Remove(sequence, out var message))
|
||||
return false;
|
||||
|
||||
_messageIndexes.Remove(sequence);
|
||||
_messageCount--;
|
||||
_totalBytes -= (ulong)(message.RawHeaders.Length + message.Payload.Length);
|
||||
UpdateLastSequenceForSubject(message.Subject, sequence);
|
||||
@@ -890,7 +891,7 @@ public sealed class FileStore : IStreamStore, IAsyncDisposable, IDisposable
|
||||
private void AdvanceFirstSequence(ulong start)
|
||||
{
|
||||
var candidate = start;
|
||||
while (!_messageIndexes.ContainsKey(candidate) && candidate <= _last)
|
||||
while (!_messages.ContainsKey(candidate) && candidate <= _last)
|
||||
candidate++;
|
||||
|
||||
if (candidate <= _last)
|
||||
@@ -911,7 +912,7 @@ public sealed class FileStore : IStreamStore, IAsyncDisposable, IDisposable
|
||||
|
||||
for (var seq = startExclusive - 1; ; seq--)
|
||||
{
|
||||
if (_messageIndexes.ContainsKey(seq))
|
||||
if (_messages.ContainsKey(seq))
|
||||
return seq;
|
||||
|
||||
if (seq == 0)
|
||||
@@ -926,7 +927,7 @@ public sealed class FileStore : IStreamStore, IAsyncDisposable, IDisposable
|
||||
|
||||
for (var seq = removedSequence - 1; ; seq--)
|
||||
{
|
||||
if (_messageIndexes.TryGetValue(seq, out var candidate) && string.Equals(candidate.Subject, subject, StringComparison.Ordinal))
|
||||
if (_messages.TryGetValue(seq, out var candidate) && string.Equals(candidate.Subject, subject, StringComparison.Ordinal))
|
||||
{
|
||||
_lastSequenceBySubject[subject] = seq;
|
||||
return;
|
||||
@@ -941,7 +942,6 @@ public sealed class FileStore : IStreamStore, IAsyncDisposable, IDisposable
|
||||
|
||||
private void RebuildIndexesFromMessages()
|
||||
{
|
||||
_messageIndexes.Clear();
|
||||
_lastSequenceBySubject.Clear();
|
||||
_messageCount = 0;
|
||||
_totalBytes = 0;
|
||||
@@ -2317,12 +2317,16 @@ public sealed class FileStore : IStreamStore, IAsyncDisposable, IDisposable
|
||||
if (block is null)
|
||||
continue;
|
||||
|
||||
var waited = 0;
|
||||
while (block.PendingWriteSize < CoalesceMinimum && waited < MaxFlushWaitMs)
|
||||
// Go-style exponential backoff: 1→2→4→8ms (vs linear 1ms × 8).
|
||||
var waitMs = 1;
|
||||
var totalWaited = 0;
|
||||
while (block.PendingWriteSize < CoalesceMinimum && totalWaited < MaxFlushWaitMs)
|
||||
{
|
||||
try { await Task.Delay(1, ct); }
|
||||
var delay = Math.Min(waitMs, MaxFlushWaitMs - totalWaited);
|
||||
try { await Task.Delay(delay, ct); }
|
||||
catch (OperationCanceledException) { break; }
|
||||
waited++;
|
||||
totalWaited += delay;
|
||||
waitMs *= 2;
|
||||
}
|
||||
|
||||
block.FlushPending();
|
||||
|
||||
Reference in New Issue
Block a user