Skip to content

Optimize HTTP2ToHTTP1 client codec to reduce empty data frames#535

Merged
fabianfett merged 7 commits intomainfrom
ff-reduce-frames
Feb 10, 2026
Merged

Optimize HTTP2ToHTTP1 client codec to reduce empty data frames#535
fabianfett merged 7 commits intomainfrom
ff-reduce-frames

Conversation

@fabianfett
Copy link
Member

The codec now caches the most recent outbound frame and only writes it on flush or when receiving .end. This lets us set endStream=true on the final data or headers frame instead of sending a separate empty data frame.

Changes:

  • Introduced PendingFrameWrite to cache frame payload and promise
  • Modified processOutboundData to return optional frames instead of writing immediately
  • Added flush() method to both client codecs to handle cached writes
  • Promise merging ensures correct completion notification when combining frames

The codec now caches the most recent outbound frame and only writes it on flush or when receiving `.end`. This lets us set `endStream=true` on the final data or headers frame instead of sending a separate empty data frame.

Changes:
- Introduced `PendingFrameWrite` to cache frame payload and promise
- Modified `processOutboundData` to return optional frames instead of writing immediately
- Added `flush()` method to both client codecs to handle cached writes
- Promise merging ensures correct completion notification when combining frames
@fabianfett fabianfett added the 🔨 semver/patch No public API change. label Feb 9, 2026
@@ -0,0 +1,110 @@
#!/bin/bash
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to duplicate these two scripts or can they be run from the NIO repo? These scripts do get updated from time to time so not having them spread out would be great.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed them

return (.init(.data(data), flushPromise), nil)

case (.data(let data), .some(let trailers)):
let trailers = self.makeH2TrailerFramePayload(trailers)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit, can you avoid shadowing trailers here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed.

Comment on lines +199 to +201
// We only need to merge, if there is an outstanding frame write. Therefore we can bang
// the frame write here.
switch (self.pendingFrameWrite!.promise, endPromise) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like that the ! is hidden away in this function, it puts the onus on the caller to know that self.pendingFrameWrite isn't nil.

FWIW setOrCascade might be helpful here https://github.com/apple/swift-nio/blob/db01d879426d6d99b2c2d4a6e802a4a0c6e8de2a/Sources/NIOCore/EventLoopFuture.swift#L2133-L2153

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

uses setOrCascade now.

@fabianfett fabianfett enabled auto-merge (squash) February 10, 2026 10:13
@fabianfett fabianfett merged commit 979f431 into main Feb 10, 2026
49 of 50 checks passed
@fabianfett fabianfett deleted the ff-reduce-frames branch February 10, 2026 10:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🔨 semver/patch No public API change.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants