Skip to content

Benchmark suite

bench/Pulse.Mqtt.Benchmarks covers the same ground as the upstream MQTTnet.Benchmarks project, scenario for scenario, so the two libraries can be profiled along the same axes. Every class carries MemoryDiagnoser; run any subset with:

dotnet run -c Release --project bench/Pulse.Mqtt.Benchmarks -- --filter <pattern> --job short

For the direct head-to-head numbers against MQTTnet over a real broker, see Benchmark-vs-MQTTnet.md.

Scenario map

MQTTnet benchmarkPulse counterpartWhat it measures
SerializerBenchmarkSerializerBenchmarksEncode and decode 10,000 minimal PUBLISH packets
MqttPacketReaderWriterBenchmarkPacketReaderWriterBenchmarks100,000 rounds of mixed field reads/writes
MqttBufferReaderBenchmarkStringDecodeBenchmarksLong UTF-8 string decode against the raw encoding call
TopicFilterComparerBenchmarkTopicFilterComparerBenchmarks100,000 rounds of eight representative filter matches
MemoryCopyBenchmarkMemoryCopyBenchmarksArray copy against span copy at codec block sizes
AsyncLockBenchmarkSendLockBenchmarksThe send-path lock: contended hand-off and raw acquire/release
LoggerBenchmarkLoggerBenchmarks10,000 log calls through source-generated LoggerMessage
ChannelAdapterBenchmarkConnectionBenchmarks10,000 packets through the packet engine, both directions
SendPacketAsyncBenchmarkPacketPipeBenchmarks.Send_Small_PacketOne small packet encoded and flushed to a PipeWriter
ReaderExtensionsBenchmarkPacketPipeBenchmarks.Decode_Large_PublishOne 10 KB PUBLISH framed and decoded from a PipeReader
MqttTcpChannelBenchmarkTcpTransportBenchmarks.Tcp_Send_10000_Chunks10,000 five-byte chunks over loopback TCP
TcpPipesBenchmarkTcpTransportBenchmarks.Loopback_Send_10000_ChunksThe same chunks over the in-memory transport pair
MessageProcessingBenchmarkMessageProcessingBenchmarks10,000 awaited QoS 0 publishes through the client to a broker
MessageDeliveryBenchmarkMessageDeliveryBenchmarksFan-out delivery to ten subscribers across a large topic space
SubscribeBenchmarkSubscribeBenchmarks10,000 awaited single-topic SUBSCRIBE round trips
UnsubscribeBenchmarkUnsubscribeBenchmarks10,000 awaited single-topic UNSUBSCRIBE round trips
VarIntBenchmarks, PacketBenchmarks(Pulse originals)Variable-byte integers; single-packet encode/decode and routing

Not ported: MessageProcessingMqttConnectionContextBenchmark (ASP.NET Core server transport — Pulse has no server), and RoundtripProcessingBenchmark / ServerProcessingBenchmark, whose bodies are commented out upstream and measure nothing.

Where MQTTnet's broker-based scenarios start MqttServer, the Pulse counterparts run against PulseMqttTestBroker over the in-memory loopback transport — the equivalent in-process broker this library ships for testing. The TCP scenarios use a real socket pair.

Results

ShortRun job (3 iterations), Intel Core i7-12700H, Windows 11, .NET 10.0.9, workstation GC. ShortRun error bars are wide; treat means as indicative, allocations as exact.

Codec and primitives

BenchmarkMeanAllocated
Serialize_10000_Messages596 µs (60 ns each)0 B
Deserialize_10000_Messages0.96 ms (96 ns each)144 B per packet
Read_100_000_Messages (11 fields each)12.7 ms360 B per round (the three decoded strings)
Write_100_000_Messages (11 fields each)18.8 ms0 B
Pulse_ReadString (long string)763 nsparity with Encoding.UTF8.GetString (0.97 ratio)
Match_100_000_Rounds (8 filters each)24.4 ms (~30 ns per match)0 B
Send_Small_Packet (pipe)405 ns416 B
Decode_Large_Publish (10 KB, pipe)2.71 µs11.1 KB (the payload copy)

Engine and locking

BenchmarkMeanAllocated
Connection Send_10000_Messages2.87 ms (287 ns each)0 B
Connection Receive_10000_Messages3.4–4.7 ms (run-to-run spread)145 B per packet
SendLock Wait_100_000_Times4.17 ms (42 ns per acquire/release)136 B total
SendLock Synchronize_100_Tasks1.55 s (dominated by the deliberate 5 ms hold)53 KB
Log_10000_Messages_NullLogger3.1 µs (0.3 ns each)0 B
Log_10000_Messages_Disabled_Level6.7 µs (0.7 ns each)0 B
Log_10000_Messages_Enabled_Sink731 µs104 B per call (the rendered string)

Client, broker, and transport

BenchmarkMeanAllocated
Send_10000_Messages (client to broker, QoS 0)8.65 ms (865 ns each)350 B per publish, both sides
DeliverMessages (10 subscribers × 5 topics)239 µs68 KB
DeliverMessages (10 subscribers × 50 topics)8.03 ms716 KB
Subscribe_10000_Topics119 ms19.2 MB
Unsubscribe_10000_Topics143 ms17.7 MB
Tcp_Send_10000_Chunks (5 B each, loopback TCP)149 ms0 B
Loopback_Send_10000_Chunks (in-memory pair)1.17 ms466 B
MemoryCopy Array vs Span (63–5095 B)parity within noise0 B

The suite's first run flagged two real costs. Subscribing topics one at a time snapshotted the durable subscription set per call — 1.92 s and 1.51 GB for 10,000 subscribes. Incremental subscription bookkeeping (dictionary state plus in-place session-store updates) brought that to 119 ms and 19.2 MB, and the same run motivated the single-pass publish/ack encoders that took serialization to zero allocation. The remaining gap between the TCP chunk drain and the in-memory pair is per-segment socket cost, the same physics any client pays on that path.

Last updated:

Released under the MIT License.