Skip to content

Add Framing/JsonFraming throughput and allocation benchmarks#8202

Merged
Aaronontheweb merged 2 commits into
akkadotnet:devfrom
Aaronontheweb:feature/framing-benchmarks
May 10, 2026
Merged

Add Framing/JsonFraming throughput and allocation benchmarks#8202
Aaronontheweb merged 2 commits into
akkadotnet:devfrom
Aaronontheweb:feature/framing-benchmarks

Conversation

@Aaronontheweb
Copy link
Copy Markdown
Member

Summary

Adds a new FramingBenchmarks class to Akka.Benchmarks that measures throughput and per-frame allocation overhead for the four hot paths in the framing system:

  • LengthField_EncodeFraming.SimpleFramingProtocolEncoder (4-byte big-endian length prefix + payload)
  • LengthField_Decode — encode→decode round-trip through SimpleFramingProtocol
  • Delimiter_DecodeFraming.Delimiter over a stream of single-message-per-chunk inputs
  • JsonFraming_MultiChunkJsonFraming.ObjectScanner with chunks deliberately sized to straddle object boundaries (so most Offer calls into JsonObjectParser concatenate 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's Concat byte[] allocation, and the JSON parser's merge byte[] in non-empty Offer). 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 NBench JsonFramingBenchmark).

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:

[IterationSetup(Target = nameof(LengthField_Encode))]
public void Setup() {
    _gate = new TaskCompletionSource<NotUsed>(TaskCreationOptions.RunContinuationsAsynchronously);
    _completion = Source.FromTask(_gate.Task)
        .ConcatMany(_ => Source.From(_rawMessages))
        .Via(Framing.SimpleFramingProtocolEncoder(int.MaxValue))
        .RunWith(Sink.Ignore<ByteString>(), _materializer);
}

[Benchmark(OperationsPerInvoke = MessageCount)]
public Task LengthField_Encode() {
    _gate.SetResult(NotUsed.Instance);
    return _completion;
}

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. With OperationsPerInvoke = 10000, the two fixed hops (gate-signal + sink-complete) amortize to noise and the reported Allocated column reads as bytes-per-framed-message.

For JsonFraming_MultiChunk, input chunks are sized at MessageSize / 3 so every chunk crosses at least one object boundary. This exercises the multi-Offer concatenation path inside JsonObjectParser rather than the empty-buffer fast path.

Parameters

  • MessageSize{64, 1024} bytes
  • MessageCount fixed at 10,000

8 cells total. Wall-clock under Job.Default (BDN's chosen warmup/iterations) is well under 10 minutes on a modern machine.

Test plan

  • Smoke run with --job dry confirms the benchmark executes end-to-end on dev (already verified locally)
  • Full run with default job to capture baseline numbers
  • Numbers can then be compared against framing-related changes on feature branches

Notes

Ports the same FramingBenchmarks.cs will need to be backported (with ByteStringReadOnlyMemory<byte> / ReadOnlySequence<byte> adaptations) to feature branches that change the underlying byte-buffer type. The benchmark structure stays identical; only the type name changes.

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 Aaronontheweb merged commit 121707b into akkadotnet:dev May 10, 2026
12 checks passed
@Aaronontheweb Aaronontheweb deleted the feature/framing-benchmarks branch May 10, 2026 14:48
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant