-
Notifications
You must be signed in to change notification settings - Fork 30.2k
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
net: fix bytesWritten during writev #14236
net: fix bytesWritten during writev #14236
Conversation
When a writev is caused on a socket (sometimes through corking and uncorking), previously net would call Buffer.byteLength on the array of buffers and chunks. This throws a TypeError, because Buffer.byteLength throws when passed a non-string. In dbfe8c4, behavior changed to throw when passed a non-string. This is correct behavior. Previously, it would cast the argument to a string, so before this commit, bytesWritten would give an erroneous value. This commit corrects the behavior equally both before and after dbfe8c4. This commit fixes this bug by iterating over each chunk in the pending stack and calculating the length individually. Also adds a regression test. Refs: nodejs#2960
const common = require('../common'); | ||
const assert = require('assert'); | ||
const net = require('net'); | ||
const Buffer = require('buffer').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.
Nit: Using the global Buffer
is fine in 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.
Removed!
socket.end(); | ||
}); | ||
|
||
server.listen(common.PORT, common.mustCall(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.
Please use a dynamic port for parallel 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.
Thanks! fixed
I think this will have a non 0 performance impact as it uses |
else | ||
bytes += Buffer.byteLength(chunk.chunk, chunk.encoding); | ||
} | ||
} else if (data) { |
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.
Perhaps this can be simplified a bit by merging with the if/else below:
if (Array.isArray(data)) {
// ...
} else if (data instanceof Buffer) {
// ...
} else if (data) {
// ...
}
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 tip! I just collapsed the branches.
lib/net.js
Outdated
for (var i = 0; i < data.length; i++) { | ||
const chunk = data[i]; | ||
|
||
if (chunk instanceof 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.
We can avoid this check for each item in the array if data.allBuffers === true
. writeGeneric()
uses this optimization already.
I think this will have a non 0 performance impact as it uses istanceof for each chunk but I guess there is no way around that.
/cc @lpinca ^
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.
Is it worth optimizing? This is a pretty rarely-traveled code area, so I'd favor simplicity here, but I can change it if you'd 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.
I don't know how much bytesWritten
is used, but I mostly mentioned the optimization because of @lpinca's comment.
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 @mscdex’s suggestion makes sense. Also, using process.binding('util').isUint8Array(chunk)
should be a faster alternative to a full instanceof
check, that should be okay 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.
I've added the data.allBuffers
check, thanks.
@addaleax I'm not so sure that's faster — this benchmark says its a bit slower, probably because of going past the C++ boundary.
lib/net.js
Outdated
else | ||
bytes += Buffer.byteLength(chunk.chunk, chunk.encoding); | ||
} | ||
} else if (data instanceof 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.
These instanceof checks are going to be expensive on perf
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 specific check wasn't added in this PR, it was just down-indented (no perf change).
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 wonder if we could get away with using data != undefined && typeof data !== 'string'
instead? If so, we could first branch on the first condition and then do a if (typeof data !== 'string') { ... } else { ... }
to avoid duplicate if (data)
-style checks.
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.
@mscdex Yes, .write()
will error if it's neither a Buffer nor a string, so this works! I've updated it with the optimization.
I've added a way to avoid the edit CI: https://ci.nodejs.org/job/node-test-pull-request/9211/ |
The CI is green, minus OSX, which seems unrelated. Does this LGTY @mscdex @jasnell @lpinca ? |
This should be faster. Because it's either a Buffer or a string, this check should be equivalent.
LGTM for now if CI is still green: https://ci.nodejs.org/job/node-test-pull-request/9369/. |
Still LGTM. |
Thanks everyone. Two test failures:
These failures are unrelated, so I'll merge tonight without objections. |
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.
LGTM if CI passes.
Landed in 85a5e5a, thanks everyone. |
When a writev is caused on a socket (sometimes through corking and uncorking), previously net would call Buffer.byteLength on the array of buffers and chunks. This throws a TypeError, because Buffer.byteLength throws when passed a non-string. In dbfe8c4, behavior changed to throw when passed a non-string. This is correct behavior. Previously, it would cast the argument to a string, so before this commit, bytesWritten would give an erroneous value. This commit corrects the behavior equally both before and after dbfe8c4. This commit fixes this bug by iterating over each chunk in the pending stack and calculating the length individually. Also adds a regression test. This additionally changes an `instanceof Buffer` check to `typeof !== 'string'`, which should be equivalent. PR-URL: #14236 Reviewed-By: Brian White <[email protected]> Reviewed-By: Luigi Pinca <[email protected]> Reviewed-By: Tobias Nießen <[email protected]> Refs: #2960
When a writev is caused on a socket (sometimes through corking and uncorking), previously net would call Buffer.byteLength on the array of buffers and chunks. This throws a TypeError, because Buffer.byteLength throws when passed a non-string. In dbfe8c4, behavior changed to throw when passed a non-string. This is correct behavior. Previously, it would cast the argument to a string, so before this commit, bytesWritten would give an erroneous value. This commit corrects the behavior equally both before and after dbfe8c4. This commit fixes this bug by iterating over each chunk in the pending stack and calculating the length individually. Also adds a regression test. This additionally changes an `instanceof Buffer` check to `typeof !== 'string'`, which should be equivalent. PR-URL: #14236 Reviewed-By: Brian White <[email protected]> Reviewed-By: Luigi Pinca <[email protected]> Reviewed-By: Tobias Nießen <[email protected]> Refs: #2960
should this be backported to v6.x? |
@MylesBorins possibly — nobody's reported it as a bug before — so it's up to you. |
the original references commit does not exist on v6.x, so I'm going to opt to not backport for now @nodejs/tsc feel free to chime in if you think we should reconsider |
When a
writev
is caused on a socket (sometimes through corking anduncorking), previously net would call
Buffer.byteLength
on the array ofbuffers and chunks. This throws a
TypeError
, becauseBuffer.byteLength
throws when passed a non-string.
In dbfe8c4, behavior changed to throw when passed a non-string. This is
correct behavior. Previously, it would cast the argument to a string, so
before this commit,
bytesWritten
would give an erroneous value. Thiscommit corrects the behavior equally both before and after dbfe8c4.
This commit fixes this bug by iterating over each chunk in the pending
stack and calculating the length individually. Also adds a regression
test.
Refs: #2960
Checklist
make -j4 test
(UNIX), orvcbuild test
(Windows) passesAffected core subsystem(s)
net