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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user