Skip to content

Commit 5498fa4

Browse files
committed
perf(rt): add ByteBuffer-backed stream to avoid Data copies on the NIO 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.
1 parent 78da1cd commit 5498fa4

4 files changed

Lines changed: 669 additions & 18 deletions

File tree

Sources/Smithy/Stream.swift

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,42 @@ public protocol WriteableStream: AnyObject, Sendable {
5050
/// - Parameter data: data to write
5151
func write(contentsOf data: Data) throws
5252

53+
/// Writes the contents of `data` to the stream asynchronously.
54+
///
55+
/// Unlike the synchronous `write(contentsOf:)`, an async write lets a stream apply
56+
/// backpressure: a conforming stream may suspend the caller until the data has been
57+
/// consumed (or until buffered data drops below a high-water mark), so a fast producer
58+
/// cannot grow the stream's buffer without bound.
59+
///
60+
/// A default implementation is provided that simply calls the synchronous
61+
/// `write(contentsOf:)`, so existing conformers compile unchanged and gain a (non-suspending)
62+
/// async entry point for free. Streams that support backpressure should override this.
63+
///
64+
/// - Note: This is intentionally a distinct method name (not an `async` overload of
65+
/// `write(contentsOf:)`). Overloading the existing synchronous method with an `async`
66+
/// variant of the same name would be source-breaking: in an `async` context Swift prefers
67+
/// the async overload, so existing call sites that write `try stream.write(contentsOf:)`
68+
/// would suddenly require `try await`. The `writeAsync` name mirrors the existing
69+
/// `read` / `readAsync` and `readToEnd` / `readToEndAsync` convention and avoids that hazard.
70+
/// - Parameter data: data to write
71+
func writeAsync(contentsOf data: Data) async throws
72+
5373
/// Closes the stream
5474
func close()
5575

5676
func closeWithError(_ error: Error)
5777
}
5878

79+
public extension WriteableStream {
80+
/// Default async write: bridges to the synchronous `write(contentsOf:)`.
81+
///
82+
/// This default keeps the new `writeAsync(contentsOf:)` requirement source-compatible for all
83+
/// existing conformers — they need no code change and inherit this implementation.
84+
func writeAsync(contentsOf data: Data) async throws {
85+
try self.write(contentsOf: data)
86+
}
87+
}
88+
5989
/// Protocol that provides reading and writing data to a stream
6090
public protocol Stream: ReadableStream, WriteableStream {
6191
}

0 commit comments

Comments
 (0)