Skip to content

Commit

Permalink
Make HEADERS frame payload non-indirect
Browse files Browse the repository at this point in the history
Motivation:

In previous patches we shrank the size of HTTP2Frame by
making various data types indirect in the frame payload.
This included HEADERS, which is unfortunte as HEADERS frames
are quite common.

This patch changes the layout of the HEADERS frame to remove the
indirect case.

Modifications:

- Move END_STREAM into an OptionSet.
- Turn the two optional bits into flags in the aforementioned
    OptionSet
- Replace the properties with computed properties.
- Remove the indirect case

Result:

HEADERS frames are cheaper.
  • Loading branch information
Lukasa committed Nov 16, 2023
1 parent 285e444 commit 002ec5e
Show file tree
Hide file tree
Showing 2 changed files with 90 additions and 9 deletions.
95 changes: 86 additions & 9 deletions Sources/NIOHTTP2/HTTP2Frame.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,15 @@ public struct HTTP2Frame: Sendable {

/// Stream priority data, used in PRIORITY frames and optionally in HEADERS frames.
public struct StreamPriorityData: Equatable, Hashable, Sendable {
public var exclusive: Bool
public var dependency: HTTP2StreamID
public var exclusive: Bool
public var weight: UInt8

internal init(exclusive: Bool, dependency: HTTP2StreamID, weight: UInt8) {
self.exclusive = exclusive
self.dependency = dependency
self.weight = weight
}
}

/// Frame-type-specific payload data.
Expand All @@ -46,7 +52,7 @@ public struct HTTP2Frame: Sendable {
/// frames into a single ``HTTP2Frame/FramePayload/headers(_:)`` instance.
///
/// See [RFC 7540 § 6.2](https://httpwg.org/specs/rfc7540.html#rfc.section.6.2).
indirect case headers(Headers)
case headers(Headers)

/// A `PRIORITY` frame, used to change priority and dependency ordering among
/// streams.
Expand Down Expand Up @@ -212,35 +218,106 @@ public struct HTTP2Frame: Sendable {

/// The payload of a `HEADERS` frame.
public struct Headers: Sendable {
/// An OptionSet that keeps track of the various boolean flags in HEADERS.
/// It allows us to elide having our two optionals by keeping track of their
/// optionality here, which frees up a byte and keeps the total size of
/// HTTP2Frame at 24 bytes.
@usableFromInline
struct Booleans: OptionSet {
@usableFromInline
var rawValue: UInt8

@inlinable
init(rawValue: UInt8) {
self.rawValue = rawValue
}

@usableFromInline static let endStream = Booleans(rawValue: 1 << 0)
@usableFromInline static let priorityPresent = Booleans(rawValue: 1 << 1)
@usableFromInline static let paddingPresent = Booleans(rawValue: 1 << 2)
}

/// The decoded header block belonging to this `HEADERS` frame.
public var headers: HPACKHeaders

/// Stream priority data.
///
/// If `.priorityPresent` is not set in our boolean flags, this value is ignored.
@usableFromInline
var _priorityData: StreamPriorityData

/// The number of padding bytes in this frame.
///
/// If `.paddingPresent` is not set in our boolean flags, this value is ignored.
@usableFromInline
var _paddingBytes: UInt8

/// Boolean flags that control the presence of other values in this frame.
@usableFromInline
var booleans: Booleans

/// The stream priority data transmitted on this frame, if any.
public var priorityData: StreamPriorityData?
@inlinable
public var priorityData: StreamPriorityData? {
get {
if self.booleans.contains(.priorityPresent) {
return self._priorityData
} else {
return nil
}
}
set {
if let newValue = newValue {
self._priorityData = newValue
self.booleans.insert(.priorityPresent)
} else {
self.booleans.remove(.priorityPresent)
}
}
}

/// The value of the `END_STREAM` flag on this frame.
public var endStream: Bool

/// The underlying number of padding bytes. If nil, no padding is present.
internal private(set) var _paddingBytes: UInt8?
@inlinable
public var endStream: Bool {
get {
self.booleans.contains(.endStream)
}
set {
if newValue {
self.booleans.insert(.endStream)
} else {
self.booleans.remove(.endStream)
}
}
}

/// The number of padding bytes sent in this frame. If nil, this frame was not padded.
@inlinable
public var paddingBytes: Int? {
get {
return self._paddingBytes.map { Int($0) }
if self.booleans.contains(.paddingPresent) {
return Int(self._paddingBytes)
} else {
return nil
}
}
set {
if let newValue = newValue {
precondition(newValue >= 0 && newValue <= Int(UInt8.max), "Invalid padding byte length: \(newValue)")
self._paddingBytes = UInt8(newValue)
self.booleans.insert(.paddingPresent)
} else {
self._paddingBytes = nil
self.booleans.remove(.paddingPresent)
}
}
}

public init(headers: HPACKHeaders, priorityData: StreamPriorityData? = nil, endStream: Bool = false, paddingBytes: Int? = nil) {
self.headers = headers
self.booleans = .init(rawValue: 0)
self._paddingBytes = 0
self._priorityData = StreamPriorityData(exclusive: false, dependency: .rootStream, weight: 0)

self.priorityData = priorityData
self.endStream = endStream
self.paddingBytes = paddingBytes
Expand Down
4 changes: 4 additions & 0 deletions Tests/NIOHTTP2Tests/HTTP2FrameParserTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2375,4 +2375,8 @@ class HTTP2FrameParserTests: XCTestCase {
payload: .data(.init(data: .byteBuffer(payload), endStream: true)))
try assertReadsFrame(from: greaseBuf, matching: expectedFrame, expectedFlowControlledLength: 13)
}

func testFrameFitsIntoAnExistentialContainer() throws {
XCTAssertLessThanOrEqual(MemoryLayout<HTTP2Frame>.size, 24)
}
}

0 comments on commit 002ec5e

Please sign in to comment.