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

Merge http2 stream & net socket #19527

Closed
wants to merge 14 commits into from

Conversation

aks-
Copy link
Member

@aks- aks- commented Mar 22, 2018

extract out codebase which is identical in both net.js and http2/core.js into new stream_base object.

this should conclude #19060

i'm working on another pr where we can standardize the property name between net.js and http2/core.js. and refactor it further.

Checklist
  • make -j4 test (UNIX), or vcbuild test (Windows) passes
  • tests and/or benchmarks are included
  • documentation is changed or added
  • commit message follows commit guidelines

@nodejs-github-bot nodejs-github-bot added the lib / src Issues and PRs related to general changes in the lib or src directory. label Mar 22, 2018
@aks- aks- changed the title Merge http2 stream net socket Merge http2 stream & net socket Mar 22, 2018
@mscdex
Copy link
Contributor

mscdex commented Mar 22, 2018

I think we should be avoiding adding new methods/properties, underscore-prefixed or not. At the very least we should be using Symbols to hide such implementation details.

Changes like this will also need to be benchmarked appropriately.

const StreamBaseMethods = {
createWriteWrap(req, streamId, handle, oncomplete) {
if (streamId)
req.stream = streamId;
Copy link
Member

Choose a reason for hiding this comment

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

I think this could remain in the HTTP/2 code, it’s specific to the protocol.

@@ -0,0 +1,80 @@
'use strict';
Copy link
Member

Choose a reason for hiding this comment

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

Could you move this file into lib/internal/? The streams/ directory is currently used almost exclusively for the stream module itself, but this implements parts of network & HTTP/2 streams

Copy link
Member Author

Choose a reason for hiding this comment

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

aha i see, makes sense. doing it right now.

lib/net.js Outdated
const handle = this._handle;
this.createWriteWrap(req, null, handle, afterWrite);
if (writev) ret = this.writevGeneric(req, data, cb);
else ret = this.writeGeneric(req, data, encoding, cb);
Copy link
Member

Choose a reason for hiding this comment

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

Style nit: Can you put the bodies on their own lines? That’s more common style inside Node core, I think

@aks-
Copy link
Member Author

aks- commented Mar 22, 2018

@mscdex updated code to use symbols, can you please take a look to see if it looks good?

}

const StreamBase = {
[kCreateWriteWrap](req, handle, oncomplete) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I would call this something else, since it's not actually creating and returning a WriteWrap instance. Perhaps something like kSetupWriteWrap or similar?

if (err) {
return this.destroy(errnoException(err, 'write', error), cb);
}
return null;
Copy link
Contributor

Choose a reason for hiding this comment

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

This is unnecessary and can be removed.

if (err) {
return this.destroy(errnoException(err, 'write', error), cb);
}
return null;
Copy link
Contributor

Choose a reason for hiding this comment

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

This is unnecessary and can be removed.

lib/net.js Outdated
@@ -307,6 +313,7 @@ function Socket(options) {
this[BYTES_READ] = 0;
}
util.inherits(Socket, stream.Duplex);
StreamBase.apply(Socket.prototype);
Copy link
Member

Choose a reason for hiding this comment

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

Can we get rid of apply and use Object.assign()? I find it cleaner.

Object.assign(Socket.prototype, StreamBase);

I also find the StreamBase name a bit odd for a set of mixins.

Copy link
Member Author

Choose a reason for hiding this comment

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

yea, i agree. i called it StreamBaseMethods prior to this... maybe StreamSharedMethods? and +1 on using Object.assign will do soon

@jasnell jasnell requested a review from mcollina March 23, 2018 00:11
@aks-
Copy link
Member Author

aks- commented Mar 23, 2018

@mscdex @lpinca changes applied, can you please check now?

Copy link
Member

@mcollina mcollina left a comment

Choose a reason for hiding this comment

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

I would still prefer prefer if the methods were top-level functions rather than properties of the prototype. However I wouldn't put a -1 on that one.

I would also rename the file to lib/internal/stream_base_commons.js. Those are tightly couple to StreamBase, which is slightly different from require('stream').

@lpinca
Copy link
Member

lpinca commented Mar 23, 2018

👍 to @mcollina's suggestion of using top-level functions. That would also allow us to not use symbols.

@mscdex
Copy link
Contributor

mscdex commented Mar 23, 2018

I also prefer top-level functions, especially since some were already that way to begin with. My suggestion of Symbols was if we absolutely needed them as properties.

Copy link
Member

@apapirovski apapirovski left a comment

Choose a reason for hiding this comment

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

A good start but this needs some cleanup. I wholeheartedly agree with the suggestions to use top level functions.

}

const StreamSharedMethods = {
[kCreateWriteWrap](req, handle, oncomplete) {
Copy link
Member

Choose a reason for hiding this comment

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

This might as well create the WriteWrap and return it.

},

[kWriteGeneric](req, data, encoding, cb) {
const { handle, error } = req;
Copy link
Member

Choose a reason for hiding this comment

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

The error can't be gotten out of ahead of time like this, AFAIK. Also, destructuring assignment with more than 1 variable is still pretty slow last time I tested it.

[kWriteGeneric](req, data, encoding, cb) {
const { handle, error } = req;

const err = _handleWriteReq(req, handle, data, encoding);
Copy link
Member

Choose a reason for hiding this comment

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

Don't get the handle out of req just to pass it in to the function which already accepts req. Just do that work in _handleWriteReq which IMO should drop the leading _. I don't see any reason for it.

req.async = false;
},

[kWritevGeneric](req, data, cb) {
Copy link
Member

Choose a reason for hiding this comment

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

Just make all of these helper functions. There's no need for them to exist on the prototype. this can be a param of the function.

const err = _handleWriteReq(req, handle, data, encoding);

if (err)
return this.destroy(errnoException(err, 'write', error), cb);
Copy link
Member

Choose a reason for hiding this comment

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

In relation to a comment further up above, this should be req.error.

const kWriteGeneric = Symbol('writeGeneric');

function _handleWriteReq(req, handle, data, encoding) {
switch (encoding) {
Copy link
Member

Choose a reason for hiding this comment

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

Just get the handle from the req here instead of passing it in.

lib/net.js Outdated
err = this._handle.writev(req, chunks, allBuffers);
let ret;
const req = new WriteWrap();
const handle = this._handle;
Copy link
Member

Choose a reason for hiding this comment

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

This assignment is unnecessary. Just pass in this._handle below.

@@ -1675,13 +1657,10 @@ class Http2Stream extends Duplex {
const handle = this[kHandle];
Copy link
Member

Choose a reason for hiding this comment

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

This assignment is now unnecessary. Just pass in this[kHandle] down below.

req.async = false;
const err = createWriteReq(req, handle, data, encoding);
if (err)
throw errnoException(err, 'write', req.error);
Copy link
Member

Choose a reason for hiding this comment

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

The semantics here are now different. We're calling destroy with an error instead of just throwing. IMO we should just restore the if (err) blocks and not have that code inside the helpers.

Copy link
Member

Choose a reason for hiding this comment

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

Actually, I see now that @addaleax requested this. Since this would only destroy the stream, I suppose that should be fine.

Copy link
Member

Choose a reason for hiding this comment

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

@apapirovski Fwiw, this is taken from #19389, so that one should land first

lib/net.js Outdated

if (err)
return this.destroy(errnoException(err, 'write', req.error), cb);
if (ret) return ret;
Copy link
Member

Choose a reason for hiding this comment

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

Given the note above re: changed semantics for http2, I would rather we just test for err and then call this.destroy here rather than inside the helpers. The if (ret) return ret; is super opaque as to what the return value is or its meaning.

Copy link
Member

@apapirovski apapirovski Mar 23, 2018

Choose a reason for hiding this comment

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

Since @addaleax requested this related change, I rescind the feedback above. That said, a comment should be left indicating that if (ret) return ret; means there was an error and this.destroy was called with it.

Since this.destroy returns this, the same thing could be communicated without a comment. The variable could be changed to be err and then the code would be if (err) return this;

Copy link
Member

Choose a reason for hiding this comment

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

@apapirovski Just for context, I talked with @aks- about this and I don’t think it’s a good permanent solution anyway, but doing more might require some changes to the bytesWritten tracking for net

But yes, a comment here would be very helpful

@aks- aks- force-pushed the merge-http2-stream-net-socket branch from 16f9ee6 to 560635d Compare March 23, 2018 13:29
@aks-
Copy link
Member Author

aks- commented Mar 23, 2018

i've updated the code. i think it looks good now? included ~all the suggestions

if (err)
return this.destroy(errnoException(err, 'write', req.error), cb);

writeGeneric(this, req, data, encoding, cb);
Copy link
Member

Choose a reason for hiding this comment

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

Would be nice to have this done for writev too :)

Copy link
Member Author

Choose a reason for hiding this comment

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

@addaleax done 👍

lib/net.js Outdated

if (err)
return this.destroy(errnoException(err, 'write', req.error), cb);
if (ret) return ret;
Copy link
Member

Choose a reason for hiding this comment

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

I think there was a suggestion to leave a comment somewhere saying that this line is only run in case of an error?

Copy link
Member Author

Choose a reason for hiding this comment

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

done :D

Copy link
Member

@mcollina mcollina left a comment

Choose a reason for hiding this comment

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

LGTM

lib/net.js Outdated

if (err)
return this.destroy(errnoException(err, 'write', req.error), cb);
// this only runs when writeGeneric returns error.
Copy link
Member

Choose a reason for hiding this comment

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

Nit: the comment is a bit misleading in a couple of ways:

  1. It can also run when writevGeneric fails.
  2. Both writeGeneric and writeGeneric return self when an error occurs instead of the actual error.

I'd use something like "Bail out if handle.write* returned an error".

// Retain chunks
if (err === 0) req._chunks = chunks;

if (err)
Copy link
Member

Choose a reason for hiding this comment

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

Nit: it should be ok to use an else branch on the previous if statement for this.

@lpinca
Copy link
Member

lpinca commented Mar 24, 2018

function writevGeneric(self, req, data, cb) {
const allBuffers = data.allBuffers;
let chunks;
let i;
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 restore these as var? I'm not sure since when let variables are on par with var variables for tight loop performance, but this is one of the hottest path in core, I would prefer if we used very fast ES5

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

@mcollina I might be wrong, but I think it shouldn’t make a difference for variables that are at the top level of a function?

Copy link
Member

Choose a reason for hiding this comment

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

const or let have some overhead in Node 8 and 9 in tight loops such as this one. I’m not sure if this still the case with current master.

I’d prefer if this refactoring was separated from changing from var to const/let.

Copy link
Member Author

Choose a reason for hiding this comment

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

@mcollina makes sense, i'll change them back to var

} else {
chunks = new Array(data.length << 1);
for (i = 0; i < data.length; i++) {
const entry = data[i];
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 make this a var as it was?

lib/net.js Outdated
err = createWriteReq(req, this._handle, data, encoding);
}
let ret;
const req = createWriteWrap(this._handle, afterWrite);
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 make these vars?

@addaleax
Copy link
Member

@aks-
Copy link
Member Author

aks- commented Mar 26, 2018

@mcollina changes made to use back var 👍

Copy link
Member

@mcollina mcollina left a comment

Choose a reason for hiding this comment

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

LGTM

@addaleax
Copy link
Member

@addaleax addaleax added the author ready PRs that have at least one approval, no pending requests for changes, and a CI started. label Mar 27, 2018
@addaleax
Copy link
Member

Landed in c2835e5 🎉

@addaleax addaleax closed this Mar 27, 2018
addaleax pushed a commit that referenced this pull request Mar 27, 2018
Squashed from:

- lib: separate writev responsibilities from writeGeneric
- lib: fix calling of cb twice
- lib: extract streamId out of stream_base to caller
- lib: add symbols instead of methods to hide impl details
- lib: remove unneeded lines
- lib: use Object.assign instead of apply
- lib: rename mixin StreamBase to StreamSharedMethods
- lib: use stream shared funcs as top level instead of
  properties of prototypes
- lib: mv lib/internal/stream_shared_methods.js
  lib/internal/stream_base_commons.js
- lib: add comment for readability
- lib: refactor _writev in Http2Stream
- lib: rephrase comment
- lib: revert usage of const,let for perf reasons

PR-URL: #19527
Refs: #19060
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Anna Henningsen <anna@addaleax.net>
@addaleax addaleax added this to To do in StreamBase refactor via automation Apr 1, 2018
@addaleax addaleax moved this from To do to Done in StreamBase refactor Apr 1, 2018
@targos targos added backport-requested-v9.x and removed author ready PRs that have at least one approval, no pending requests for changes, and a CI started. labels Apr 2, 2018
@targos
Copy link
Member

targos commented Apr 2, 2018

Should this be backported to v9.x-staging? If yes please follow the guide and raise a backport PR, if not let me know or add the dont-land-on label.

BridgeAR pushed a commit to BridgeAR/node that referenced this pull request May 1, 2018
Squashed from:

- lib: separate writev responsibilities from writeGeneric
- lib: fix calling of cb twice
- lib: extract streamId out of stream_base to caller
- lib: add symbols instead of methods to hide impl details
- lib: remove unneeded lines
- lib: use Object.assign instead of apply
- lib: rename mixin StreamBase to StreamSharedMethods
- lib: use stream shared funcs as top level instead of
  properties of prototypes
- lib: mv lib/internal/stream_shared_methods.js
  lib/internal/stream_base_commons.js
- lib: add comment for readability
- lib: refactor _writev in Http2Stream
- lib: rephrase comment
- lib: revert usage of const,let for perf reasons

PR-URL: nodejs#19527
Refs: nodejs#19060
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
lib / src Issues and PRs related to general changes in the lib or src directory.
Projects
No open projects
Development

Successfully merging this pull request may close these issues.

None yet

10 participants