Add Framing/JsonFraming throughput and allocation benchmarks#8202
Merged
Aaronontheweb merged 2 commits intoMay 10, 2026
Conversation
Adds FramingBenchmarks to Akka.Benchmarks covering the four hot paths in
the framing system: SimpleFramingProtocol encode, SimpleFramingProtocol
decode (encode→decode round-trip), Delimiter decode, and JsonFraming
ObjectScanner with multi-chunk input.
Each benchmark uses a deferred-source pattern (Source.FromTask gated by
a TaskCompletionSource, then ConcatMany into the actual data source) so
materializer overhead — actor spawning, stage fusing, dispatcher
attachment, sink subscription — happens in IterationSetup and is excluded
from the measurement window. The benchmark method only trips the gate
and awaits drain. With OperationsPerInvoke=10000, the two fixed hops
(gate-signal + sink-complete) dilute to noise and the reported Allocated
column reads as bytes-per-framed-message.
JSON input is built so chunks straddle object boundaries — most Offer
calls hit JsonObjectParser with a non-empty buffer, exercising the
concat path rather than the empty-buffer fast path.
Parameters: MessageSize ∈ {64, 1024} bytes, fixed MessageCount = 10000.
Uses ThroughputBenchmarkConfig (MemoryDiagnoser, GitHub markdown export,
Req/sec column).
A first run on dev surfaced two issues: 1. SimpleFramingProtocolDecoder(int.MaxValue) overflows internally — the decoder adds 4 to the limit (header bytes), pushing it negative, which fails every frame with "Maximum allowed frame size is -2147483645 but decoded frame header reported size N". Cap at 16 MB instead, well clear of int.MaxValue + 4. 2. With MessageCount = 10000, iteration time on a fast machine is 2-50 ms — below BDN's recommended 100 ms floor, so individual measurements are noisy (bimodal distributions, large StdDev). Bump to 100,000 to push iteration time into the 30-500 ms band for cleaner statistical signal. Total run time still ~3 minutes; the benchmark stays self-contained.
Aaronontheweb
added a commit
to Aaronontheweb/akka.net
that referenced
this pull request
May 10, 2026
The ByteString-flavored FramingBenchmarks.cs came in via the dev → PR merge (akkadotnet#8202 landed first on dev). That branch retired ByteString as part of this PR, so the benchmark fails to compile here. Swap to the ReadOnlySequence<byte> variant that matches the post-migration types on this branch. Same benchmark structure and parameters as the dev version (deferred- source pattern, MessageCount=100K, MessageSize ∈ {64, 1024}); only the buffer-type construction and consumption sites differ.
Aaronontheweb
added a commit
that referenced
this pull request
May 10, 2026
…dOnlySequence<byte> migration, .NET 10 TFM (#8132) * Initialize OpenSpec framework for spec-driven development Add OpenSpec CLI skills and prompts for Claude Code and GitHub Copilot to support formal specification workflows for the Akka.NET 1.6 transport and serialization epic. * Add OpenSpec proposal: modernize-akka-io-tcp Spec 1 of the Akka.NET 1.6 transport/serialization epic. Replaces ByteString with System.Memory types and SocketAsyncEventArgs with Stream + System.IO.Pipelines in Akka.IO TCP actors. Includes proposal, design, capability specs (system-memory-io, stream-pipe-transport), and implementation task breakdown. * Add OpenSpec proposal: akka-io-tls-support Spec 2 of the Akka.NET 1.6 transport/serialization epic. Adds TLS support at the Akka.IO level via TlsStreamProvider, leveraging the IStreamProvider abstraction from Spec 1. All existing DotNetty TLS HOCON configuration works unchanged. * Add OpenSpec proposal: streams-tcp-transport Spec 3 of the Akka.NET 1.6 transport/serialization epic. Replaces DotNetty with an Akka.Streams TCP-based transport. Features integrated framing + serialization via FrameBufferWriter (IBufferWriter<byte>), binary PDU encoding, and full DotNetty HOCON config compatibility. * Add OpenSpec proposals: serializer-v2 and transport-performance Spec 4: SerializerV2 with IBufferWriter<byte>/ReadOnlySequence<byte> API, SerializerV1Adapter, MessagePackSerializer, mechanical port of internal Protobuf serializers. Source generator deferred. Spec 5: Performance validation using RemotePingPong benchmark. New transport must exceed DotNetty. Covers flush batching, Pipe tuning, buffer pool optimization, and continuous benchmark tracking. * Add implementation order for Akka.NET 1.6 transport epic Defines 5 sequential milestones with branches, completion criteria, and orchestration strategy. Includes DotNetty performance baseline (~680K msgs/sec peak on .NET 10). Each milestone is reviewed by a human before proceeding to the next. * Add OpenProse milestone runner for orchestrated implementation Opus captain orchestrates Sonnet workers through OpenSpec task lists. Reads tasks.md, dispatches task groups, verifies builds, fixes errors in a loop, runs tests, and archives the change on completion. Designed to execute one milestone at a time with human review between milestones. * Remove branch creation from milestone runner Branch should be created manually before invoking the program, not by the orchestrator itself. * Migrate all projects to net10.0 single-target - Replace netstandard2.0 + net6.0 multi-targeting with net10.0 only - Remove NetLibVersion and NetFrameworkTestVersion from Directory.Build.props - Remove all netstandard-conditional ItemGroup blocks from csproj files - Remove Polyfill package references (no longer needed on net10.0) - Remove BCL packages now included in net10.0 (System.Collections.Immutable, etc.) - Suppress SYSLIB0050/0051 obsolete serialization warnings - Update OpenSpec files to reflect net10.0 decision - Update OpenProse milestone-runner to commit after each task group * Delete ByteString and migrate Akka.IO to ReadOnlyMemory<byte> - Change Tcp.Write.Data and Tcp.Received.Data from ByteString to ReadOnlyMemory<byte> - Update all Write.Create() factory overloads for ReadOnlyMemory<byte> - Delete Akka.Util.ByteString class entirely - Move ByteOrder enum to ByteHelpers.cs - Fix TcpConnection send/receive to work with ReadOnlyMemory<byte> - Remove ByteString-based SocketAsyncEventArgs extensions - Migrate Udp and UdpConnected message types to ReadOnlyMemory<byte> - Fix ChunkedMessage and ProducerController/ConsumerController delivery code - Akka.csproj builds with 0 errors, 0 warnings * Migrate Akka.Streams, Akka.Remote, and Akka.Cluster from ByteString Akka.Streams: - Replace all ByteString with ReadOnlyMemory<byte> in DSL, TcpStages, FileIO, StreamConverters, Framing, JsonFraming - Rewrite DelimiterFramingStage and LengthFieldFramingStage for Memory<byte> - Rewrite JsonObjectParser for ReadOnlyMemory<byte> with Span-based access - Update IOSources, IOSinks, FilePublisher/Subscriber, InputStreamPublisher Akka.Remote: - Fix ByteOrder ambiguity in DotNetty transport settings (DotNetty vs Akka.Util) - Most ByteString references were already Google.Protobuf.ByteString (unchanged) Akka.Cluster: - Update ReliableDeliverySerializer for ReadOnlyMemory<byte> ChunkedMessage All library projects build with 0 errors, 0 warnings. * Add IStreamProvider abstraction and TcpStreamProvider - Create IStreamProvider interface with ConnectAsync/Close contract - Create TcpStreamProvider: plaintext NetworkStream from connected Socket - Update TcpOutgoingConnection to accept IStreamProvider (defaults to TcpStreamProvider) - Update TcpListener to wrap accepted sockets in NetworkStream - Update TcpIncomingConnection to accept Stream parameter - Stream+Pipe usage deferred to Task Group 4; existing SAEA code untouched * Rewrite TcpConnection from SAEA to Stream + Pipe Replace SocketAsyncEventArgs-based I/O with three background tasks coordinated through the actor mailbox (TurboMQTT pattern): - ReadFromStreamAsync: stream.ReadAsync → PipeWriter with backpressure - ReadFromPipeAsync: PipeReader → byte[] copy → Tcp.Received delivery - WriteToStreamAsync: Channel<WriteCommand> → stream.WriteAsync → ACK Flow control: - SuspendReading/ResumeReading via SemaphoreSlim gate - Pull mode: auto-suspend after each Tcp.Received delivery Shutdown sequences: - Tcp.Close: flush writes → cancel reads → close stream → Tcp.Closed - Tcp.Abort: immediate CTS cancel → RST → Tcp.Aborted - Tcp.ConfirmedClose: FIN → await peer FIN → Tcp.ConfirmedClosed - EOF: PipeWriter complete → Tcp.PeerClosed - I/O error: Tcp.ErrorClosed with cause message Lifecycle: Task.WhenAll tracking, Interlocked.CompareExchange CTS guard, self-tell before caller-tell ordering. Buffers/ and SocketEventArgsPool retained for UDP usage. * Migrate all test and benchmark projects from ByteString - Fix ~198 ByteString compilation errors across 7 test/benchmark projects - Akka.Streams.Tests: migrate TcpSpec, FileSourceSpec, JsonFramingSpec, InputStreamSourceSpec, OutputStreamSinkSpec, FlowGroupBySpec, BugSpec, etc. - Akka.Streams.Tests.TCK: update ByteString type references - Akka.Docs.Tests: update TelnetClient, EchoConnection, StreamTcpDocTests - Akka.Benchmarks: delete ByteStringBenchmarks.cs, fix TcpOperationsBenchmarks - Akka.Cluster.Tests: fix ReliableDeliverySerializerSpecs - Akka.Tests: fix remaining ByteString references - Full solution builds with 0 errors, 0 warnings * Fix TcpConnection Stream+Pipe: actor-driven reads, Props, blocking sockets Major fixes to the Stream+Pipe TcpConnection rewrite: - Remove SemaphoreSlim gate — replace with actor-driven PipeTo pattern for pipe reads. All flow control is now in the actor's message loop, no cross-thread synchronization needed. - Fix TcpOutgoingConnection Props creation (reflection needs explicit args) - Set Socket.Blocking=true before wrapping in NetworkStream - Fix EOF detection: detect from PipeReader.IsCompleted instead of racing StreamEof self-tell with buffered pipe data - Pull mode works correctly: each ResumeReading triggers one pipe read 12/22 TcpSpec tests passing (7 remaining are close/abort/error shutdown) * Fix TcpConnection shutdown coordination and race conditions - Never complete PipeWriter with exception (causes PipeReader.ReadAsync to throw, bypassing actor message loop and losing buffered data) - Wrap RequestPipeRead in try-catch for defense in depth - Handle cancelled pipe reads (send IoTaskFailed instead of silent drop) - Track IoTasksCompleted with boolean flag to prevent message loss across behaviour transitions - Add TryFinishClose() for coordinated shutdown readiness checks - Add StreamEof handler to PeerSentEofBehaviour (prevent dead letters) - Fix HandleConfirmedClose to always wait for WritesFlushed - Add volatile _readStreamHasError flag for cross-thread error detection - Add drain read when pipe completes with buffered data 18/19 TcpSpec tests passing (1 intermittent in batch, 3 pre-skipped) * Fix TcpConnection self-tells, close sequencing, and write batching * Fix TcpConnection preregistration buffering semantics * Remove TcpOutgoingConnection DNS fallback retries * Refresh TCP approvals and retire netfx PR validation * Fix 4 CI test failures from ByteString migration - ChunkedMessage: implement IEquatable with Span.SequenceEqual for ReadOnlyMemory<byte> content equality (ReliableDeliverySerializer tests) - TcpOutgoingConnection: pass DnsEndPoint directly to Socket.ConnectAsync for dual-stack sockets instead of manual resolution (DNS endpoint test) - DotNetty TLS tests: handle TargetInvocationException wrapping and CryptographicException from .NET 10's X509CertificateLoader - DeltaPropagationSelector: clamp Slice length to prevent ArgumentOutOfRangeException in round-robin propagation * Fix DNS endpoint error reporting and update API approval baselines - Revert dual-stack DnsEndPoint passthrough to Socket.ConnectAsync; always use Akka's async DNS resolver for consistent cross-platform behavior (Windows DNS timeout for unresolvable hosts is too slow) - Update CoreAPISpec approval baselines for all public API changes (ByteString removal, ChunkedMessage IEquatable, IStreamProvider, etc.) * Fix DNS endpoint IPv6 address selection for dual-stack sockets Prefer IPv6 addresses for InterNetworkV6 sockets in CreateEndpoint. Map IPv4 addresses to IPv6 (::ffff:x.x.x.x) as fallback so dual-stack sockets can still reach IPv4-only hosts. * Fix DNS endpoint test: revert to IPv4-preferred for dual-stack sockets Dual-stack sockets prefer IPv4 mapped to IPv6 (::ffff:x.x.x.x) to match dev behavior — most servers bind to IPv4 loopback. Remove the IPv6 test variant that was added but never worked (DNS-based connect to a specific-family server is inherently unreliable). * Use Socket.ConnectAsync(DnsEndPoint) for DNS connections Pass DnsEndPoint directly to Socket.ConnectAsync so the runtime tries all resolved addresses (IPv4 + IPv6) until one connects. This is the correct approach for dual-stack sockets — eliminates the single-address guessing in CreateEndpoint that couldn't handle mismatched server/client address families. - Remove Resolving behaviour and CreateEndpoint (no longer needed) - Restore IPv6 test variant with skip for systems without ::1 in DNS - Fix unresolvable-host test to use connect timeout instead of relying on platform-specific DNS failure speed * Eliminate unnecessary allocations: use Span/Memory overloads and BinaryPrimitives - FileSubscriber: use FileStream.Write(ReadOnlySpan<byte>) instead of .ToArray() - OutputStreamSubscriber: use Stream.Write(ReadOnlySpan<byte>) instead of .ToArray() - UdpConnection: use Socket.Send(ReadOnlySpan<byte>) instead of .ToArray() - WithUdpSend: use Socket.SendTo(ReadOnlySpan<byte>) instead of .ToArray() - Framing: replace IEnumerator-based length decoders with BinaryPrimitives span-based decoding (eliminates array + enumerator allocation per frame) - Framing: use BinaryPrimitives.WriteInt32BigEndian for header encoding * Migrate Tcp.Write.Data to ReadOnlySequence<byte> for zero-copy write path Change Tcp.Write.Data from ReadOnlyMemory<byte> to ReadOnlySequence<byte> to enable zero-copy framing in Milestone 4. The FrameBufferWriter will chain length-prefix + payload segments without flattening into a single allocation. - Write.Data type: ReadOnlyMemory<byte> -> ReadOnlySequence<byte> - Add Create(ReadOnlySequence<byte>) overloads - Keep Create(ReadOnlyMemory<byte>) and Create(byte[]) as convenience wrappers that create single-segment sequences - WriteBatchToStreamAsync: iterate sequence segments for both single- command fast path and multi-command batching path - Update API approval baselines * Spike: ITransportConnection with duplex pipe write path Replace Channel<WriteCommand> + direct stream writes with a duplex pipe architecture encapsulated in ITransportConnection. The transport owns both read and write pipes plus their pump loops, handling all buffer management and flush batching internally. Key changes: - Add ITransportConnection interface with WriteAsync, FlushAsync, ShutdownAsync, CloseAsync, Abort, plus ReadCompleted/WriteCompleted task observability and HasReadError/ReadError for error propagation - Add TcpTransportConnection implementation with read pump (Stream -> Input pipe) and write pump (Output pipe -> Stream) - Rewrite TcpConnection to use ITransportConnection: remove Channel, WriteToStreamAsync, WriteBatchToStreamAsync, ArrayPool batching - Write path is now: actor writes to pipe buffer (memcpy, no syscall), pipe pump flushes to stream asynchronously with implicit batching - Flush batching is implicit: while the pump is blocked on WriteAsync (syscall), actor writes accumulate in the pipe buffer. Next pump cycle sends everything coalesced. - TcpConnection reduced from ~1300 to ~1030 lines Benchmark: 3.60M req/sec peak (56% over SAEA baseline, same ballpark as the Channel+Stream approach with less complexity) All 42 TCP tests pass (23 TcpIntegration + 19 TcpSpec). * Remove dead IStreamProvider/TcpStreamProvider, update API baselines - Delete IStreamProvider.cs and TcpStreamProvider.cs (replaced by ITransportConnection/TcpTransportConnection) - Remove unused streamProvider parameter from TcpOutgoingConnection - Remove null streamProvider arg from TcpManager - Update API approval baselines for the new public surface * Fix slopwatch SW003 violations: narrow empty catch blocks - Narrow bare `catch { }` to specific expected exception types (ObjectDisposedException, SocketException, InvalidOperationException) - Add slopwatch-ignore annotations for legitimate shutdown catches where exceptions are expected (CTS cancellation, socket already closed) - Narrow OperationCanceledException catches with `when` guards * Fix CI test failures: batching ack timing and Windows DNS timeout - TcpConnectionBatchingSpec: acks now fire on pipe acceptance (immediate) instead of stream write completion. Remove ExpectNoMsg assertion, keep stream-level batching verification (32 + 96 byte pattern). - TcpIntegrationSpec DNS test: increase connect timeout to 5s and ExpectMsg timeout to 30s for Windows DNS resolver which can take 10-15+ seconds to fail for unresolvable hosts. * Fix xUnit3 multi-node adapter build errors from ByteString removal - Remove System.Runtime.Loader package (unnecessary on net10.0) - Replace ByteString.FromString with Encoding.UTF8.GetBytes in Executor - Fix TcpLoggingServer: decode Tcp.Received.Data to string via UTF8, use .Length instead of .Count on ReadOnlyMemory<byte> - Fix nullable reference warnings in MultiNodeTestCase * Fix multi-TFM and nuspec references from dev merge - Akka.Cluster.TestKit.Xunit2: change TargetFrameworks (with undefined NetLibVersion) to single TargetFramework using NetStandardLibVersion - Akka.Remote.TestKit.Xunit2.Tests: same fix for NetFrameworkTestVersion - Update nuspec templates: netstandard2.0 -> net10.0 publish paths * Fix nuget pack: remove xunit.runner.utility.netstandard15.dll reference The Xunit2 multi-node test adapter hardcoded inclusion of xunit.runner.utility.netstandard15.dll which doesn't exist on net10.0. This was a netstandard-era shim no longer needed. * Avoid byte copy in ReliableDeliverySerializer chunk deserialization Use ByteString.Memory directly instead of ToByteArray().AsMemory() in the SequencedMessage and DurableQueue.MessageSent chunk paths to skip an unnecessary byte[] allocation and copy. Also modernize the spec data to collection expressions and u8 string literals. * Clean up reviewer-flagged dead code and actor-context async issues - ClusterClientDiscovery: drop NETSTANDARD2_1 conditional, remove ConfigureAwait(false) (inappropriate inside actor), and pass the CancellationToken through the single ReadAsStringAsync call. - AkkaAssertEqualityComparerAdapter: drop NETSTANDARD2_0/2_1 branch; project targets net10.0 only and Nullable is enabled. - Akka.Streams.TestKit/TestUtils: delete the NETSTANDARD2_1 TaskWaitAsyncPolyfill block (no consumers; BCL provides WaitAsync). - TcpIntegrationSpec: TODO note pointing at issue #8178 for the DNS resolution path; full fix is a separate PR per reviewer. * Migrate Akka.IO and Akka.Streams from ReadOnlyMemory<byte> to ReadOnlySequence<byte> Replace ReadOnlyMemory<byte> with ReadOnlySequence<byte> across the public TCP and Streams DSL surface so the read path can flow non-contiguous buffers end-to-end without forcing flatten copies in downstream framing stages. Public API changes: - Tcp.Received.Data is now ReadOnlySequence<byte> (matches Tcp.Write.Data). - Akka.Streams Tcp DSL (IncomingConnection.Flow, BindAndHandle, OutgoingConnection) now emits/consumes ReadOnlySequence<byte>. - Framing.Delimiter, Framing.LengthField, SimpleFramingProtocol*, JsonFraming.ObjectScanner all flow ReadOnlySequence<byte>. - FileIO.FromFile/ToFile, StreamConverters.* now use ReadOnlySequence<byte>. - JsonObjectParser.Offer/Poll signatures updated; the empty-buffer fast path preserves the original zero-copy behaviour for single-segment inputs. Internal: - TcpConnection still copies pipe segments into a byte[] before delivery (Tell is non-blocking; eliminating the copy requires an ack protocol that is out of scope), but the result is wrapped as ReadOnlySequence<byte> so downstream stages can chain segments without further flattening. - IO sinks (FileSubscriber, OutputStreamSubscriber) iterate sequence segments instead of using a single .Span path. - Framing and JsonObjectParser keep their existing per-frame allocation footprint via bridge code; the SequenceReader-based rewrite that eliminates Concat / ToArray() compactions ships in the next commit. Tests, benchmarks, docs, and examples updated to construct ReadOnlySequence<byte> instances and consume via ToArray()/FirstSpan/CopyTo extension methods. Build: 0 errors, 0 warnings across the full solution. * Eliminate per-frame copies in Framing and JsonObjectParser Replace the byte[]-allocating concat helpers and defensive ToArray() compactions with zero-copy ReadOnlySequence<byte> slicing and segment chains. The framing/parsing hot path no longer allocates merge buffers or compact copies on every frame; only the segment node objects when multiple inputs need to be chained. - Add Implementation/BufferSegment.cs: small ReadOnlySequenceSegment<byte> helper with a Concat method that links existing segments without copying data. - Framing.cs: - Concat now delegates to BufferSegment.Concat (zero data copy). - SimpleFramingProtocolEncoder pushes header+payload as a chained sequence instead of allocating a combined byte[]. - DelimiterFraming.IndexOf / HasSubstring use SequenceReader<byte> so multi-segment inputs scan without materialization. - DelimiterFramingStage and LengthFieldFramingStage drop the parsedFrame.ToArray() and _buffer.ToArray() defensive copies; slicing a ReadOnlySequence is a struct view, consumed segments fall out of scope when _buffer is reassigned. - JsonObjectParser.cs: - Internal storage is now ReadOnlySequence<byte>; multi-Offer scenarios chain segments via BufferSegment.Concat (no merge byte[]). - SeekObject scans via SequenceReader<byte> (no per-byte materialization). - Poll slices the buffer without compacting the remainder. Tests: Framing/JsonFraming spec suite passes (44/44); Akka.Tests IO suite passes (51/51). Full solution still builds clean. * Polish TcpConnection: phase doc, region-grouped state flags, drop log prefixes; refresh API baselines - Restore the ASCII phase diagram at the top of TcpConnection.cs reflecting the post-rewrite states: Connecting → AwaitReg → Open → (PeerSentEof | Closing) → Closed. Map each state to its Become(...) handler. - Group transient connection-state flags (_peerClosed, _outputShutdown, _keepOpenOnPeerClosed, _closingGracefully, _readPumpCompleted, _readPumpHasError, _readPumpError) inside a #region with a paragraph documenting the close-handshake invariants — what each flag means, when it is set, and how the read/write paths gate on combinations. - Strip the redundant "[TcpConnection] " prefix from all Log.Debug calls. The log source already carries the actor path, which identifies the connection unambiguously. - Refresh the Akka.Streams and Akka core API approval baselines for the ReadOnlyMemory<byte> → ReadOnlySequence<byte> public surface changes. Tests: TcpIntegrationSpec + TcpConnectionBatchingSpec all green (24/24). API approval tests pass. * Adapt FramingBenchmarks to ReadOnlySequence<byte> The ByteString-flavored FramingBenchmarks.cs came in via the dev → PR merge (#8202 landed first on dev). That branch retired ByteString as part of this PR, so the benchmark fails to compile here. Swap to the ReadOnlySequence<byte> variant that matches the post-migration types on this branch. Same benchmark structure and parameters as the dev version (deferred- source pattern, MessageCount=100K, MessageSize ∈ {64, 1024}); only the buffer-type construction and consumption sites differ.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a new
FramingBenchmarksclass toAkka.Benchmarksthat measures throughput and per-frame allocation overhead for the four hot paths in the framing system:LengthField_Encode—Framing.SimpleFramingProtocolEncoder(4-byte big-endian length prefix + payload)LengthField_Decode— encode→decode round-trip throughSimpleFramingProtocolDelimiter_Decode—Framing.Delimiterover a stream of single-message-per-chunk inputsJsonFraming_MultiChunk—JsonFraming.ObjectScannerwith chunks deliberately sized to straddle object boundaries (so mostOffercalls intoJsonObjectParserconcatenate into a non-empty buffer)Why this PR
The framing stages have always been suspected of being allocation-heavy on the per-message path (the
parsedFrame.ToArray()/_buffer.ToArray()defensive compactions, the encoder'sConcatbyte[] allocation, and the JSON parser's merge byte[] in non-emptyOffer). There is no BenchmarkDotNet coverage of the framing paths today, so any change in this area has to be evaluated against indirect signal (the TCP echo benchmark, the NBenchJsonFramingBenchmark).This adds direct, comparable BDN numbers — the same harness shape used by
TcpOperationsBenchmarks/StreamThroughputBenchmarks— so future framing changes can be measured cleanly.Benchmark design
The benchmark uses a deferred-source pattern so all materializer overhead sits outside the measurement window:
The graph materializes during
IterationSetup— actors spawn, stages fuse, the dispatcher attaches, the sink subscribes — and parks waiting on the TCS. The benchmark method only trips the gate and awaits drain. WithOperationsPerInvoke = 10000, the two fixed hops (gate-signal + sink-complete) amortize to noise and the reportedAllocatedcolumn reads as bytes-per-framed-message.For
JsonFraming_MultiChunk, input chunks are sized atMessageSize / 3so every chunk crosses at least one object boundary. This exercises the multi-Offer concatenation path insideJsonObjectParserrather than the empty-buffer fast path.Parameters
MessageSize∈{64, 1024}bytesMessageCountfixed at 10,0008 cells total. Wall-clock under
Job.Default(BDN's chosen warmup/iterations) is well under 10 minutes on a modern machine.Test plan
--job dryconfirms the benchmark executes end-to-end ondev(already verified locally)Notes
Ports the same
FramingBenchmarks.cswill need to be backported (withByteString→ReadOnlyMemory<byte>/ReadOnlySequence<byte>adaptations) to feature branches that change the underlying byte-buffer type. The benchmark structure stays identical; only the type name changes.