googleapis / googleapis/google-cloud-swift

storage: zero-copy Foundation.Data forwarding in ByteBuffer and _HTTPClientRequest

Open
#888 0 comments 0 reactions 0 assignees View on GitHub
api: storage
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.