Skip to content
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

Reuse detected keepalive of http_parser as long as possible #299

Merged
merged 1 commit into from
May 14, 2018

Conversation

normanmaurer
Copy link
Member

Motivation:

To detect if keepalive is used we need to search the headers (if HTTP/1.1 is used). This may be expensive depending how many headers are present. http_parser itself detects already if keep alive should be used and so we can re-use this as long as the user did not modify the headers.

Modifications:

Reuse keepalive parsed by http_parser as long as the headers were not modified.

Result:

Less overhead as long as the headers are not modified.

@normanmaurer normanmaurer requested review from Lukasa and weissi and removed request for Lukasa April 10, 2018 17:25
@normanmaurer
Copy link
Member Author

Also @tanner0101

This gives some perf win in my benchmarks. Let me know what you think about this idea @tanner0101 @weissi @Lukasa

fileprivate enum HTTPHeadersModificationState {
case modifiedNonContinuous
case modified
case notmodified
Copy link
Contributor

Choose a reason for hiding this comment

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

Why did you choose for notmodified over notModified?

Copy link
Member Author

Choose a reason for hiding this comment

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

no idea.. let me fix :)

switch newState {
case .modified where self.state == .notmodified:
self.state = newState
case .modifiedNonContinuous:
Copy link
Contributor

Choose a reason for hiding this comment

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

Any reason to not group this with the above case?

Copy link
Member Author

Choose a reason for hiding this comment

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

good idea :)

Copy link
Contributor

@Lukasa Lukasa left a comment

Choose a reason for hiding this comment

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

So I generally think this is a good idea. I did wonder whether we should also check headers being added/removed to see if they modify the state, but ultimately I don't think it's worthwhile.

fatalError("Should never be called with \(newState)")
default:
break
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I tend to think this kind of method is nicer on the enum. Also no need to say "ifNeeded" in the name.

Copy link
Contributor

Choose a reason for hiding this comment

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

In fact, it may be nicer just to have functions that explain what they are, e.g. state.appendedHeader() and state.removedHeader(). Then you can have some computed vars that use the right names, e.g state.continuous and state.wasModified.

@normanmaurer
Copy link
Member Author

@Lukasa addressed... good idea


mutating func addedHeader() {
switch self {
case .notModified:
Copy link
Contributor

@BasThomas BasThomas Apr 11, 2018

Choose a reason for hiding this comment

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

What about explicitly stating an if-else?

if case .notModified = self {
  self = .modified
}

without the (then unnecessary) return?

Copy link
Contributor

@Lukasa Lukasa left a comment

Choose a reason for hiding this comment

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

Some minor nits.

@@ -38,13 +38,17 @@ public struct HTTPRequestHead: Equatable {
/// The header fields for this HTTP request.
public var headers: HTTPHeaders

/// The keepAlive that may be parsed by http_parser before. This is only safe to be used as long as
/// the user did not modify the headers yet.
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's update this comment slightly:

/// The keepAlive value that was detected by http_parser. This can only be used safely
/// as long as the user has not made modifications to this header block.

@@ -202,6 +211,31 @@ private extension UInt8 {
}
}

fileprivate enum HTTPHeadersModificationState {
Copy link
Contributor

Choose a reason for hiding this comment

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

Mind adding some quick doc comments here?

}

mutating func removedHeader() {
if self != .modifiedNonContinuous {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why doesn't this use if case?

@normanmaurer
Copy link
Member Author

@Lukasa addressed

Copy link
Member

@weissi weissi left a comment

Choose a reason for hiding this comment

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

I'd prefer to have the state tracking in one place and not to store a potentially wrong keepAlive value

case modifiedNonContinuous
/// Was modified and the underlying storage is continous.
case modified

Copy link
Member

Choose a reason for hiding this comment

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

nit: why a space here?

@@ -38,13 +38,17 @@ public struct HTTPRequestHead: Equatable {
/// The header fields for this HTTP request.
public var headers: HTTPHeaders

/// The keepAlive value that was detected by http_parser. This can only be used safely
/// as long as the user has not made modifications to this header block.
private let keepAlive: Bool?
Copy link
Member

Choose a reason for hiding this comment

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

I don't really like having a state variable here that is only valid when also consulting another variable. I'd say the whole state should live in the headers. Couldn't we architect it this way: We allow (as an internal constructor) to pre-seed the headers with a keep alive state. Whenever the headers change, we invalidate that.

So the keep-alive state in the headers could be one of the following:

  • keepAlive: no header scan needed, I know I have a Connection: keep-alive header
  • close: no header scan needed, I know I have a Connection: close header
  • default: no header scan needed, I know I had neither Connection: keep-alive nor Connection: close
  • needsScan: header scan needed

so internally we could pre-seed it with either keepAlive, close, or default, depending on what http_parser tells us. Then on any modification we change it to needsScan.

Copy link
Member Author

Choose a reason for hiding this comment

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

@weissi sounds good... will do later today :)

@normanmaurer
Copy link
Member Author

@Lukasa @weissi PTAL again

// We know we should close the connection.
case close
// We need to scan the headers to find out if keep alive is used or not
case needScan(continuous: Bool)
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is there a continuous flag here? You have a HTTPHeadersModificationState that tracks this.

Copy link
Member Author

Choose a reason for hiding this comment

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

Because I forgot to remove the modification state... it’s all tracked via this one now

Copy link
Member Author

Choose a reason for hiding this comment

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

ok removed

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't like this much. I'd rather this bool was moved out of this enum: it just has nothing to do with what this enum is tracking.

If we do that, we can then name the third case of this enum unknown, and then the implementation of HTTPHeaders.isKeepAlive gets a lot nicer because it can just return .unknown when it doesn't know the answer instead of being passed the HTTP version.

@Lukasa
Copy link
Contributor

Lukasa commented Apr 12, 2018

@swift-nio-bot test this please

// We know we should close the connection.
case close
// We need to scan the headers to find out if keep alive is used or not
case needScan(continuous: Bool)
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't like this much. I'd rather this bool was moved out of this enum: it just has nothing to do with what this enum is tracking.

If we do that, we can then name the third case of this enum unknown, and then the implementation of HTTPHeaders.isKeepAlive gets a lot nicer because it can just return .unknown when it doesn't know the answer instead of being passed the HTTP version.

@Lukasa Lukasa added the semver/patch No public API change. label Apr 12, 2018
@normanmaurer
Copy link
Member Author

@Lukasa @weissi need to investigate as it actually makes things slower now ?!?!?

@weissi
Copy link
Member

weissi commented Apr 27, 2018

@normanmaurer ping, what’s up with this friend?

@normanmaurer
Copy link
Member Author

@weissi I need to investigate as it actually resulted in a slowdown after rebasing

@weissi
Copy link
Member

weissi commented Apr 27, 2018

@normanmaurer you probably want to use the isolated (no network) perf test to verify that. Otherwise there’ll be too much noise I suspect

@tomerd
Copy link
Member

tomerd commented Apr 30, 2018

@swift-nio-bot test this please

@normanmaurer
Copy link
Member Author

Alright updated please take a look again @weissi @Lukasa

Before:

 wrk -c 256 -d 10s -t 4 -s ~/Downloads/pipeline-many.lua -H "X-Host: SomeValue" -H "Host: swiftnio.io" -H "ThereAreEvenMoreHeaders: AndMoreValues" http://127.0.0.1:8888
Running 10s test @ http://127.0.0.1:8888
  4 threads and 256 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   104.83ms  209.95ms   2.00s    90.27%
    Req/Sec   132.15k    31.57k  346.95k    88.41%
  5234833 requests in 10.04s, 249.62MB read
  Socket errors: connect 0, read 162, write 0, timeout 17
Requests/sec: 521428.30
Transfer/sec:     24.86MB

with this pr:

wrk -c 256 -d 10s -t 4 -s ~/Downloads/pipeline-many.lua -H "X-Host: SomeValue" -H "Host: swiftnio.io" -H "ThereAreEvenMoreHeaders: AndMoreValues" http://127.0.0.1:8888
Running 10s test @ http://127.0.0.1:8888
  4 threads and 256 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   105.65ms  215.06ms   1.99s    91.27%
    Req/Sec   137.61k    32.64k  329.69k    89.59%
  5406252 requests in 10.03s, 257.79MB read
  Socket errors: connect 0, read 137, write 4, timeout 24
Requests/sec: 538977.98
Transfer/sec:     25.70MB

@normanmaurer
Copy link
Member Author

And @tanner0101

Copy link
Contributor

@Lukasa Lukasa left a comment

Choose a reason for hiding this comment

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

Some minor nits, and it'd be nice to have some testing of these behaviours, but otherwise fine.

@@ -15,6 +15,14 @@
import NIO
import CNIOHTTPParser

private extension UnsafeMutablePointer where Pointee == http_parser {

Copy link
Contributor

Choose a reason for hiding this comment

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

No need for this whitespace.

private let connectionUtf8 = "connection".utf8


// Keep state of keep alive state.
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: keep track, not keep state.

@normanmaurer
Copy link
Member Author

@Lukasa yep will add some tests just to ensure we correctly set it to "unknown" when we add / remove connection header.

Motivation:

To detect if keepalive is used we need to search the headers (if HTTP/1.1 is used). This may be expensive depending how many headers are present. http_parser itself detects already if keep alive should be used and so we can re-use this as long as the user did not modify the headers.

Modifications:

Reuse keepalive parsed by http_parser as long as the headers were not modified.

Result:

Less overhead as long as the headers are not modified.
@normanmaurer
Copy link
Member Author

@Lukasa added some tests.

Copy link
Contributor

@Lukasa Lukasa left a comment

Choose a reason for hiding this comment

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

LGTM.

@Lukasa Lukasa added this to the 1.7.0 milestone May 14, 2018
case .keepAlive:
return true
case .unknown:
guard let connection = self["connection"].first?.lowercased() else {
Copy link
Member

Choose a reason for hiding this comment

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

whilst you're working on this: worth fixing this implementation of follow-up?

Copy link
Member Author

Choose a reason for hiding this comment

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

Let me do a follow up

Copy link
Member

@weissi weissi left a comment

Choose a reason for hiding this comment

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

thanks! LGTM, just one question in there,.

@normanmaurer normanmaurer merged commit a7614d9 into apple:master May 14, 2018
@normanmaurer normanmaurer deleted the use_parser_keepalive branch May 14, 2018 16:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
semver/patch No public API change.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants