googleapis / googleapis/google-cloud-swift
storage: zero-copy Foundation.Data forwarding in ByteBuffer and _HTTPClientRequest
- Dominant language
- Swift
- Stars
- 26
- Forks
- 10
- Avg merge
- 12h 57m
- Merged PRs (30d)
- 213
Description
# storage: zero-copy `Foundation.Data` forwarding in `ByteBuffer` and `_HTTPClientRequest`
## Problem Description
When applications upload in-memory data using `Foundation.Data` (such as via `client.upload(data, ...)` or `BytesSource(data: data)`), the client library unconditionally copies the entire payload in memory before forwarding it to the HTTP client.
### The Mechanism
1. In `pkgs/swift-google-cloud-storage/Sources/GoogleCloudStorage/ByteBuffer.swift:114-125`:
```swift
public var byteBuffer: NIOCore.ByteBuffer {
switch storage {
case .byteBuffer(let buffer):
return buffer
case .data(let data):
return data.withUnsafeBytes { rawBuffer in
var buf = ByteBufferAllocator().buffer(capacity: rawBuffer.count)
buf.writeBytes(rawBuffer)
return buf
}
}
}
```
When `ByteBuffer` wraps a `Foundation.Data` instance, accessing the `.byteBuffer` property allocates a new `NIOCore.ByteBuffer` and copies the raw bytes byte-by-byte into the new buffer using `writeBytes`.
2. In `StorageClient+Upload.swift:957` (`buildUploadChunkRequest`):
```swift
fileprivate static func buildUploadChunkRequest(
...
data: ByteBuffer,
...
) async throws -> GoogleCloudGax._HTTPClientRequest {
...
request.setBody(buffer: data.byteBuffer)
return request
}
```
`buildUploadChunkRequest` always invokes `data.byteBuffer`. This forces an eager memory copy of every chunk when the caller provided `Data`.
3. Meanwhile, `GoogleCloudGax._HTTPClientRequest` already natively supports setting `Foundation.Data` directly without an explicit manual copy:
```swift
public mutating func setBody(data: Data) {
self.body = .data(data)
}
```
And in `_HTTPClientRequest.execute`:
```swift
switch self.body {
case .byteBuffer(let b):
request.body = .bytes(b)
case .data(let d):
request.body = .bytes(.init(data: d))
...
```
`NIOCore.ByteBuffer(data: d)` from `NIOFoundationCompat` wraps `Data` using Copy-On-Write / slice sharing where possible, avoiding manual allocation and explicit buffer copying.
---
## Quantitative Impact
- For a **100 MiB** or **500 MiB** in-memory `Data` upload, **100% of the payload is copied** into temporary buffers before transmission.
- Copying 32 MiB in memory takes ~10–20 ms on modern server CPUs, consuming memory bus bandwidth and polluting the CPU L2/L3 cache right before the network stack transmits the data.
- It also doubles the transient heap memory footprint during uploads of large in-memory objects.
---
## Proposed Solution
1. **Forward Storage Directly to `_HTTPClientRequest`**:
Expose a helper or method on `_HTTPClientRequest` (or within `StorageClient+Upload.swift`) that accepts `GoogleCloudStorage.ByteBuffer` without converting to `NIOCore.ByteBuffer`:
```swift
extension GoogleCloudGax._HTTPClientRequest {
mutating func setBody(storageBuffer: GoogleCloudStorage.ByteBuffer) {
switch storageBuffer.storage {
case .data(let data):
self.setBody(data: data)
case .byteBuffer(let buffer):
self.setBody(buffer: buffer)
}
}
}
```
In `buildUploadChunkRequest`:
```swift
request.setBody(storageBuffer: data)
```
2. **Optimize `ByteBuffer.byteBuffer` Fallback**:
In `ByteBuffer.swift`, update `.byteBuffer` to use `NIOCore.ByteBuffer(data: data)` from `NIOFoundationCompat` rather than manual `ByteBufferAllocator().buffer(capacity:)` and `buf.writeBytes(rawBuffer)`.
---
## Acceptance Criteria
- Uploading large `Foundation.Data` instances does not perform manual `writeBytes` copies into intermediate buffers.
- Unit tests verifying `ByteBuffer` conversions and uploads of `Foundation.Data` pass with zero regressions.
- All storage tests pass with `-warnings-as-errors`.
Contributor guide
Research direction
Start with pkgs/swift-google-cloud-storage/Sources/GoogleCloudStorage/ByteBuffer.swift:114-125 and StorageClient+Upload.swift:957, then inspect GoogleCloudGax._HTTPClientRequest.setBody and execute. Update the upload path so Foundation.Data is forwarded without manual writeBytes copying, and use the NIOFoundationCompat conversion for the fallback. Run the ByteBuffer, upload, and storage tests with warnings-as-errors; done means the acceptance criteria pass without regressions.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- swift
- Domain
- cloud
- Issue type
- Refactor
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 76/100