Skip to content

Conversation

@brendanashworth
Copy link
Contributor

@brendanashworthbrendanashworth commented Aug 2, 2016

Checklist
  • make -j4 test (UNIX), or vcbuild test nosign (Windows) passes
  • commit message follows commit guidelines
Affected core subsystem(s)

http

Description of change

This commit optimizes outgoing HTTP responses by corking the socket upon each write and then uncorking on the next tick. By doing this we get to remove a "shameful hack" that is years old while at the same time fixing #7914 and getting some performance improvements.

The second commit is just a cleanup commit that makes nothing do even less nothing.

benchmarks!
 improvement significant p.value http/simple.js c=50 chunks=0 length=1024 type="buffer" 1.79 % 6.312062e-01 http/simple.js c=50 chunks=0 length=1024 type="bytes" -0.05 % 9.835961e-01 http/simple.js c=50 chunks=0 length=102400 type="buffer" 5.62 % 1.250185e-01 http/simple.js c=50 chunks=0 length=102400 type="bytes" 504.10 % *** 3.837119e-12 http/simple.js c=50 chunks=0 length=4 type="buffer" 6.68 % ** 9.582778e-03 http/simple.js c=50 chunks=0 length=4 type="bytes" -5.70 % * 3.114749e-02 http/simple.js c=50 chunks=1 length=1024 type="buffer" 2.64 % 4.060173e-01 http/simple.js c=50 chunks=1 length=1024 type="bytes" 7.69 % * 2.072578e-02 http/simple.js c=50 chunks=1 length=102400 type="buffer" 1.05 % 6.860244e-01 http/simple.js c=50 chunks=1 length=102400 type="bytes" 43.97 % *** 1.209619e-11 http/simple.js c=50 chunks=1 length=4 type="buffer" 2.05 % 4.243028e-01 http/simple.js c=50 chunks=1 length=4 type="bytes" 2.94 % 2.278701e-01 http/simple.js c=50 chunks=4 length=1024 type="buffer" 3.99 % 1.028562e-01 http/simple.js c=50 chunks=4 length=1024 type="bytes" 93.05 % *** 6.741354e-16 http/simple.js c=50 chunks=4 length=102400 type="buffer" -0.67 % 8.158624e-01 http/simple.js c=50 chunks=4 length=102400 type="bytes" 21.27 % *** 2.395868e-05 http/simple.js c=50 chunks=4 length=4 type="buffer" 1.20 % 7.638681e-01 http/simple.js c=50 chunks=4 length=4 type="bytes" 83.91 % *** 9.042857e-12 http/simple.js c=500 chunks=0 length=1024 type="buffer" 3.91 % 2.187250e-01 http/simple.js c=500 chunks=0 length=1024 type="bytes" -3.62 % 2.884252e-01 http/simple.js c=500 chunks=0 length=102400 type="buffer" 4.06 % 1.118611e-01 http/simple.js c=500 chunks=0 length=102400 type="bytes" 462.59 % *** 1.329366e-14 http/simple.js c=500 chunks=0 length=4 type="buffer" 3.36 % 1.562332e-01 http/simple.js c=500 chunks=0 length=4 type="bytes" -6.84 % * 2.868192e-02 http/simple.js c=500 chunks=1 length=1024 type="buffer" 1.44 % 6.530913e-01 http/simple.js c=500 chunks=1 length=1024 type="bytes" 7.24 % ** 1.642590e-03 http/simple.js c=500 chunks=1 length=102400 type="buffer" -1.07 % 7.202359e-01 http/simple.js c=500 chunks=1 length=102400 type="bytes" 38.69 % *** 1.560892e-07 http/simple.js c=500 chunks=1 length=4 type="buffer" 0.22 % 9.431102e-01 http/simple.js c=500 chunks=1 length=4 type="bytes" 2.24 % 2.752881e-01 http/simple.js c=500 chunks=4 length=1024 type="buffer" 4.81 % 1.307765e-01 http/simple.js c=500 chunks=4 length=1024 type="bytes" 86.55 % *** 9.028564e-15 http/simple.js c=500 chunks=4 length=102400 type="buffer" 0.91 % 7.931741e-01 http/simple.js c=500 chunks=4 length=102400 type="bytes" 11.95 % ** 1.007196e-03 http/simple.js c=500 chunks=4 length=4 type="buffer" 3.75 % 1.063908e-01 http/simple.js c=500 chunks=4 length=4 type="bytes" 77.90 % *** 2.854701e-10 

CI: https://ci.nodejs.org/job/node-test-pull-request/3498/

@nodejs-github-botnodejs-github-bot added the http Issues or PRs related to the http subsystem. label Aug 2, 2016
@mscdexmscdex added the performance Issues and PRs related to the performance of Node.js. label Aug 2, 2016
@addaleax
Copy link
Member

CI failed with some possibly related failures:

@ronkorving
Copy link
Contributor

Makes you wonder if this block could benefit from being changed to multiple sends like we do with Buffers:

chunk=len.toString(16)+CRLF+chunk+CRLF;ret=this._send(chunk,encoding,callback);

@brendanashworth
Copy link
ContributorAuthor

I do think those failures are related, so I'm going to do some looking into those. @ronkorving good idea — the string concatenation could be a problem. I might try that, thanks!

I'm having slight second thoughts about my approach to this. I think the idea of corking and uncorking on the next tick (like #2020) should be in streams rather than HTTP so I'm going to put together an alternative PR with that in mind. This sort of stuff should also be available to TLS and such.

@jasnell
Copy link
Member

@nodejs/http @nodejs/streams

@mcollina
Copy link
Member

Maybe this might benefit from some string concatenation optimizations, e.g. https://github.com/davidmarkclements/flatstr. cc @davidmarkclements.

@brendanashworth#2020 is unrelated to this case. I agree that uncork is not the perfect API, but the cost of changing it would be very high, and we might discuss that in another issue (feel free to open!)

Copy link
Member

Choose a reason for hiding this comment

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

This is not correct. Multiple cork() call increase the corked counter: https://github.com/nodejs/node/blob/master/lib/_stream_writable.js#L226-L230.

This needs to be a for loop that calls uncork() until corked is 0.

Copy link
ContributorAuthor

Choose a reason for hiding this comment

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

AFAIK that would be a backwards-incompatible change. It would change behavior in something like this:

(req,res)=>{res.cork();res.write('should not write');res.destroy();}

Because Socket#destroy() stops all IO on the socket, it isn't supposed to flush the last message. With this commit, writing to the stream will only cork it once, and thus we only uncork once, to leave the dev's previous cork-intent there. Looping until we uncork fully would write the message which I don't believe happens right now (edge case, yes 😛 )

Copy link
Member

Choose a reason for hiding this comment

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

I'm lost. Why are you uncorking on destroy then?
Probably we shouldn't.

Copy link
ContributorAuthor

Choose a reason for hiding this comment

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

I'm uncorking on destroy because, without it, it broke a test — leaving it corked would be backwards-incompatible as well. Unfortunately this is part of a gray area. There isn't any guarantee that the message would be flushed regardless, as net.js may not be able to write it synchronously. If the message is buffered to be written asynchronously, and res.destroy() is called before that happens, the message will not be written at all. It's an odd gray area, but this is the best way to keep code breakage down.

Copy link
Member

Choose a reason for hiding this comment

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

which test was failing?

IMHO not transmitting the data when destroy() is hit is the correct behavior of destroy(). destroy is a dangerous operation anyway. @mafintosh what do you think?

Beware that, if the socket was corked twice, we need to call enough uncork() for this to take into effect.

Copy link
ContributorAuthor

@brendanashworthbrendanashworthOct 11, 2016

Choose a reason for hiding this comment

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

IMHO not transmitting the data when destroy() is hit is the correct behavior of destroy().

Yes, that's how it behaves right now. .write()can however flush the data synchronously if the socket is ready, so this has undefined behavior:

res.write('this may or may not send');res.destroy();

The data may or may not be flushed. This has been around in node forever.

Beware that, if the socket was corked twice, we need to call enough uncork() for this to take into effect.

That's not what I'm doing here — we add only a single cork, so we uncork only once. If the user wants to cork the socket, write to it and destroy it, we shouldn't flush the message. That would be backwards-incompatible.

(edit): which test was failing?

test/parallel/test-http-abort-client.js fails without this change.

Copy link
Member

@mcollinamcollinaOct 11, 2016

Choose a reason for hiding this comment

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

IMHO not transmitting the data when destroy() is hit is the correct behavior of destroy().
Yes, that's how it behaves right now. .write() can however flush the data synchronously if the socket is ready, so this has undefined behavior:

res.write('this may or may not send');res.destroy();

The behavior of destroy() is something we should define and clarify throughout core.
I'm ok with this particular change. Can you add a reference to the failing unit test in the comment? Otherwise it will be extremely hard to make the connection when looking at the code.

Beware that, if the socket was corked twice, we need to call enough uncork() for this to take into effect.
That's not what I'm doing here — we add only a single cork, so we uncork only once. If the user wants to cork the socket, write to it and destroy it, we shouldn't flush the message. That would be backwards-incompatible.

In this PR, every time write() is called, this.connection.cork() is called. It is entirely possible that _writableState.corked is greater that one.

res.write('a')res.write('b')res.destroy()// will not uncork

Copy link
ContributorAuthor

Choose a reason for hiding this comment

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

The behavior of destroy() is something we should define and clarify throughout core.

Not so much destroy(), it's just the socket.write()API is weird. It returns a boolean about whether or not it works like a sync or async function. It doesn't necessarily have to be fixed, it just has to be worked around because test/parallel/test-http-abort-client.js and userland doesn't always use it perfectly. I'll add a better comment.

It is entirely possible that _writableState.corked is greater that one.

Ah, I think you're right. I'll fix that.

Copy link
Member

Choose a reason for hiding this comment

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

it's just the socket.write() API is weird. It returns a boolean about whether or not it works like a sync or async function.

This is not true. socket.write() is consistent from the point of view of the stream. It returns true if the user can keep writing, and false otherwise. From an implementation perspective, it will return true for a while after "operations become asynchronous" (that's a simplification) because of the buffering.

Add a unit test for the "write-write-destroy" use case, similar to test/parallel/test-http-abort-client.js.

@mcollina
Copy link
Member

Good work! I think it is a good contribution!

@brendanashworth
Copy link
ContributorAuthor

@mcollina thanks for your review! I like your judgement on the streams api, I might open an issue once this PR is 👍

@mcollina
Copy link
Member

I think this should be semver-major.
It's probably safe to be semver-minor, but I prefer to be safe than sorry, and we are cutting v7 soon anyway.

Any other opinion?

@jasnell
Copy link
Member

I happily defer to your judgement on the semveriness of it. Marking semver-major to be safe.

@jasnelljasnell added the semver-major PRs that contain breaking changes and should be released in the next major version. label Aug 4, 2016
@addaleax
Copy link
Member

@mcollina
Copy link
Member

@addaleax we are still waiting for some nits to be fixed. However, I would love to see this lands on v7.

@brendanashworth how are you with this? Do you need any help?

@brendanashworth
Copy link
ContributorAuthor

@mcollina doing good! It's not so much the nits that I need to spend time working on, but I need to investigate the test failures. I just haven't had the time to ask rvagg for access to one of the nodes yet — I'll try to get it working within the week.

@ronkorving
Copy link
Contributor

@brendanashworth Any updates on the suggestion I made?

@jbergstroem
Copy link
Member

Just checking in! Keen on progress updates.

@mcollina
Copy link
Member

@brendanashworth I would love to see this landed. Would you like somebody else to continue your work? Have you have more time to work on this?

@jbergstroem
Copy link
Member

I can facilitate access to any test host if need be.

Since CI results are long gone, I rebased (no conflicts) and started a new run: https://ci.nodejs.org/job/node-test-commit/5483/

@brendanashworth
Copy link
ContributorAuthor

Sorry about the lack of updates — I'll be doing the rest of the work on this over the long weekend 😄 happy to see that there's interest in landing this!

Copy link
Member

@indutnyindutny left a comment

Choose a reason for hiding this comment

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

LGTM if CI is green and benchmarks are good!

Copy link
Member

@mcollinamcollina left a comment

Choose a reason for hiding this comment

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

Good work!

LGTM with some nits to be fixed regarding comments.

Copy link
Member

Choose a reason for hiding this comment

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

can you remove "should" here? They are corked.

Copy link
ContributorAuthor

Choose a reason for hiding this comment

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

got it 👍

Copy link
Member

Choose a reason for hiding this comment

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

can you rephrase this? Something like "avoid writing an empty buffer"

Copy link
ContributorAuthor

Choose a reason for hiding this comment

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

sure!

Copy link
Member

@mcollinamcollina left a comment

Choose a reason for hiding this comment

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

Sorry for the confusion, not LGTM yet. There is the discussion around _writableState.corked to be finalized.

@mcollina
Copy link
Member

@brendanashworth
Copy link
ContributorAuthor

@mcollina thank you for your very thorough review 😅 please take another look. @ronkorving thanks for your suggestion earlier — I've incorporated it into the latest changes.

Regarding the CI and other platforms — there were problems with test-http-client-upgrade2 on freebsd and folks which seem to have gone away on the latest run. I'm not sure whether that's because its flaky or because its fixed...

Copy link
Member

@mcollinamcollina left a comment

Choose a reason for hiding this comment

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

Sorry to be picky, but connectionCorkNT is needed

Copy link
Member

Choose a reason for hiding this comment

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

do not allocate a closure here, it will slow things down considerably. Use connectionCorkNT as it was done before.

@brendanashworthbrendanashworthforce-pushed the http-fix-cork branch 2 times, most recently from c109972 to ec42085CompareOctober 17, 2016 03:04
@rvaggrvaggforce-pushed the master branch 2 times, most recently from c133999 to 83c7a88CompareOctober 18, 2016 17:02
This commit opts for a simpler way to batch writes to HTTP clients into fewer packets. Instead of the complicated snafu which was before, now OutgoingMessage#write automatically corks the socket and uncorks on the next tick, allowing streams to batch them efficiently. It also makes the code cleaner and removes an ugly-ish hack.
The first change in `_writeRaw`: This reduces drops an unnecessary if check (`outputLength`), because it is redone in `_flushOutput`. It also changes an if/else statement to an if statement, because the blocks were unrelated. The second change in `write`: This consolidates code in #write() that handled different string encodings and Buffers. There was no reason to handle the encodings differently, so after splitting them based on Buffer vs encoding, the code is consolidated. This might see a speedup. Shoutout to Ron Korving <[email protected]> for spotting this.
@brendanashworth
Copy link
ContributorAuthor

brendanashworth commented Dec 14, 2016

Copy link
Member

@mcollinamcollina left a comment

Choose a reason for hiding this comment

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

LGTM if CI is green and the perf results are still confirmed.

@mscdex
Copy link
Contributor

mscdex commented Jan 7, 2017

I'm seeing regressions with strong significance when both chunks <= 1 and length <= 1024 for strings (mostly the 'bytes' type in the benchmark):

 improvement significant p.value http/simple.js c=50 chunks=0 length=1024 type="buffer" benchmarker="wrk" 2.88 % *** 1.972424e-07 http/simple.js c=50 chunks=0 length=1024 type="bytes" benchmarker="wrk" -11.54 % *** 6.466690e-32 http/simple.js c=50 chunks=0 length=102400 type="buffer" benchmarker="wrk" 2.57 % *** 3.612979e-08 http/simple.js c=50 chunks=0 length=102400 type="bytes" benchmarker="wrk" 213.15 % *** 2.030384e-32 http/simple.js c=50 chunks=0 length=4 type="buffer" benchmarker="wrk" 3.66 % *** 7.068239e-10 http/simple.js c=50 chunks=0 length=4 type="bytes" benchmarker="wrk" -12.58 % *** 5.493742e-35 http/simple.js c=50 chunks=1 length=1024 type="buffer" benchmarker="wrk" -1.17 % ** 2.759235e-03 http/simple.js c=50 chunks=1 length=1024 type="bytes" benchmarker="wrk" 0.50 % 2.348991e-01 http/simple.js c=50 chunks=1 length=102400 type="buffer" benchmarker="wrk" -0.66 % 7.578784e-02 http/simple.js c=50 chunks=1 length=102400 type="bytes" benchmarker="wrk" 354.77 % *** 3.996312e-36 http/simple.js c=50 chunks=1 length=4 type="buffer" benchmarker="wrk" -1.36 % ** 2.483386e-03 http/simple.js c=50 chunks=1 length=4 type="bytes" benchmarker="wrk" -5.23 % *** 3.465286e-14 http/simple.js c=50 chunks=4 length=1024 type="buffer" benchmarker="wrk" 0.60 % 3.069889e-01 http/simple.js c=50 chunks=4 length=1024 type="bytes" benchmarker="wrk" 775.19 % *** 7.762663e-52 http/simple.js c=50 chunks=4 length=102400 type="buffer" benchmarker="wrk" 0.36 % 3.421042e-01 http/simple.js c=50 chunks=4 length=102400 type="bytes" benchmarker="wrk" 18.99 % *** 1.534301e-32 http/simple.js c=50 chunks=4 length=4 type="buffer" benchmarker="wrk" -0.11 % 7.803265e-01 http/simple.js c=50 chunks=4 length=4 type="bytes" benchmarker="wrk" 835.54 % *** 7.120948e-51 http/simple.js c=500 chunks=0 length=1024 type="buffer" benchmarker="wrk" 3.87 % *** 4.717166e-10 http/simple.js c=500 chunks=0 length=1024 type="bytes" benchmarker="wrk" -11.26 % *** 2.732567e-33 http/simple.js c=500 chunks=0 length=102400 type="buffer" benchmarker="wrk" 2.71 % *** 6.950788e-09 http/simple.js c=500 chunks=0 length=102400 type="bytes" benchmarker="wrk" 230.10 % *** 2.497100e-43 http/simple.js c=500 chunks=0 length=4 type="buffer" benchmarker="wrk" 2.87 % *** 1.664165e-07 http/simple.js c=500 chunks=0 length=4 type="bytes" benchmarker="wrk" -11.78 % *** 1.783927e-30 http/simple.js c=500 chunks=1 length=1024 type="buffer" benchmarker="wrk" -1.56 % *** 3.648309e-04 http/simple.js c=500 chunks=1 length=1024 type="bytes" benchmarker="wrk" 0.59 % 9.217040e-02 http/simple.js c=500 chunks=1 length=102400 type="buffer" benchmarker="wrk" -0.50 % 2.265089e-01 http/simple.js c=500 chunks=1 length=102400 type="bytes" benchmarker="wrk" 406.52 % *** 2.627477e-51 http/simple.js c=500 chunks=1 length=4 type="buffer" benchmarker="wrk" -1.64 % *** 1.109017e-04 http/simple.js c=500 chunks=1 length=4 type="bytes" benchmarker="wrk" -4.80 % *** 1.372847e-14 http/simple.js c=500 chunks=4 length=1024 type="buffer" benchmarker="wrk" 0.30 % 4.962431e-01 http/simple.js c=500 chunks=4 length=1024 type="bytes" benchmarker="wrk" 24.65 % *** 2.527061e-47 http/simple.js c=500 chunks=4 length=102400 type="buffer" benchmarker="wrk" 0.31 % 5.864431e-01 http/simple.js c=500 chunks=4 length=102400 type="bytes" benchmarker="wrk" 25.25 % *** 4.540902e-49 http/simple.js c=500 chunks=4 length=4 type="buffer" benchmarker="wrk" 0.17 % 6.059476e-01 http/simple.js c=500 chunks=4 length=4 type="bytes" benchmarker="wrk" 23.39 % *** 2.387182e-40 

@jasnelljasnell added the stalled Issues and PRs that are stalled. label Mar 1, 2017
@ronkorving
Copy link
Contributor

@brendanashworth Any chance you could look into those performance regressions?

@mscdex
Copy link
Contributor

A rebase is needed.

@mcollinamcollina mentioned this pull request Jun 9, 2017
3 tasks
@jasnell
Copy link
Member

Ping. What do we want to do with this one?

@jasnelljasnell closed this Aug 24, 2017
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

httpIssues or PRs related to the http subsystem.performanceIssues and PRs related to the performance of Node.js.semver-majorPRs that contain breaking changes and should be released in the next major version.stalledIssues and PRs that are stalled.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants

@brendanashworth@addaleax@ronkorving@jasnell@mcollina@jbergstroem@mscdex@indutny@nodejs-github-bot