Skip to content

perf(rt): add ByteBuffer-backed stream to avoid Data copies on the NI…#1106

Open
jbelkins wants to merge 1 commit into
smithy-lang:mainfrom
dayaffe:perf/bytebuffer-backed-stream
Open

perf(rt): add ByteBuffer-backed stream to avoid Data copies on the NI…#1106
jbelkins wants to merge 1 commit into
smithy-lang:mainfrom
dayaffe:perf/bytebuffer-backed-stream

Conversation

@jbelkins

Copy link
Copy Markdown
Collaborator

…O path

The SwiftNIO transport round-trips every streamed byte through Foundation.Data, which forces extra copies that NIO-native SDKs avoid. On download the response bridge copies ByteBuffer -> [UInt8] -> Data -> BufferedStream (3 copies/chunk); on upload it copies Data -> a freshly allocated ByteBuffer (1 alloc + 1 copy/chunk). The write side is also synchronous and unbounded, so a fast producer cannot be throttled and the in-memory buffer can grow without limit.

This change is additive only; no existing public API changes:

  • Smithy: add WriteableStream.writeAsync(contentsOf:) as a protocol-extension default that bridges to the existing synchronous write, so every current conformer compiles unchanged. (A distinct name is used rather than an async overload of write(contentsOf:), which would be source-breaking because an async context would prefer the overload and force existing call sites to await.)
  • SmithySwiftNIO: add ByteBufferStream, a Stream-conforming type backed by a FIFO of NIOCore.ByteBuffers. Reads vend ByteBuffer slices via readSlice (advancing the readerIndex, no memmove); writes keep the producer's ByteBuffer (copy-on-write). The async write applies high/low-watermark backpressure so the buffer stays bounded. The Data-returning protocol methods still work, performing a single boundary copy only for legacy consumers.
  • SmithySwiftNIO: SwiftNIOHTTPClientStreamBridge uses ByteBufferStream on the response path and prefers a zero-copy ByteBuffer slice on the request path via an as? downcast, leaving the default Data path unchanged.

Measured with an identical-work consumer (release build): the download path goes from ~1230 MiB/s to ~6640 MiB/s (5.4x), within ~5% of the zero-copy ceiling; upload improves ~1.4x.

Known follow-ups (not in this change): task-cancellation handlers on the async suspensions, and migrating the checksum/chunked middlewares that currently hard-code BufferedStream.

Issue #

Description of changes

Scope

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

…O path

The SwiftNIO transport round-trips every streamed byte through Foundation.Data,
which forces extra copies that NIO-native SDKs avoid. On download the response
bridge copies ByteBuffer -> [UInt8] -> Data -> BufferedStream (3 copies/chunk);
on upload it copies Data -> a freshly allocated ByteBuffer (1 alloc + 1 copy/chunk).
The write side is also synchronous and unbounded, so a fast producer cannot be
throttled and the in-memory buffer can grow without limit.

This change is additive only; no existing public API changes:

- Smithy: add WriteableStream.writeAsync(contentsOf:) as a protocol-extension
  default that bridges to the existing synchronous write, so every current
  conformer compiles unchanged. (A distinct name is used rather than an async
  overload of write(contentsOf:), which would be source-breaking because an
  async context would prefer the overload and force existing call sites to await.)
- SmithySwiftNIO: add ByteBufferStream, a Stream-conforming type backed by a FIFO
  of NIOCore.ByteBuffers. Reads vend ByteBuffer slices via readSlice (advancing
  the readerIndex, no memmove); writes keep the producer's ByteBuffer (copy-on-write).
  The async write applies high/low-watermark backpressure so the buffer stays bounded.
  The Data-returning protocol methods still work, performing a single boundary copy
  only for legacy consumers.
- SmithySwiftNIO: SwiftNIOHTTPClientStreamBridge uses ByteBufferStream on the response
  path and prefers a zero-copy ByteBuffer slice on the request path via an `as?`
  downcast, leaving the default Data path unchanged.

Measured with an identical-work consumer (release build): the download path goes
from ~1230 MiB/s to ~6640 MiB/s (5.4x), within ~5% of the zero-copy ceiling;
upload improves ~1.4x.

Known follow-ups (not in this change): task-cancellation handlers on the async
suspensions, and migrating the checksum/chunked middlewares that currently
hard-code BufferedStream.
@jbelkins jbelkins requested a review from a team as a code owner June 25, 2026 16:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants