Skip to content

Improve Performance of HTTP Request and Response inits - #145

Open
nerdsupremacist wants to merge 5 commits into
apple:mainfrom
nerdsupremacist:mq-http-parsed-fields-perf
Open

Improve Performance of HTTP Request and Response inits#145
nerdsupremacist wants to merge 5 commits into
apple:mainfrom
nerdsupremacist:mq-http-parsed-fields-perf

Conversation

@nerdsupremacist

Copy link
Copy Markdown
Member

Motivation

Currently constructing a request or response from parsed http fields requires multiple unnecessary allocations.
Mainly due to:

  1. Allocations from resizing the array as we keep appending more values to it
  2. When running validation checks, we temporarily use an array of values to represent the values for a given field name. When those fields are set this requires an extra array allocation just to run the check

Modifications

  • Reserve needed capacity during initial of parsed fields to prevent multiple subsequent allocations
  • Use lazy sequence methods to avoid allocating a brand new array when validating if all the values for a field are the same

Result

Reduction in allocations and instructions in benchmarks

Before

HTTPRequest.init(parsed)
╒═══════════════════════╤═══════════╤═══════════╤═══════════╤═══════════╤═══════════╤═══════════╤═══════════╤═══════════╕
│ Metric                │        p0 │       p25 │       p50 │       p75 │       p90 │       p99 │      p100 │   Samples │
╞═══════════════════════╪═══════════╪═══════════╪═══════════╪═══════════╪═══════════╪═══════════╪═══════════╪═══════════╡
│ Instructions (K) *    │        14 │        14 │        14 │        14 │        14 │        14 │        51 │     10000 │
├───────────────────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┤
│ Malloc (total) *      │         5 │         5 │         5 │         5 │         5 │         5 │         5 │     10000 │
╘═══════════════════════╧═══════════╧═══════════╧═══════════╧═══════════╧═══════════╧═══════════╧═══════════╧═══════════╛

HTTPResponse.init(parsed)
╒═══════════════════════╤═══════════╤═══════════╤═══════════╤═══════════╤═══════════╤═══════════╤═══════════╤═══════════╕
│ Metric                │        p0 │       p25 │       p50 │       p75 │       p90 │       p99 │      p100 │   Samples │
╞═══════════════════════╪═══════════╪═══════════╪═══════════╪═══════════╪═══════════╪═══════════╪═══════════╪═══════════╡
│ Instructions (K) *    │        18 │        18 │        18 │        18 │        18 │        19 │        41 │     10000 │
├───────────────────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┤
│ Malloc (total) *      │         7 │         7 │         7 │         7 │         7 │         7 │         7 │     10000 │
╘═══════════════════════╧═══════════╧═══════════╧═══════════╧═══════════╧═══════════╧═══════════╧═══════════╧═══════════╛

After

HTTPRequest.init(parsed)
╒═══════════════════════╤═══════════╤═══════════╤═══════════╤═══════════╤═══════════╤═══════════╤═══════════╤═══════════╕
│ Metric                │        p0 │       p25 │       p50 │       p75 │       p90 │       p99 │      p100 │   Samples │
╞═══════════════════════╪═══════════╪═══════════╪═══════════╪═══════════╪═══════════╪═══════════╪═══════════╪═══════════╡
│ Instructions (K) *    │        10 │        10 │        10 │        10 │        10 │        11 │        28 │     10000 │
├───────────────────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┤
│ Malloc (total) *      │         2 │         2 │         2 │         2 │         2 │         2 │         2 │     10000 │
╘═══════════════════════╧═══════════╧═══════════╧═══════════╧═══════════╧═══════════╧═══════════╧═══════════╧═══════════╛

HTTPResponse.init(parsed)
╒═══════════════════════╤═══════════╤═══════════╤═══════════╤═══════════╤═══════════╤═══════════╤═══════════╤═══════════╕
│ Metric                │        p0 │       p25 │       p50 │       p75 │       p90 │       p99 │      p100 │   Samples │
╞═══════════════════════╪═══════════╪═══════════╪═══════════╪═══════════╪═══════════╪═══════════╪═══════════╪═══════════╡
│ Instructions (K) *    │        12 │        12 │        12 │        12 │        12 │        12 │        36 │     10000 │
├───────────────────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┤
│ Malloc (total) *      │         2 │         2 │         2 │         2 │         2 │         2 │         2 │     10000 │
╘═══════════════════════╧═══════════╧═══════════╧═══════════╧═══════════╧═══════════╧═══════════╧═══════════╧═══════════╛

@nerdsupremacist nerdsupremacist changed the title Mq http parsed fields perf Improve Performance of HTTP Request and Response inits Aug 25, 2026
Comment thread Sources/HTTPTypes/HTTPFields.swift Outdated
Comment on lines 146 to 147
func fields(for name: HTTPField.Name) -> some Sequence<HTTPField> {
HTTPFieldSequence(fields: self, name: name)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this should still return the HTTPFieldSequence. HTTPFieldSequence should become internal though.

Comment thread Sources/HTTPTypes/HTTPFields.swift Outdated
Comment on lines +31 to +33
init(reservingCapacity capacity: Int) {
fields.reserveCapacity(capacity)
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

let's remove this and instead just call reserveCapacity on the HTTPFields.

}

init(parsed: [HTTPFields.Element]) throws {
let nonPseudoCount = parsed.count { !$0.name.isPseudo }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is O(n). Let's just over reserve.

@nerdsupremacist nerdsupremacist Aug 25, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Can do. FYI I tested it and it didn't yield any difference in the benchmarks but that could also just be the benchmarks


init(parsed: [HTTPFields.Element]) throws {
let nonPseudoCount = parsed.count { !$0.name.isPseudo }
self.fields = HTTPFields(reservingCapacity: nonPseudoCount)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
self.fields = HTTPFields(reservingCapacity: nonPseudoCount)
self.fields = HTTPFields()
self.fields.reserveCapacity(parsed.count)

Comment on lines 105 to 112
guard self.fields.fields(for: .contentLength).allValuesSame else {
throw ParsingError.multipleContentLength
}
guard self.fields[values: .contentDisposition].allElementsSame else {
guard self.fields.fields(for: .contentDisposition).allValuesSame else {
throw ParsingError.multipleContentDisposition
}
guard self.fields[values: .location].allElementsSame else {
guard self.fields.fields(for: .location).allValuesSame else {
throw ParsingError.multipleLocation

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Urgh we walk the full field list three times here. can we just do it once?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good callout. I have added the validation to happen while we're appending the values rather than after the fact. Given there was a lot of duplicate code I wrapped the validation in a property wrapper (might be overkill let me know and I can remove it)

Comment on lines +55 to +57
for field in parsed {
try self.add(field: field)
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We should do the pseudo headers first. for all remaining headers, we likely can use: append(contentsOf: parsed[pseudoHeaders...])

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants