-
Notifications
You must be signed in to change notification settings - Fork 656
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement lazy headers parsing #291
Conversation
Sources/NIOHTTP1/HTTPTypes.swift
Outdated
@@ -145,8 +145,217 @@ public struct HTTPResponseHead: Equatable { | |||
} | |||
} | |||
|
|||
fileprivate typealias HTTPHeadersStorage = [String: [(String, String)]] // [lowerCasedName: [(originalCaseName, value)] | |||
/// The Index for a header name or value that points into the underlying `ByteBuffer`. | |||
typealias HTTPHeaderIndex = (start: Int, length: Int) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like structs more than tuples for stuff that we're using a lot.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Lukasa ok let me do this and also cache the String.
Sources/NIOHTTP1/HTTPTypes.swift
Outdated
typealias HTTPHeaderIndex = (start: Int, length: Int) | ||
|
||
/// Internal storage used for `HTTPHeaders`. | ||
private final class HTTPHeadersByteStorage : CustomStringConvertible { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok let me do this
Sources/NIOHTTP1/HTTPTypes.swift
Outdated
into.write(buffer: &buf) | ||
} else { | ||
// slow-path.... | ||
// TODO: This can still be improved to write as many continous data as possible and just skip over stuff that was removed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sources/NIOHTTP1Server/main.swift
Outdated
@@ -33,6 +33,42 @@ extension String { | |||
} | |||
} | |||
|
|||
private final class HTTPHelloWorldHandler: ChannelInboundHandler { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also @tanner0101 |
@@ -197,8 +414,7 @@ public struct HTTPHeaders: CustomStringConvertible { | |||
// recommended. | |||
/// - Parameter value: The header field value to add for the given name. | |||
public mutating func add(name: String, value: String) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What would that support look like?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
basically its just faster to write a staticString into a ByteBuffer
so I think it make sense to also support it.
Sources/NIOHTTP1/HTTPTypes.swift
Outdated
@@ -145,8 +145,217 @@ public struct HTTPResponseHead: Equatable { | |||
} | |||
} | |||
|
|||
fileprivate typealias HTTPHeadersStorage = [String: [(String, String)]] // [lowerCasedName: [(originalCaseName, value)] | |||
/// The Index for a header name or value that points into the underlying `ByteBuffer`. | |||
typealias HTTPHeaderIndex = (start: Int, length: Int) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like structs more than tuples for stuff that we're using a lot.
Sources/NIOHTTP1/HTTPTypes.swift
Outdated
typealias HTTPHeaderIndex = (start: Int, length: Int) | ||
|
||
/// Internal storage used for `HTTPHeaders`. | ||
private final class HTTPHeadersByteStorage : CustomStringConvertible { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed.
Sources/NIOHTTP1/HTTPTypes.swift
Outdated
/// Internal storage used for `HTTPHeaders`. | ||
private final class HTTPHeadersByteStorage : CustomStringConvertible { | ||
private var buffer: ByteBuffer | ||
fileprivate var indicies: [HTTPHeaderIndex] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Too many 'i's here: you want "indices"
Sources/NIOHTTP1/HTTPTypes.swift
Outdated
} | ||
|
||
/// Returns the number of headers stored. | ||
var count: Int { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This returns twice the correct number.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good catch!
Sources/NIOHTTP1/HTTPTypes.swift
Outdated
let utf8 = name.utf8 | ||
let stringLength = utf8.count | ||
var idx = 0 | ||
while idx < self.indicies.count { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is probably more naturally expressed as for index in stride(from: 0, to: self.indices.count, by: 2)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TIL... thanks
Sources/NIOHTTP1/HTTPTypes.swift
Outdated
self.storage[name.lowercased()] = nil | ||
if self.storage.remove(name: name) { | ||
// We removed something fomr the internal storage and so it is not continous anymore. | ||
self.continous = false |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems like something the internal storage would know about, not the top-level.
In fact, I think it's strictly computable: the storage is contiguous if each element's start index is two bytes after the previous element's end index. That lets you just count it, though naturally you can also spend a word and keep a boolean that keeps track of this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Lukasa I think I would like to keep the Bool just so we not need to scan the whole array.
Sources/NIOHTTP1/HTTPTypes.swift
Outdated
buffer.write(staticString: crlf) | ||
} | ||
|
||
/// Used for HTTP headers that cannot be joined with commas, e.g. set-cookie. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this correctly handle set-cookie now?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If our tests included something for it yes... I will double-check
} | ||
} | ||
|
||
public func makeIterator() -> Iterator { | ||
return Iterator(wrapping: storage.makeIterator()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why change this label?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No idea... I like storage more. That said I can revert.
@@ -171,7 +171,7 @@ public class HTTPServerUpgradeHandler: ChannelInboundHandler { | |||
/// The core of the upgrade handling logic. | |||
private func handleUpgrade(ctx: ChannelHandlerContext, request: HTTPRequestHead, requestedProtocols: [String]) -> Bool { | |||
let connectionHeader = Set(request.headers.getCanonicalForm("connection").map { $0.lowercased() }) | |||
let allHeaderNames = Set(request.headers.map { $0.name.lowercased() }) | |||
let allHeaderNames: Set<String> = Set(request.headers.map { $0.name.lowercased() }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why does this need an explicit type now?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It does not... Somehow this happened. Let me revert
@@ -732,7 +732,7 @@ class HTTPUpgradeTestCase: XCTestCase { | |||
let resultString = buffers.map { $0.getString(at: $0.readerIndex, length: $0.readableBytes)! }.joined(separator: "") | |||
assertResponseIs(response: resultString, | |||
expectedResponseLine: "HTTP/1.1 101 Switching Protocols", | |||
expectedResponseHeaders: ["x-upgrade-complete: true", "upgrade: myproto", "connection: upgrade"]) | |||
expectedResponseHeaders: ["X-Upgrade-Complete: true", "upgrade: myproto", "connection: upgrade"]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we preserve emitted case, or should we explicitly keep our old behaviour?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if we keep our old behaviour it will be more "expensive" and I think there is nothing that enforce us to do so... @weissi WDYT ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's definitely nothing that forces us to do so, but I guarantee this will break a bunch of people's unit tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could add an optional flag to preserve case, and default it to false
. That lets users have a fast path if they want it while preserving the current behaviour.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@tanner0101 @weissi WDYT ? IMHO its fine as it is honestly as you can not depend on a case outcome (as any is impl specific).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
great work!
Sources/NIOHTTP1/HTTPTypes.swift
Outdated
return | ||
} | ||
array.forEach { | ||
self.indices.removeSubrange($0...$0 + 1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I used an array of optional header indexes in my implementation so that removed values could just be set to nil
without having to re-arrage the array.
removeSubrange
:
Complexity: O(n), where n is the length of the collection.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@tanner0101 in my first iteration I did exactly this but it had some performance impact (most likely because of all the extra branching that was needed while iterating though the headers.
With the current code:
✗ wrk -c 256 -d 10s -t 4 -s ~/Downloads/pipeline-many.lua -H "X-Host: SomeValue" -H "Host: swiftnio.io" -H "ThereAreEvenMoreHeaders: AndMoreValues" http://localhost:8888
Running 10s test @ http://localhost:8888
4 threads and 256 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 124.51ms 182.45ms 1.99s 89.44%
Req/Sec 78.90k 27.18k 210.27k 82.01%
3065753 requests in 10.05s, 146.19MB read
Socket errors: connect 0, read 179, write 0, timeout 104
Requests/sec: 304981.98
Transfer/sec: 14.54MB
With using optionals:
✗ wrk -c 256 -d 10s -t 4 -s ~/Downloads/pipeline-many.lua -H "X-Host: SomeValue" -H "Host: swiftnio.io" -H "ThereAreEvenMoreHeaders: AndMoreValues" http://localhost:8888
Running 10s test @ http://localhost:8888
4 threads and 256 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 117.40ms 149.18ms 2.00s 90.74%
Req/Sec 71.97k 23.04k 165.79k 71.54%
2854242 requests in 10.06s, 136.10MB read
Socket errors: connect 0, read 160, write 0, timeout 122
Requests/sec: 283653.24
Transfer/sec: 13.53MB
As you know its all about trade-offs but the question is how often people will remove headers vs trying to access some. Also note its worst case 0(n) as often it should be able to do much better as the sub-range you remove may be at the end of the array.
These numbers can be reproduced all the time.
Also just for the record this is the performance of current master (without this pr at all):
✗ wrk -c 256 -d 10s -t 4 -s ~/Downloads/pipeline-many.lua -H "X-Host: SomeValue" -H "Host: swiftnio.io" -H "ThereAreEvenMoreHeaders: AndMoreValues" http://localhost:8888
Running 10s test @ http://localhost:8888
4 threads and 256 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 138.82ms 147.82ms 1.99s 85.21%
Req/Sec 51.70k 21.46k 109.23k 65.57%
2042977 requests in 10.09s, 97.42MB read
Socket errors: connect 0, read 49, write 9, timeout 136
Requests/sec: 202409.96
Transfer/sec: 9.65MB
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also note with the latest commit (f533f4d) we are now doing:
wrk -c 256 -d 10s -t 4 -s ~/Downloads/pipeline-many.lua -H "X-Host: SomeValue" -H "Host: swiftnio.io" -H "ThereAreEvenMoreHeaders: AndMoreValues" http://localhost:8888
Running 10s test @ http://localhost:8888
4 threads and 256 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 110.49ms 148.06ms 1.95s 93.29%
Req/Sec 81.94k 24.12k 201.77k 76.19%
3265185 requests in 10.05s, 155.70MB read
Socket errors: connect 0, read 112, write 0, timeout 114
Requests/sec: 325043.36
Transfer/sec: 15.50MB
Less array access FTW ;)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh wow, that's awesome! 👍
Sources/NIOHTTP1/HTTPDecoder.swift
Outdated
@@ -17,10 +17,9 @@ import CNIOHTTPParser | |||
|
|||
private struct HTTPParserState { | |||
var dataAwaitingState: DataAwaitingState = .messageBegin | |||
var currentHeaders: HTTPHeaders? | |||
var currentHTTPHeaders: [HTTPHeaderIndex] = [] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: i would call this currentHeaderIndexes
. HTTP
is redundant and this doesn't actually contain headers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@tanner0101 I used currentHeaderIndices
... thanks for the suggestion :)
@@ -145,8 +145,33 @@ public struct HTTPResponseHead: Equatable { | |||
} | |||
} | |||
|
|||
fileprivate typealias HTTPHeadersStorage = [String: [(String, String)]] // [lowerCasedName: [(originalCaseName, value)] | |||
/// The Index for a header name or value that points into the underlying `ByteBuffer`. | |||
struct HTTPHeaderIndex { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i think it would be less confusing to read this code (and probably less error prone to modify it) if there were another struct that contained two of these: one for the name and one for the value.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@tanner0101 so you suggest having an extra struct which holds two of these structs and store the extra struct in the array (which means removing the % 2 logic) ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@tanner0101 done in f533f4d.... also gave a nice speed up due less array accesses ;)
@swift-nio-bot test this please |
Sources/NIOHTTP1/HTTPTypes.swift
Outdated
} | ||
return [] | ||
let result = self[name] | ||
return result.map { $0.split(separator: ",").map { String($0.trimWhitespace()) } }.reduce([], +) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think using reduce(into:_:) might save some copies here.
Also implemented lazy uri string creation in 52a6139 . Before:
After:
|
I will revert 40571c1 before merging as this is only used atm for benchmarking |
Also I am not sure if we should do 52a6139 or not... I mean for this kind of benchmarks its a win but I would expect that almost all servers will need to access the URI anyway. For the win see #291 (comment) @tanner0101 @weissi @Lukasa WDYT ? |
New numbers with 72d9aca.
|
Sources/NIOHTTP1/HTTPTypes.swift
Outdated
@@ -63,6 +65,21 @@ public struct HTTPRequestHead: Equatable { | |||
} | |||
} | |||
|
|||
/// Internal representation of an URI | |||
enum Uri { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be called URI
.
Sources/NIOHTTP1/HTTPEncoder.swift
Outdated
@@ -95,16 +91,23 @@ private func isChunkedPart(_ headers: HTTPHeaders) -> Bool { | |||
/// | |||
/// Note that for HTTP/1.0 if there is no Content-Length then the response should be followed | |||
/// by connection close. We require that the user send that connection close: we don't do it. | |||
private func sanitizeTransportHeaders(hasBody: HTTPMethod.HasBody, headers: inout HTTPHeaders, version: HTTPVersion) { | |||
/// | |||
/// Returns true if its chunked. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Boo, let's use an explicit enum please. :)
Sources/NIOHTTP1/HTTPTypes.swift
Outdated
/// - Parameter headers: The headers for this HTTP request. | ||
init(version: HTTPVersion, method: HTTPMethod, uri: String, headers: HTTPHeaders) { | ||
init(version: HTTPVersion, method: HTTPMethod, rawUri: Uri, headers: HTTPHeaders) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be rawURI
.
Sources/NIOHTTP1/HTTPTypes.swift
Outdated
@@ -24,8 +24,13 @@ public struct HTTPRequestHead: Equatable { | |||
/// The HTTP method for this request. | |||
public let method: HTTPMethod | |||
|
|||
// Internal representation of the URI. | |||
private let rawUri: Uri |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be rawURI
.
Sources/NIOHTTP1/HTTPTypes.swift
Outdated
case string(String) | ||
case byteBuffer(ByteBuffer) | ||
|
||
var asString: String { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the Swift convention here is actually to provide an extension on String
that gives it an appropriate constructor from this type.
Sources/NIOHTTP1/HTTPTypes.swift
Outdated
let stringLength = utf8.count | ||
var array: [String] = [] | ||
for header in headers { | ||
if stringLength == header.name.length && self.buffer.equalCaseInsensitiveASCII(seq: utf8, at: header.name) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similarly, if we have a proper length check in equalCaseInsensitiveASCII
then we can elide this one.
Sources/NIOHTTP1/HTTPTypes.swift
Outdated
return value.count == header.value.length && self.buffer.getString(at: header.value.start, length: header.value.length) == value | ||
} | ||
return true | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are three methods here that have very similar bodies. We should probably rewrite those as either methods that take non-escaping closures or methods that return the header indices so that we can reduce the code duplication.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Lukasa will review this ... that said while these look kind of similar they are not completely as one is starting from the back and the others not etc.
Sources/NIOHTTP1/HTTPTypes.swift
Outdated
let utf8 = name.utf8 | ||
let stringLength = utf8.count | ||
for header in headers { | ||
if stringLength == header.name.length && self.buffer.equalCaseInsensitiveASCII(seq: utf8, at: header.name) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, this length check can be elided.
Sources/NIOHTTP1/HTTPTypes.swift
Outdated
|
||
var value = self.buffer.getSlice(at: header.value.start, length: header.value.length)! | ||
into.write(buffer: &value) | ||
into.write(staticString: crlf) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right now names and values are always contiguous, right? If they are, we can halve this out by writing just one slice:
var header = self.buffer.getSlice(at: header.name.start, length: header.name.length + header.value.length + 4)
into.write(buffer: &header)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Lukasa I think this will not work in all cases as we can't be sure it uses :
... It could also just use :
(which is legit as well).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why can't we be sure? Whatever we're sending out was either written there by us, or it was provided by http_parser, but in either case it did parse.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Lukasa wouldn't it be possible that http_parser
handed something which is only separated by :
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, but does that matter? The leading whitespace is explicitly optional. RFC 7230 says it right in the ABNF.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Each header field consists of a case-insensitive field name followed by a colon (":"), optional leading whitespace, the field value, and optional trailing whitespace.
(Emphasis mine)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Lukasa and thats exactly the problem ...
self.buffer.getSlice(at: header.name.start, length: header.name.length + header.value.length + 4)
This will only work with :
. Without the whitespace it would be +3
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, sure, but that's ok, just omit that part of the logic and do this:
let fieldLength = (header.value.start + header.value.length) - header.name.start
var header = self.buffer.getSlice(at: header.name.start, length: fieldLength)
into.write(buffer: &header)
into.write(staticString: "\r\n")
That also has the effect of stripping any OWS at the end of the field.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Lukasa good point.. fixing now
Sources/NIOHTTP1/HTTPTypes.swift
Outdated
return [] | ||
|
||
for (b1, b2) in zip(utf8, utf8Other) { | ||
guard b1 & 0xdf == b2 & 0xdf else { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, this should probably check that the bytes are in the ASCII range.
Sources/NIOHTTP1/HTTPDecoder.swift
Outdated
var currentUri: String? | ||
var currentNameIndex: HTTPHeaderIndex? | ||
var currentHeaders: [HTTPHeader] = [] | ||
var currentUri: URI? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given that we're here changing this, can we call this currentURI
now?
Sources/NIOHTTP1/HTTPEncoder.swift
Outdated
ctx.write(wrapOutboundOut(.byteBuffer(buffer)), promise: promise) | ||
} | ||
|
||
private func isChunkedPart(_ headers: HTTPHeaders) -> Bool { | ||
return headers["transfer-encoding"].contains("chunked") | ||
private enum Chunked { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's call this enum BodyFraming
and have three cases, .chunked
, contentLength
, and neither
.
Sources/NIOHTTP1/HTTPTypes.swift
Outdated
@@ -63,6 +65,23 @@ public struct HTTPRequestHead: Equatable { | |||
} | |||
} | |||
|
|||
/// Internal representation of an URI |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: "a URI"
Sources/NIOHTTP1/HTTPTypes.swift
Outdated
} | ||
|
||
private extension String { | ||
init(_ uri: URI) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: might be better to have this label be explicit?
Sources/NIOHTTP1/HTTPTypes.swift
Outdated
} | ||
return withVeryUnsafeBytes { buffer in | ||
// This should never happens as we control when this is called. Adding and assert to ensure this. | ||
assert(index.start <= self.capacity - index.length) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I pretty strongly recommend upgrading this to a precondition
, as the consequences of this failing is memory unsafety.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sources/NIOHTTP1/HTTPTypes.swift
Outdated
var array: [String] = [] | ||
for header in headers { | ||
if self.buffer.equalCaseInsensitiveASCII(view: utf8, at: header.name) { | ||
array.append(string(idx: header.value)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we use the explicit self
here? Took me a while to work out where this String
constructor came from. 😉
Sources/NIOHTTP1/HTTPTypes.swift
Outdated
str.append("(\"") | ||
str.append(string(idx: h.name)) | ||
str.append("\", \"") | ||
str.append(string(idx: h.value)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can these two calls to string
have an explicit self
?
Sources/NIOHTTP1/HTTPTypes.swift
Outdated
for header in headers { | ||
if self.buffer.equalCaseInsensitiveASCII(view: utf8, at: header.name) { | ||
if let value = value { | ||
return self.buffer.equalCase(string: value, at: header.name) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't right: it exits early. That means if you have a header block that consists of [('hello', 'world'), ('hello', 'fish')]
and you ask headers.contains(name: 'hello', value: 'fish')
this will return false
instead of true
. Mind adding a test that covers this case, and then fixing this function?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Lukasa also beside this I used header.name
while it should be header.value
🤦♂️
Sources/NIOHTTP1/HTTPTypes.swift
Outdated
return result | ||
} | ||
|
||
return result.map { $0.split(separator: ",").map { String($0.trimWhitespace()) } }.reduce([], +) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be rebased onto master, which contains an improvement in this code to use flatMap
instead of map.reduce
.
Sources/NIOHTTP1/HTTPTypes.swift
Outdated
if let result = storage[queryName] { | ||
return result.map { tuple in tuple.1.split(separator: ",").map { String($0.trimWhitespace()) } }.reduce([], +) | ||
for (b1, b2) in zip(utf8, utf8Other) { | ||
guard b1 & 0xdf == b2 & 0xdf else { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same question about enforcing ASCII here.
Sources/NIOHTTP1/HTTPTypes.swift
Outdated
storage[keyLower] = (storage[keyLower] ?? []) + [(name, value)] | ||
let nameStart = self.buffer.writerIndex | ||
|
||
for c in name.utf8 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mean, you can use lazy.map { $0.isASCII }.reduce(true) { $0 && $1 }
, but sadly that's only shorter, not necessarily faster.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah... that said we can add an add method that takes StaticString for name and check isASCII directly on it to provide a fast path
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@normanmaurer name.utf8.contains(where: UInt8.isASCII)
or the opposite
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sources/NIOHTTP1/HTTPEncoder.swift
Outdated
ctx.write(wrapOutboundOut(.byteBuffer(buffer)), promise: promise) | ||
} | ||
|
||
private func isChunkedPart(_ headers: HTTPHeaders) -> Bool { | ||
return headers["transfer-encoding"].contains("chunked") | ||
private enum BodyFraming { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorrrrrry, but: mind adding a doc comment?
Sources/NIOHTTP1/HTTPTypes.swift
Outdated
@@ -384,16 +512,18 @@ private extension Substring { | |||
|
|||
extension HTTPHeaders: Equatable { | |||
public static func ==(lhs: HTTPHeaders, rhs: HTTPHeaders) -> Bool { | |||
if lhs.storage.count != rhs.storage.count { | |||
let lhsNames = Set(lhs.names.map { lhs.string(idx: $0).lowercased() }) | |||
let rhsNames = Set(rhs.names.map { rhs.string(idx: $0).lowercased() }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor nit, but: this makes comparing headers slower in the majority of cases. Most header blocks are non-equal, and most do not contain the same number of headers. Any reason we don't want to keep count
around as we did before?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Lukasa nope let me add it.
Sources/NIOHTTP1/HTTPTypes.swift
Outdated
if let result = storage[queryName] { | ||
return result.map { tuple in tuple.1.split(separator: ",").map { String($0.trimWhitespace()) } }.reduce([], +) | ||
for (b1, b2) in zip(self.utf8, utf8Other) { | ||
guard b1 & 0xdf == b2 & 0xdf else { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method still does not enforce ASCII-ness.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
forgot to remove this method... its not used anymore.
Sources/NIOHTTP1/HTTPTypes.swift
Outdated
public init(_ headers: [(String, String)] = []) { | ||
/// - parameters | ||
/// - headers: An initial set of headers to use to populate the header block. | ||
/// - allocator: The allocator to use to allocate the underyling storage. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: underlying
Sources/NIOHTTP1/HTTPTypes.swift
Outdated
return strArray.joined() | ||
} | ||
|
||
/// Constructor used by our decoder to construct headers without the need of convert bytes to string. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: s/convert/converting/
Sources/NIOHTTP1/HTTPTypes.swift
Outdated
@@ -384,16 +496,21 @@ private extension Substring { | |||
|
|||
extension HTTPHeaders: Equatable { | |||
public static func ==(lhs: HTTPHeaders, rhs: HTTPHeaders) -> Bool { | |||
if lhs.storage.count != rhs.storage.count { | |||
guard lhs.names.count == rhs.headers.count else { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are these not comparing against the same property?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
brainfart...
Sources/NIOHTTP1/HTTPTypes.swift
Outdated
/// ASCII string. For future-proofing with HTTP/2 lowercase header names are strongly | ||
// recommended. | ||
/// - Parameter value: The header field value to add for the given name. | ||
public mutating func add(name: StaticString, value: String) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sources/NIOHTTP1/HTTPTypes.swift
Outdated
/// ASCII string. For future-proofing with HTTP/2 lowercase header names are strongly | ||
// recommended. | ||
/// - Parameter value: The header field value to add for the given name. | ||
public mutating func add(name: StaticString, value: String) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This won't work the way you want: I quickly asked @weissi and he reckons that the compiler won't ever select this method over its other overload. Can you try replacing the body of this method with fatalError
and running the tests to see if you ever actually execute this code? If you don't, this probably needs a different set of labels to make the compiler actually choose it.
Sources/NIOHTTP1/HTTPTypes.swift
Outdated
func validateAndWrite(buffer: inout ByteBuffer) -> Int { | ||
switch self { | ||
case .staticString(let string): | ||
precondition(string.isASCII, "name must be ASCII") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Where is StaticString.isASCII
coming from? Why can't we use it on string
below?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Only a few small notes here.
Sources/NIOHTTP1/HTTPEncoder.swift
Outdated
case .yes, .unlikely: | ||
/* leave alone */ | ||
() | ||
return headers["transfer-encoding"].first == "chunked" ? .chunked : .neither |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is wrong. For HTTP/1.0, there is no chunked transfer encoding. This should look for content-length, not chunked TE. I appreciate it won't affect the outcome of the code, but we should aim to get this right in case we want to use this elsewhere.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So you suggest checking for content length only ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup, chunked + HTTP/1.0 is not spec-valid.
Sources/NIOHTTP1/HTTPTypes.swift
Outdated
return false | ||
} | ||
return withVeryUnsafeBytes { buffer in | ||
// This should never happens as we control when this is called. Adding and assert to ensure this. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/and/an/
Sources/NIOHTTP1/HTTPTypes.swift
Outdated
guard byte.isASCII && address.advanced(by: index.start + idx).pointee & 0xdf == byte & 0xdf else { | ||
return false | ||
} | ||
idx += 1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This loop can be cleaned up by using for (idx, byte) in view.enumerated()
, which can avoid using the explicit index.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I'm happy. Just @weissi to review.
Also needs a rebase, natch. |
624762c
to
d4007f0
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
that generally looks really great! left a few comments
Sources/NIOHTTP1/HTTPDecoder.swift
Outdated
var currentHeaders: HTTPHeaders? | ||
var currentUri: String? | ||
var currentNameIndex: HTTPHeaderIndex? | ||
var currentHeaders: [HTTPHeader] = [] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should we maybe 'guess' a usual number of headers and put an array here that has reservedCapacity
?
Sources/NIOHTTP1/HTTPDecoder.swift
Outdated
self.currentHeaders = nil | ||
self.currentUri = nil | ||
self.currentNameIndex = nil | ||
self.currentHeaders = [] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
would need to replicate that here or make it come from some constant
@@ -78,12 +78,15 @@ private func writeHead(wrapOutboundOut: (IOData) -> NIOAny, writeStartLine: (ino | |||
|
|||
var buffer = ctx.channel.allocator.buffer(capacity: 256) | |||
writeStartLine(&buffer) | |||
headers.write(buffer: &buffer) | |||
headers.write(into: &buffer) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
assuming that's not public API, shouldn't we follow the usual ByteBuffer API and make it
buffer.write(headers: headers)
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will do a follow up
Sources/NIOHTTP1/HTTPTypes.swift
Outdated
} | ||
|
||
public var description: String { | ||
var strArray: [String] = [] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why don't we just shove all of that in a [String: String]
dictionary and cause that .description
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good idea
array.forEach { | ||
self.headers.remove(at: $0) | ||
} | ||
self.continuous = false |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
extra PR is better I think
|
||
fileprivate init(wrapping: HTTPHeadersStorage.Iterator) { | ||
self.storageIterator = wrapping | ||
fileprivate init(headerParts: Array<(String, String)>.Iterator) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we write [(String, String)]
instead of Array<...>
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(same above)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@weissi [(String, String)].Iterator
does not compile...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
works for me:
5> func foo(headerParts: Array<(String, String)>.Iterator) {}
6>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sorry, wrong paste
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
6> print([(String, String)].Iterator.self)
IndexingIterator<Array<(String, String)>>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@normanmaurer OMG, apologies, it does work anywhere but in function signatures.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@weissi so all good here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice one, great work1
Motivation: We are currently parsing each header eagly which means we need to convert from bytes to String frequently. The reality is that most of the times the user is not really interested in all the headers and so it is kind of wasteful to do so. Modification: Rewrite our internal storage of HTTPHeaders to use a ByteBuffer as internal storage and so only parse headers on demand. Result: Less overhead for parsing headers.
@swift-nio-bot test this please |
1 similar comment
@swift-nio-bot test this please |
And merged! :) |
Motivation:
We are currently parsing each header eagly which means we need to convert from bytes to String frequently. The reality is that most of the times the user is not really interested in all the headers and so it is kind of wasteful to do so.
Modification:
Rewrite our internal storage of HTTPHeaders to use a ByteBuffer as internal storage and so only parse headers on demand.
Result:
Less overhead for parsing headers.