Skip to content

Commit e8011a7

Browse files
authored
PseudoHeaderFields adopting CoW to reduce size (#88)
HTTPRequest size 288 -> 16 HTTPResponse size 80 -> 16 rdar://144520456
1 parent 4bd2f81 commit e8011a7

File tree

3 files changed

+178
-24
lines changed

3 files changed

+178
-24
lines changed

Sources/HTTPTypes/HTTPRequest.swift

Lines changed: 108 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -146,53 +146,156 @@ public struct HTTPRequest: Sendable, Hashable {
146146

147147
/// The pseudo header fields of a request.
148148
public struct PseudoHeaderFields: Sendable, Hashable {
149+
private final class _Storage: @unchecked Sendable, Hashable {
150+
var method: HTTPField
151+
var scheme: HTTPField?
152+
var authority: HTTPField?
153+
var path: HTTPField?
154+
var extendedConnectProtocol: HTTPField?
155+
156+
init(
157+
method: HTTPField,
158+
scheme: HTTPField?,
159+
authority: HTTPField?,
160+
path: HTTPField?,
161+
extendedConnectProtocol: HTTPField?
162+
) {
163+
self.method = method
164+
self.scheme = scheme
165+
self.authority = authority
166+
self.path = path
167+
self.extendedConnectProtocol = extendedConnectProtocol
168+
}
169+
170+
func copy() -> Self {
171+
.init(
172+
method: self.method,
173+
scheme: self.scheme,
174+
authority: self.authority,
175+
path: self.path,
176+
extendedConnectProtocol: self.extendedConnectProtocol
177+
)
178+
}
179+
180+
static func == (lhs: _Storage, rhs: _Storage) -> Bool {
181+
lhs.method == rhs.method && lhs.scheme == rhs.scheme && lhs.authority == rhs.authority
182+
&& lhs.path == rhs.path && lhs.extendedConnectProtocol == rhs.extendedConnectProtocol
183+
}
184+
185+
func hash(into hasher: inout Hasher) {
186+
hasher.combine(self.method)
187+
hasher.combine(self.scheme)
188+
hasher.combine(self.authority)
189+
hasher.combine(self.path)
190+
hasher.combine(self.extendedConnectProtocol)
191+
}
192+
}
193+
194+
private var _storage: _Storage
195+
149196
/// The underlying ":method" pseudo header field.
150197
///
151198
/// The value of this field must be a valid method.
152199
///
153200
/// https://www.rfc-editor.org/rfc/rfc9110.html#name-methods
154201
public var method: HTTPField {
155-
willSet {
202+
get {
203+
self._storage.method
204+
}
205+
set {
156206
precondition(newValue.name == .method, "Cannot change pseudo-header field name")
157207
precondition(HTTPField.isValidToken(newValue.rawValue._storage), "Invalid character in method field")
208+
209+
if !isKnownUniquelyReferenced(&self._storage) {
210+
self._storage = self._storage.copy()
211+
}
212+
self._storage.method = newValue
158213
}
159214
}
160215

161216
/// The underlying ":scheme" pseudo header field.
162217
public var scheme: HTTPField? {
163-
willSet {
218+
get {
219+
self._storage.scheme
220+
}
221+
set {
164222
if let name = newValue?.name {
165223
precondition(name == .scheme, "Cannot change pseudo-header field name")
166224
}
225+
226+
if !isKnownUniquelyReferenced(&self._storage) {
227+
self._storage = self._storage.copy()
228+
}
229+
self._storage.scheme = newValue
167230
}
168231
}
169232

170233
/// The underlying ":authority" pseudo header field.
171234
public var authority: HTTPField? {
172-
willSet {
235+
get {
236+
self._storage.authority
237+
}
238+
set {
173239
if let name = newValue?.name {
174240
precondition(name == .authority, "Cannot change pseudo-header field name")
175241
}
242+
243+
if !isKnownUniquelyReferenced(&self._storage) {
244+
self._storage = self._storage.copy()
245+
}
246+
self._storage.authority = newValue
176247
}
177248
}
178249

179250
/// The underlying ":path" pseudo header field.
180251
public var path: HTTPField? {
181-
willSet {
252+
get {
253+
self._storage.path
254+
}
255+
set {
182256
if let name = newValue?.name {
183257
precondition(name == .path, "Cannot change pseudo-header field name")
184258
}
259+
260+
if !isKnownUniquelyReferenced(&self._storage) {
261+
self._storage = self._storage.copy()
262+
}
263+
self._storage.path = newValue
185264
}
186265
}
187266

188267
/// The underlying ":protocol" pseudo header field.
189268
public var extendedConnectProtocol: HTTPField? {
190-
willSet {
269+
get {
270+
self._storage.extendedConnectProtocol
271+
}
272+
set {
191273
if let name = newValue?.name {
192274
precondition(name == .protocol, "Cannot change pseudo-header field name")
193275
}
276+
277+
if !isKnownUniquelyReferenced(&self._storage) {
278+
self._storage = self._storage.copy()
279+
}
280+
self._storage.extendedConnectProtocol = newValue
194281
}
195282
}
283+
284+
init(
285+
method: HTTPField,
286+
scheme: HTTPField?,
287+
authority: HTTPField?,
288+
path: HTTPField?,
289+
extendedConnectProtocol: HTTPField? = nil
290+
) {
291+
self._storage = .init(
292+
method: method,
293+
scheme: scheme,
294+
authority: authority,
295+
path: path,
296+
extendedConnectProtocol: extendedConnectProtocol
297+
)
298+
}
196299
}
197300

198301
/// The pseudo header fields.

Sources/HTTPTypes/HTTPResponse.swift

Lines changed: 64 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -162,32 +162,88 @@ public struct HTTPResponse: Sendable, Hashable {
162162
let code =
163163
Int(codeIterator.next()! - 48) * 100 + Int(codeIterator.next()! - 48) * 10
164164
+ Int(codeIterator.next()! - 48)
165-
return Status(uncheckedCode: code, reasonPhrase: self.reasonPhrase)
165+
return Status(uncheckedCode: code, reasonPhrase: self.pseudoHeaderFields.reasonPhrase)
166166
}
167167
set {
168168
self.pseudoHeaderFields.status.rawValue = ISOLatin1String(unchecked: newValue.fieldValue)
169-
self.reasonPhrase = newValue.reasonPhrase
169+
self.pseudoHeaderFields.reasonPhrase = newValue.reasonPhrase
170170
}
171171
}
172172

173173
/// The pseudo header fields of a response.
174174
public struct PseudoHeaderFields: Sendable, Hashable {
175+
private final class _Storage: @unchecked Sendable, Hashable {
176+
var status: HTTPField
177+
var reasonPhrase: String
178+
179+
init(status: HTTPField, reasonPhrase: String) {
180+
self.status = status
181+
self.reasonPhrase = reasonPhrase
182+
}
183+
184+
func copy() -> Self {
185+
.init(
186+
status: self.status,
187+
reasonPhrase: self.reasonPhrase
188+
)
189+
}
190+
191+
static func == (lhs: _Storage, rhs: _Storage) -> Bool {
192+
lhs.status == rhs.status
193+
}
194+
195+
func hash(into hasher: inout Hasher) {
196+
hasher.combine(self.status)
197+
}
198+
}
199+
200+
private var _storage: _Storage
201+
175202
/// The underlying ":status" pseudo header field.
176203
///
177204
/// The value of this field must be 3 ASCII decimal digits.
178205
public var status: HTTPField {
179-
willSet {
206+
get {
207+
self._storage.status
208+
}
209+
set {
180210
precondition(newValue.name == .status, "Cannot change pseudo-header field name")
181211
precondition(Status.isValidStatus(newValue.rawValue._storage), "Invalid status code")
212+
213+
if !isKnownUniquelyReferenced(&self._storage) {
214+
self._storage = self._storage.copy()
215+
}
216+
self._storage.status = newValue
182217
}
183218
}
219+
220+
var reasonPhrase: String {
221+
get {
222+
self._storage.reasonPhrase
223+
}
224+
set {
225+
if !isKnownUniquelyReferenced(&self._storage) {
226+
self._storage = self._storage.copy()
227+
}
228+
self._storage.reasonPhrase = newValue
229+
}
230+
}
231+
232+
private init(status: HTTPField) {
233+
self._storage = .init(status: status, reasonPhrase: "")
234+
}
235+
236+
init(status: Status) {
237+
self._storage = .init(
238+
status: HTTPField(name: .status, uncheckedValue: ISOLatin1String(unchecked: status.fieldValue)),
239+
reasonPhrase: status.reasonPhrase
240+
)
241+
}
184242
}
185243

186244
/// The pseudo header fields.
187245
public var pseudoHeaderFields: PseudoHeaderFields
188246

189-
private var reasonPhrase: String
190-
191247
/// The response header fields.
192248
public var headerFields: HTTPFields
193249

@@ -196,20 +252,9 @@ public struct HTTPResponse: Sendable, Hashable {
196252
/// - status: The status code and an optional reason phrase.
197253
/// - headerFields: The response header fields.
198254
public init(status: Status, headerFields: HTTPFields = [:]) {
199-
let statusField = HTTPField(name: .status, uncheckedValue: ISOLatin1String(unchecked: status.fieldValue))
200-
self.pseudoHeaderFields = .init(status: statusField)
201-
self.reasonPhrase = status.reasonPhrase
255+
self.pseudoHeaderFields = .init(status: status)
202256
self.headerFields = headerFields
203257
}
204-
205-
public func hash(into hasher: inout Hasher) {
206-
hasher.combine(self.pseudoHeaderFields)
207-
hasher.combine(self.headerFields)
208-
}
209-
210-
public static func == (lhs: HTTPResponse, rhs: HTTPResponse) -> Bool {
211-
lhs.pseudoHeaderFields == rhs.pseudoHeaderFields && lhs.headerFields == rhs.headerFields
212-
}
213258
}
214259

215260
extension HTTPResponse: CustomDebugStringConvertible {
@@ -273,7 +318,7 @@ extension HTTPResponse: Codable {
273318
public func encode(to encoder: Encoder) throws {
274319
var container = encoder.container(keyedBy: CodingKeys.self)
275320
try container.encode(self.pseudoHeaderFields, forKey: .pseudoHeaderFields)
276-
try container.encode(self.reasonPhrase, forKey: .reasonPhrase)
321+
try container.encode(self.pseudoHeaderFields.reasonPhrase, forKey: .reasonPhrase)
277322
try container.encode(self.headerFields, forKey: .headerFields)
278323
}
279324

@@ -288,7 +333,7 @@ extension HTTPResponse: Codable {
288333
guard Status.isValidReasonPhrase(reasonPhrase) else {
289334
throw DecodingError.invalidReasonPhrase(reasonPhrase)
290335
}
291-
self.reasonPhrase = reasonPhrase
336+
self.pseudoHeaderFields.reasonPhrase = reasonPhrase
292337
self.headerFields = try container.decode(HTTPFields.self, forKey: .headerFields)
293338
}
294339
}

Tests/HTTPTypesTests/HTTPTypesTests.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,4 +248,10 @@ final class HTTPTypesTests: XCTestCase {
248248
XCTAssertEqual(trailerFields[HTTPField.Name("trailer1")!], "value1")
249249
XCTAssertEqual(trailerFields[HTTPField.Name("trailer2")!], "value2")
250250
}
251+
252+
func testTypeLayoutSize() {
253+
XCTAssertEqual(MemoryLayout<HTTPRequest>.size, MemoryLayout<AnyObject>.size * 2)
254+
XCTAssertEqual(MemoryLayout<HTTPResponse>.size, MemoryLayout<AnyObject>.size * 2)
255+
XCTAssertEqual(MemoryLayout<HTTPFields>.size, MemoryLayout<AnyObject>.size)
256+
}
251257
}

0 commit comments

Comments
 (0)