Modernize Akka.IO TCP: ByteString removal, Stream+Pipe transport, ReadOnlySequence<byte> migration, .NET 10 TFM#8132
Conversation
|
Update after cleanup:
|
|
Follow-up cleanup note: There are still some legacy Recommendation for that later cleanup:
I intentionally did not fold that repo-wide cleanup into this PR. |
Akka.IO TCP Benchmark: SAEA (dev) vs Stream+Pipe (this PR)Environment: Linux Ubuntu 24.04.4, Intel Core i9-9900K 3.60GHz, 8 cores, .NET 10.0.5, ServerGC Summary (10-byte messages, LongRun)
Peak throughput: 2.3M → 4.25M req/sec (+84%). Allocations cut ~50%. Full BenchmarkDotNet output — dev baseline (SAEA)
Full BenchmarkDotNet output — Stream+Pipe (this PR)
|
Spike: ITransportConnection with duplex pipe write pathBranch: Explored replacing the What changed
Why
Benchmark results (same machine, same config as PR numbers)
Peak: 3.60M req/sec (10B messages, 40 clients). Allocations: 222-249B/op (down from 441-473B SAEA). Note: significant run-to-run variance on this VM (~30%). The Channel+Stream approach benchmarked at both 4.25M and 2.87M on different runs. Dedicated hardware needed for clearer signal. Test status
|
Dedicated Hardware Benchmark: Three-Way ComparisonHardware: Intel Core i7-6700K 4.00GHz (Skylake), 4 physical / 8 logical cores, Linux Ubuntu 24.04.2 All three branches benchmarked on the same dedicated machine with no other workloads — much cleaner signal than the VM runs posted earlier. 10-byte messages
100-byte messages
Allocations per operation (10B, 40 clients)
Key TakeawayOn dedicated hardware, Channel+Stream and ITransportConnection (pipe) are within 5% of each other — statistically identical. Both deliver +63-74% over SAEA at scale with ~50% allocation reduction. The pipe design is now merged into this branch. It replaces the Channel + direct |
Aaronontheweb
left a comment
There was a problem hiding this comment.
Not done with review yet, but need to flush these pending
| <PropertyGroup> | ||
| <OutputType>Exe</OutputType> | ||
| <TargetFrameworks>$(NetFrameworkTestVersion);$(NetTestVersion);net6.0</TargetFrameworks> | ||
| <TargetFramework>$(NetTestVersion)</TargetFramework> |
There was a problem hiding this comment.
The vast majority of "API changes" are from migrating .NET 6 -> .NET 10, but there are legitimate API changes here
| { | ||
| [Akka.Annotations.InternalApi] | ||
| public readonly struct ChunkedMessage | ||
| public readonly struct ChunkedMessage : System.IEquatable<Akka.Delivery.Internal.ChunkedMessage> |
There was a problem hiding this comment.
Needed to restructure this since we can't depend on ByteString's equality members to do the heavy lifting for us on equality by value any more.
| LittleEndian = 1, | ||
| } | ||
| [System.Diagnostics.DebuggerDisplay("(Count = {_count}, Buffers = {_buffers})")] | ||
| public sealed class ByteString : System.Collections.Generic.IEnumerable<byte>, System.Collections.IEnumerable, System.IEquatable<Akka.IO.ByteString> |
There was a problem hiding this comment.
Deleted by design - used the compilation errors as a "to do" list for Claude, but this class has been an absolute performance pig for years and I'm not sad it's gone.
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.
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.
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.
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.
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.
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.
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.
Branch should be created manually before invoking the program, not by the orchestrator itself.
- 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
- 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
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.
- 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
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.
- 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
…ckets 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)
- 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)
- 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
- 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.)
- 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
- 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
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.
d4b00f6 to
62bae43
Compare
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.
Aaronontheweb
left a comment
There was a problem hiding this comment.
Reviewed all of it and overall it looks very good, but:
- Why not
ReadOnlySequence<T>instead ofReadOnlyMemory<T>- this would make it possible to do things like frame-length encoding without additionalbyteallocations, which is going to be a hotpath scenario for writes. - There seems to be a bit of buffer-copying going on in a few places where it shouldn't be necessary any longer. All of those call sites are going to need to be revisited individually.
| - script: dotnet slopwatch analyze -d . --fail-on error --stats | ||
| displayName: 'Run Slopwatch Analysis' | ||
|
|
||
| - template: azure-pipeline.template.yaml |
There was a problem hiding this comment.
Not doing .NET Framework CI/CD any longer
| return null; | ||
| } | ||
|
|
||
| json = await response.Content.ReadAsStringAsync(ct); |
There was a problem hiding this comment.
This change shouldn't have been made - for starters, we should never really call ConfigureAwait(false) on tasks executing inside an actor. Second, we're not adding .NET Standard 2.1 support so this don't make any sense - also, why would we discard the CancellationToken usage there?
| var i = (int)(_deltaNodeRoundRobinCounter % all.Length); | ||
| slice = all.Slice(i, sliceSize).ToImmutableArray(); | ||
|
|
||
| var remainingFromI = all.Length - i; |
There was a problem hiding this comment.
looks like a bounds checking safety issue here but I need to double check if this impacts the math around this adversely - my understanding was that if sliceSize is greater than the length of the array it safely truncates, but I am probably mistaken.
| new IPEndPoint(resolved.Ipv6.First(), remoteAddress.Port)); | ||
| else // one or the other | ||
| Register(new IPEndPoint(resolved.Addr, remoteAddress.Port), null); | ||
| // Pass DnsEndPoint directly to Socket.ConnectAsync — the runtime |
There was a problem hiding this comment.
welp, that certainly eliminates a lot of code....
| protected SocketCompleted(SocketAsyncEventArgs eventArgs) | ||
| { | ||
| Data = ByteString.CopyFrom(eventArgs.Buffer, eventArgs.Offset, eventArgs.BytesTransferred); | ||
| var copy = new byte[eventArgs.BytesTransferred]; |
There was a problem hiding this comment.
this is ugly and I'm not sure how necessary it is
| protected SocketCompleted(SocketAsyncEventArgs eventArgs) | ||
| { | ||
| Data = ByteString.CopyFrom(eventArgs.Buffer, eventArgs.Offset, eventArgs.BytesTransferred); | ||
| var copy = new byte[eventArgs.BytesTransferred]; |
There was a problem hiding this comment.
Again, kind of ugly and I'm not sure how necessary the copy is here. Probably is.
| .ContinueWith(_ => { }, cancellationToken, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); | ||
| } | ||
|
|
||
| #if NETSTANDARD2_1 |
There was a problem hiding this comment.
More possibly dead code for backporting - we'll evaluate it later.
- 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 akkadotnet#8178 for the DNS resolution path; full fix is a separate PR per reviewer.
…ySequence<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.
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.
… 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.
Benchmark update — three-way comparison after folding in the ROS migration workFollowing review feedback, the ByteString → ReadOnlyMemory migration originally in this PR has been extended to ReadOnlySequence with an accompanying perf rewrite of Framing + JsonObjectParser. The new commits on this branch (
Backup of the pre-merge branch state preserved at https://github.com/Aaronontheweb/akka.net/tree/backup/spec1-modernize-akka-io-tcp-pre-ros (commit BenchmarksA new BenchmarkDotNet Hardware: AMD Ryzen 9 9900X (Zen 5, 12C/24T), 64 GB RAM, Linux Ubuntu, .NET 10.0.7, ServerGC, BDN default Job. Three branches benchmarked:
Suite A — Framing allocations (bytes per framed message)
Key observations:
Suite A — Framing throughput (req/sec)
At 1 KB the ROS rewrite delivers +122% delimiter throughput and +52% length-field decode throughput vs dev. At 64 B the picture is mixed — Suite B — TCP (TcpOperationsBenchmarks)Spot checks at representative cells: Allocations per echo round-trip (10 B, 40 clients):
Throughput (10 B, 30 clients, peak cell):
Throughput (10 B, 1 client, latency-bound):
The TCP path is essentially unchanged between rom and ros (expected — the SAEA → Stream+Pipe rewrite was already in rom, and ROS only changes the buffer type). High-concurrency cells (20–40 clients) show large StdDev / median–mean divergence on all three branches — likely loopback TCP buffer / kernel scheduler saturation rather than CPU. Bottom line
Files / artifacts
Caveats
|
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
left a comment
There was a problem hiding this comment.
LGTM - numbers and design look good.
|
My biggest worry in this, (Outside of, we handle Suggested mitigations: Option 1: Keep Bytestring, adapt #7491 as a bridge Option 2: Provide a version of Option 3: ??? (happy for ideas...) Main concern is making sure devs are able to more easily migrate to 1.6 without having to fight tech debt cost factor in planning. |
Summary
Milestone 1 of the Akka.NET 1.6 transport/serialization epic. Modernizes the Akka.IO TCP layer:
Tcp.Received.DataandTcp.Write.Dataare nowReadOnlySequence<byte>(wasByteString). All Akka.Streams DSL stages that flowedByteStringnow flowReadOnlySequence<byte>— Framing, JsonFraming, FileIO, StreamConverters, and the TCP source/sink.netstandard2.0+net6.0— all projects targetnet10.0only.SocketAsyncEventArgsinternals inTcpConnectionwithStream+System.IO.Pipelines, fronted by a newITransportConnectionabstraction (enables a future TLS provider).FramingandJsonObjectParserviaBufferSegment-chainedReadOnlySequence<byte>(no mergebyte[]allocations, defensiveToArray()compactions removed).What changed
netstandard2.0+net6.0→net10.0onlyTcp.Received.DataByteString→ReadOnlySequence<byte>Tcp.Write.DataByteString→ReadOnlySequence<byte>Stream+Pipewith actor-driven reads, fronted byITransportConnectionITransportConnectionTcpTransportConnection(plaintext), shape ready for a future TLS implementationFraming,JsonFraming,FileIO,StreamConverters,Tcpstream now flowReadOnlySequence<byte>Framing/JsonObjectParserConcatbyte[] allocation replaced byBufferSegmentchain. DefensiveparsedFrame.ToArray()/_buffer.ToArray()/emit.ToArray()compactions removed. JsonObjectParser usesSequenceReader<byte>for byte-by-byte scan.NETSTANDARD2_1blocks removed (ClusterClientDiscovery,AkkaAssertEqualityComparerAdapter,Akka.Streams.TestKit/TestUtils);ConfigureAwait(false)regression inClusterClientDiscoveryreverted withCancellationTokenrestored; TcpConnection state-machine doc restored; redundant[TcpConnection]log prefixes stripped.ByteStringrefs wereGoogle.Protobuf.ByteString)Akka.API.Testsbaselines refreshed for theReadOnlySequence<byte>public surfaceByteStringBenchmarksdeletedTest status
dotnet build -c Release— 0 errors, 0 warningsKnown issues
Echo_should_work_even_if_server_is_in_full_close_modeloses 1 byte (999 vs 1000) when run in batch, passes individually. Root cause under investigation.Performance
A new
FramingBenchmarks(#8202 againstdev) was used to measure the framing system end-to-end across three states:dev(ByteString) → mid-PR (ReadOnlyMemory<byte>) → final (ReadOnlySequence<byte>). Full data tables in this comment. Headlines:LengthField_Decode1752 B → 632 B (−64%),Delimiter_Decodestays at 425 B regardless of payload size,LengthField_Encode489 B → 553 B.Delimiter_Decode1.88M → 4.17M req/s (+122%),LengthField_Decode2.29M → 3.47M (+52%).−38%allocations vs dev at 40 clients,+21%throughput vs dev at 1 client (latency-bound).ReadOnlyMemory<byte>state of this PR was an allocation regression vsdevfor any non-trivial payload (e.g. 1 KB delimiter went 449 B → 1441 B). The follow-onReadOnlySequence<byte>work was required to recover and improve ondev.Commits
dd14510d)ReadOnlyMemory<byte>toReadOnlySequence<byte>(d49c02c1)d52cf44e)baeb1364)A pre-merge backup of the branch state at commit 8 (
f2e80b3d8) is preserved atbackup/spec1-modernize-akka-io-tcp-pre-roson the fork.Design decisions
See
openspec/changes/modernize-akka-io-tcp/design.mdfor full rationale on the SAEA → Pipe rewrite and the buffer-type evolution.