Skip to content

Commit a2b4d28

Browse files
Lukasaglbrnttgjcairo
authored
Make HEADERS frame payload non-indirect (#428)
* Make HEADERS frame payload non-indirect 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. * Update alloc limits --------- Co-authored-by: George Barnett <[email protected]> Co-authored-by: Gustavo Cairo <[email protected]>
1 parent 79802e0 commit a2b4d28

File tree

6 files changed

+138
-57
lines changed

6 files changed

+138
-57
lines changed

Sources/NIOHTTP2/HTTP2Frame.swift

Lines changed: 86 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,15 @@ public struct HTTP2Frame: Sendable {
2727

2828
/// Stream priority data, used in PRIORITY frames and optionally in HEADERS frames.
2929
public struct StreamPriorityData: Equatable, Hashable, Sendable {
30-
public var exclusive: Bool
3130
public var dependency: HTTP2StreamID
31+
public var exclusive: Bool
3232
public var weight: UInt8
33+
34+
public init(exclusive: Bool, dependency: HTTP2StreamID, weight: UInt8) {
35+
self.exclusive = exclusive
36+
self.dependency = dependency
37+
self.weight = weight
38+
}
3339
}
3440

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

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

213219
/// The payload of a `HEADERS` frame.
214220
public struct Headers: Sendable {
221+
/// An OptionSet that keeps track of the various boolean flags in HEADERS.
222+
/// It allows us to elide having our two optionals by keeping track of their
223+
/// optionality here, which frees up a byte and keeps the total size of
224+
/// HTTP2Frame at 24 bytes.
225+
@usableFromInline
226+
struct Booleans: OptionSet, Sendable, Hashable {
227+
@usableFromInline
228+
var rawValue: UInt8
229+
230+
@inlinable
231+
init(rawValue: UInt8) {
232+
self.rawValue = rawValue
233+
}
234+
235+
@usableFromInline static let endStream = Booleans(rawValue: 1 << 0)
236+
@usableFromInline static let priorityPresent = Booleans(rawValue: 1 << 1)
237+
@usableFromInline static let paddingPresent = Booleans(rawValue: 1 << 2)
238+
}
239+
215240
/// The decoded header block belonging to this `HEADERS` frame.
216241
public var headers: HPACKHeaders
217242

243+
/// Stream priority data.
244+
///
245+
/// If `.priorityPresent` is not set in our boolean flags, this value is ignored.
246+
@usableFromInline
247+
var _priorityData: StreamPriorityData
248+
249+
/// The number of padding bytes in this frame.
250+
///
251+
/// If `.paddingPresent` is not set in our boolean flags, this value is ignored.
252+
@usableFromInline
253+
var _paddingBytes: UInt8
254+
255+
/// Boolean flags that control the presence of other values in this frame.
256+
@usableFromInline
257+
var booleans: Booleans
258+
218259
/// The stream priority data transmitted on this frame, if any.
219-
public var priorityData: StreamPriorityData?
260+
@inlinable
261+
public var priorityData: StreamPriorityData? {
262+
get {
263+
if self.booleans.contains(.priorityPresent) {
264+
return self._priorityData
265+
} else {
266+
return nil
267+
}
268+
}
269+
set {
270+
if let newValue = newValue {
271+
self._priorityData = newValue
272+
self.booleans.insert(.priorityPresent)
273+
} else {
274+
self.booleans.remove(.priorityPresent)
275+
}
276+
}
277+
}
220278

221279
/// The value of the `END_STREAM` flag on this frame.
222-
public var endStream: Bool
223-
224-
/// The underlying number of padding bytes. If nil, no padding is present.
225-
internal private(set) var _paddingBytes: UInt8?
280+
@inlinable
281+
public var endStream: Bool {
282+
get {
283+
self.booleans.contains(.endStream)
284+
}
285+
set {
286+
if newValue {
287+
self.booleans.insert(.endStream)
288+
} else {
289+
self.booleans.remove(.endStream)
290+
}
291+
}
292+
}
226293

227294
/// The number of padding bytes sent in this frame. If nil, this frame was not padded.
295+
@inlinable
228296
public var paddingBytes: Int? {
229297
get {
230-
return self._paddingBytes.map { Int($0) }
298+
if self.booleans.contains(.paddingPresent) {
299+
return Int(self._paddingBytes)
300+
} else {
301+
return nil
302+
}
231303
}
232304
set {
233305
if let newValue = newValue {
234306
precondition(newValue >= 0 && newValue <= Int(UInt8.max), "Invalid padding byte length: \(newValue)")
235307
self._paddingBytes = UInt8(newValue)
308+
self.booleans.insert(.paddingPresent)
236309
} else {
237-
self._paddingBytes = nil
310+
self.booleans.remove(.paddingPresent)
238311
}
239312
}
240313
}
241314

242315
public init(headers: HPACKHeaders, priorityData: StreamPriorityData? = nil, endStream: Bool = false, paddingBytes: Int? = nil) {
243316
self.headers = headers
317+
self.booleans = .init(rawValue: 0)
318+
self._paddingBytes = 0
319+
self._priorityData = StreamPriorityData(exclusive: false, dependency: .rootStream, weight: 0)
320+
244321
self.priorityData = priorityData
245322
self.endStream = endStream
246323
self.paddingBytes = paddingBytes

Tests/NIOHTTP2Tests/HTTP2FrameParserTests.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2584,4 +2584,8 @@ class HTTP2FrameParserTests: XCTestCase {
25842584
payload: .data(.init(data: .byteBuffer(payload), endStream: true)))
25852585
try assertReadsFrame(from: greaseBuf, matching: expectedFrame, expectedFlowControlledLength: 13)
25862586
}
2587+
2588+
func testFrameFitsIntoAnExistentialContainer() throws {
2589+
XCTAssertLessThanOrEqual(MemoryLayout<HTTP2Frame>.size, 24)
2590+
}
25872591
}

docker/docker-compose.2204.510.yaml

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,16 @@ services:
2828
test:
2929
image: swift-nio-http2:22.04-5.10
3030
environment:
31-
- MAX_ALLOCS_ALLOWED_1k_requests_inline_interleaved=33150
32-
- MAX_ALLOCS_ALLOWED_1k_requests_inline_noninterleaved=32100
33-
- MAX_ALLOCS_ALLOWED_1k_requests_interleaved=39150
34-
- MAX_ALLOCS_ALLOWED_1k_requests_noninterleaved=38100
35-
- MAX_ALLOCS_ALLOWED_client_server_h1_request_response=284050
36-
- MAX_ALLOCS_ALLOWED_client_server_h1_request_response_inline=269050
37-
- MAX_ALLOCS_ALLOWED_client_server_request_response=253050
38-
- MAX_ALLOCS_ALLOWED_client_server_request_response_inline=244050
39-
- MAX_ALLOCS_ALLOWED_client_server_request_response_many=1198050
40-
- MAX_ALLOCS_ALLOWED_client_server_request_response_many_inline=889050
31+
- MAX_ALLOCS_ALLOWED_1k_requests_inline_interleaved=31150
32+
- MAX_ALLOCS_ALLOWED_1k_requests_inline_noninterleaved=30100
33+
- MAX_ALLOCS_ALLOWED_1k_requests_interleaved=37150
34+
- MAX_ALLOCS_ALLOWED_1k_requests_noninterleaved=36100
35+
- MAX_ALLOCS_ALLOWED_client_server_h1_request_response=280050
36+
- MAX_ALLOCS_ALLOWED_client_server_h1_request_response_inline=265050
37+
- MAX_ALLOCS_ALLOWED_client_server_request_response=249050
38+
- MAX_ALLOCS_ALLOWED_client_server_request_response_inline=240050
39+
- MAX_ALLOCS_ALLOWED_client_server_request_response_many=1194050
40+
- MAX_ALLOCS_ALLOWED_client_server_request_response_many_inline=885050
4141
- MAX_ALLOCS_ALLOWED_create_client_stream_channel=37050
4242
- MAX_ALLOCS_ALLOWED_create_client_stream_channel_inline=37050
4343
- MAX_ALLOCS_ALLOWED_create_client_stream_channel_inline_no_promise_based_API=37050
@@ -47,8 +47,8 @@ services:
4747
- MAX_ALLOCS_ALLOWED_get_100000_headers_canonical_form_trimming_whitespace_from_long_string=300050
4848
- MAX_ALLOCS_ALLOWED_get_100000_headers_canonical_form_trimming_whitespace_from_short_string=200050
4949
- MAX_ALLOCS_ALLOWED_hpack_decoding=5050
50-
- MAX_ALLOCS_ALLOWED_stream_teardown_100_concurrent=282650
51-
- MAX_ALLOCS_ALLOWED_stream_teardown_100_concurrent_inline=281750
50+
- MAX_ALLOCS_ALLOWED_stream_teardown_100_concurrent=272650
51+
- MAX_ALLOCS_ALLOWED_stream_teardown_100_concurrent_inline=271750
5252
- IMPORT_CHECK_ARG=--explicit-target-dependency-import-check error
5353

5454
shell:

docker/docker-compose.2204.58.yaml

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,16 @@ services:
2828
test:
2929
image: swift-nio-http2:22.04-5.8
3030
environment:
31-
- MAX_ALLOCS_ALLOWED_1k_requests_inline_interleaved=33150
32-
- MAX_ALLOCS_ALLOWED_1k_requests_inline_noninterleaved=32100
33-
- MAX_ALLOCS_ALLOWED_1k_requests_interleaved=39150
34-
- MAX_ALLOCS_ALLOWED_1k_requests_noninterleaved=38100
35-
- MAX_ALLOCS_ALLOWED_client_server_h1_request_response=284050
36-
- MAX_ALLOCS_ALLOWED_client_server_h1_request_response_inline=269050
37-
- MAX_ALLOCS_ALLOWED_client_server_request_response=253050
38-
- MAX_ALLOCS_ALLOWED_client_server_request_response_inline=244050
39-
- MAX_ALLOCS_ALLOWED_client_server_request_response_many=1198050
40-
- MAX_ALLOCS_ALLOWED_client_server_request_response_many_inline=889050
31+
- MAX_ALLOCS_ALLOWED_1k_requests_inline_interleaved=31150
32+
- MAX_ALLOCS_ALLOWED_1k_requests_inline_noninterleaved=30100
33+
- MAX_ALLOCS_ALLOWED_1k_requests_interleaved=37150
34+
- MAX_ALLOCS_ALLOWED_1k_requests_noninterleaved=36100
35+
- MAX_ALLOCS_ALLOWED_client_server_h1_request_response=280050
36+
- MAX_ALLOCS_ALLOWED_client_server_h1_request_response_inline=265050
37+
- MAX_ALLOCS_ALLOWED_client_server_request_response=249050
38+
- MAX_ALLOCS_ALLOWED_client_server_request_response_inline=240050
39+
- MAX_ALLOCS_ALLOWED_client_server_request_response_many=1194050
40+
- MAX_ALLOCS_ALLOWED_client_server_request_response_many_inline=885050
4141
- MAX_ALLOCS_ALLOWED_create_client_stream_channel=37050
4242
- MAX_ALLOCS_ALLOWED_create_client_stream_channel_inline=37050
4343
- MAX_ALLOCS_ALLOWED_create_client_stream_channel_inline_no_promise_based_API=37050
@@ -47,8 +47,8 @@ services:
4747
- MAX_ALLOCS_ALLOWED_get_100000_headers_canonical_form_trimming_whitespace_from_long_string=300050
4848
- MAX_ALLOCS_ALLOWED_get_100000_headers_canonical_form_trimming_whitespace_from_short_string=200050
4949
- MAX_ALLOCS_ALLOWED_hpack_decoding=5050
50-
- MAX_ALLOCS_ALLOWED_stream_teardown_100_concurrent=282650
51-
- MAX_ALLOCS_ALLOWED_stream_teardown_100_concurrent_inline=281750
50+
- MAX_ALLOCS_ALLOWED_stream_teardown_100_concurrent=272650
51+
- MAX_ALLOCS_ALLOWED_stream_teardown_100_concurrent_inline=271750
5252
- IMPORT_CHECK_ARG=--explicit-target-dependency-import-check error
5353

5454
shell:

docker/docker-compose.2204.59.yaml

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,16 @@ services:
2828
test:
2929
image: swift-nio-http2:22.04-5.9
3030
environment:
31-
- MAX_ALLOCS_ALLOWED_1k_requests_inline_interleaved=33150
32-
- MAX_ALLOCS_ALLOWED_1k_requests_inline_noninterleaved=32100
33-
- MAX_ALLOCS_ALLOWED_1k_requests_interleaved=39150
34-
- MAX_ALLOCS_ALLOWED_1k_requests_noninterleaved=38100
35-
- MAX_ALLOCS_ALLOWED_client_server_h1_request_response=284050
36-
- MAX_ALLOCS_ALLOWED_client_server_h1_request_response_inline=269050
37-
- MAX_ALLOCS_ALLOWED_client_server_request_response=253050
38-
- MAX_ALLOCS_ALLOWED_client_server_request_response_inline=244050
39-
- MAX_ALLOCS_ALLOWED_client_server_request_response_many=1198050
40-
- MAX_ALLOCS_ALLOWED_client_server_request_response_many_inline=889050
31+
- MAX_ALLOCS_ALLOWED_1k_requests_inline_interleaved=31150
32+
- MAX_ALLOCS_ALLOWED_1k_requests_inline_noninterleaved=30100
33+
- MAX_ALLOCS_ALLOWED_1k_requests_interleaved=37150
34+
- MAX_ALLOCS_ALLOWED_1k_requests_noninterleaved=36100
35+
- MAX_ALLOCS_ALLOWED_client_server_h1_request_response=280050
36+
- MAX_ALLOCS_ALLOWED_client_server_h1_request_response_inline=265050
37+
- MAX_ALLOCS_ALLOWED_client_server_request_response=249050
38+
- MAX_ALLOCS_ALLOWED_client_server_request_response_inline=240050
39+
- MAX_ALLOCS_ALLOWED_client_server_request_response_many=1194050
40+
- MAX_ALLOCS_ALLOWED_client_server_request_response_many_inline=885050
4141
- MAX_ALLOCS_ALLOWED_create_client_stream_channel=37050
4242
- MAX_ALLOCS_ALLOWED_create_client_stream_channel_inline=37050
4343
- MAX_ALLOCS_ALLOWED_create_client_stream_channel_inline_no_promise_based_API=37050
@@ -47,8 +47,8 @@ services:
4747
- MAX_ALLOCS_ALLOWED_get_100000_headers_canonical_form_trimming_whitespace_from_long_string=300050
4848
- MAX_ALLOCS_ALLOWED_get_100000_headers_canonical_form_trimming_whitespace_from_short_string=200050
4949
- MAX_ALLOCS_ALLOWED_hpack_decoding=5050
50-
- MAX_ALLOCS_ALLOWED_stream_teardown_100_concurrent=282650
51-
- MAX_ALLOCS_ALLOWED_stream_teardown_100_concurrent_inline=281750
50+
- MAX_ALLOCS_ALLOWED_stream_teardown_100_concurrent=272650
51+
- MAX_ALLOCS_ALLOWED_stream_teardown_100_concurrent_inline=271750
5252
- IMPORT_CHECK_ARG=--explicit-target-dependency-import-check error
5353

5454
shell:

docker/docker-compose.2204.main.yaml

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,16 @@ services:
2727
test:
2828
image: swift-nio-http2:22.04-main
2929
environment:
30-
- MAX_ALLOCS_ALLOWED_1k_requests_inline_interleaved=33150
31-
- MAX_ALLOCS_ALLOWED_1k_requests_inline_noninterleaved=32100
32-
- MAX_ALLOCS_ALLOWED_1k_requests_interleaved=39150
33-
- MAX_ALLOCS_ALLOWED_1k_requests_noninterleaved=38100
34-
- MAX_ALLOCS_ALLOWED_client_server_h1_request_response=284050
35-
- MAX_ALLOCS_ALLOWED_client_server_h1_request_response_inline=269050
36-
- MAX_ALLOCS_ALLOWED_client_server_request_response=253050
37-
- MAX_ALLOCS_ALLOWED_client_server_request_response_inline=244050
38-
- MAX_ALLOCS_ALLOWED_client_server_request_response_many=1198050
39-
- MAX_ALLOCS_ALLOWED_client_server_request_response_many_inline=889050
30+
- MAX_ALLOCS_ALLOWED_1k_requests_inline_interleaved=31150
31+
- MAX_ALLOCS_ALLOWED_1k_requests_inline_noninterleaved=30100
32+
- MAX_ALLOCS_ALLOWED_1k_requests_interleaved=37150
33+
- MAX_ALLOCS_ALLOWED_1k_requests_noninterleaved=36100
34+
- MAX_ALLOCS_ALLOWED_client_server_h1_request_response=280050
35+
- MAX_ALLOCS_ALLOWED_client_server_h1_request_response_inline=265050
36+
- MAX_ALLOCS_ALLOWED_client_server_request_response=249050
37+
- MAX_ALLOCS_ALLOWED_client_server_request_response_inline=240050
38+
- MAX_ALLOCS_ALLOWED_client_server_request_response_many=1194050
39+
- MAX_ALLOCS_ALLOWED_client_server_request_response_many_inline=885050
4040
- MAX_ALLOCS_ALLOWED_create_client_stream_channel=37050
4141
- MAX_ALLOCS_ALLOWED_create_client_stream_channel_inline=37050
4242
- MAX_ALLOCS_ALLOWED_create_client_stream_channel_inline_no_promise_based_API=37050
@@ -46,8 +46,8 @@ services:
4646
- MAX_ALLOCS_ALLOWED_get_100000_headers_canonical_form_trimming_whitespace_from_long_string=300050
4747
- MAX_ALLOCS_ALLOWED_get_100000_headers_canonical_form_trimming_whitespace_from_short_string=200050
4848
- MAX_ALLOCS_ALLOWED_hpack_decoding=5050
49-
- MAX_ALLOCS_ALLOWED_stream_teardown_100_concurrent=282650
50-
- MAX_ALLOCS_ALLOWED_stream_teardown_100_concurrent_inline=281750
49+
- MAX_ALLOCS_ALLOWED_stream_teardown_100_concurrent=272650
50+
- MAX_ALLOCS_ALLOWED_stream_teardown_100_concurrent_inline=271750
5151
- IMPORT_CHECK_ARG=--explicit-target-dependency-import-check error
5252

5353
shell:

0 commit comments

Comments
 (0)