perf: optimize MQTT cross-protocol path (0.30x → 0.78x Go)

Replace per-message async fire-and-forget with direct-buffer write loop
mirroring NatsClient pattern: SpinLock-guarded buffer append, double-
buffer swap, single WriteAsync per batch.

- MqttConnection: add _directBuf/_writeBuf + RunMqttWriteLoopAsync
- MqttConnection: add EnqueuePublishNoFlush (zero-alloc PUBLISH format)
- MqttPacketWriter: add WritePublishTo(Span<byte>) + MeasurePublish
- MqttTopicMapper: add NatsToMqttBytes with bounded ConcurrentDictionary
- MqttNatsClientAdapter: synchronous SendMessageNoFlush + SignalFlush
- Skip FlushAsync on plain TCP sockets (TCP auto-flushes)
This commit is contained in:
Joseph Doherty
2026-03-13 14:25:13 -04:00
parent 699449da6a
commit 11e01b9026
14 changed files with 1113 additions and 10 deletions

View File

@@ -146,6 +146,92 @@ public static class MqttPacketWriter
return Write(MqttControlPacketType.Publish, totalPayload, flags);
}
/// <summary>
/// Writes a complete MQTT PUBLISH packet directly into a destination span.
/// Returns the number of bytes written. Zero-allocation hot path for message delivery.
/// </summary>
public static int WritePublishTo(Span<byte> dest, ReadOnlySpan<byte> topicUtf8,
ReadOnlySpan<byte> payload, byte qos = 0, bool retain = false, bool dup = false, ushort packetId = 0)
{
// Calculate remaining length: 2 (topic len) + topic + optional 2 (packet id) + payload
var remainingLength = 2 + topicUtf8.Length + (qos > 0 ? 2 : 0) + payload.Length;
// Encode remaining length into scratch
Span<byte> rlScratch = stackalloc byte[4];
var rlLen = EncodeRemainingLengthTo(rlScratch, remainingLength);
var totalLen = 1 + rlLen + remainingLength;
// Fixed header byte
byte flags = 0;
if (dup) flags |= 0x08;
flags |= (byte)((qos & 0x03) << 1);
if (retain) flags |= 0x01;
dest[0] = (byte)(((byte)MqttControlPacketType.Publish << 4) | flags);
var pos = 1;
// Remaining length
rlScratch[..rlLen].CopyTo(dest[pos..]);
pos += rlLen;
// Topic name (length-prefixed)
BinaryPrimitives.WriteUInt16BigEndian(dest[pos..], (ushort)topicUtf8.Length);
pos += 2;
topicUtf8.CopyTo(dest[pos..]);
pos += topicUtf8.Length;
// Packet ID (only for QoS > 0)
if (qos > 0)
{
BinaryPrimitives.WriteUInt16BigEndian(dest[pos..], packetId);
pos += 2;
}
// Application payload
payload.CopyTo(dest[pos..]);
pos += payload.Length;
return totalLen;
}
/// <summary>
/// Calculates the total wire size of a PUBLISH packet without writing it.
/// </summary>
public static int MeasurePublish(int topicLen, int payloadLen, byte qos)
{
var remainingLength = 2 + topicLen + (qos > 0 ? 2 : 0) + payloadLen;
var rlLen = MeasureRemainingLength(remainingLength);
return 1 + rlLen + remainingLength;
}
internal static int EncodeRemainingLengthTo(Span<byte> dest, int value)
{
var index = 0;
do
{
var digit = (byte)(value % 128);
value /= 128;
if (value > 0)
digit |= 0x80;
dest[index++] = digit;
} while (value > 0);
return index;
}
internal static int MeasureRemainingLength(int value)
{
var count = 0;
do
{
value /= 128;
count++;
} while (value > 0);
return count;
}
internal static byte[] EncodeRemainingLength(int value)
{
if (value < 0 || value > MqttProtocolConstants.MaxPayloadSize)