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

net: fix bytesWritten during writev #14236

Closed

Conversation

brendanashworth
Copy link
Contributor

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: #2960

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
Affected core subsystem(s)

net

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
@nodejs-github-bot nodejs-github-bot added the net Issues and PRs related to the net subsystem. label Jul 14, 2017
const common = require('../common');
const assert = require('assert');
const net = require('net');
const Buffer = require('buffer').Buffer;
Copy link
Member

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.

Copy link
Contributor Author

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() {
Copy link
Member

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks! fixed

@lpinca
Copy link
Member

lpinca commented Jul 14, 2017

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.

else
bytes += Buffer.byteLength(chunk.chunk, chunk.encoding);
}
} else if (data) {
Copy link
Contributor

@mscdex mscdex Jul 14, 2017

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) {
  // ...
}

Copy link
Contributor Author

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)
Copy link
Contributor

@mscdex mscdex Jul 14, 2017

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 ^

Copy link
Contributor Author

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.

Copy link
Contributor

@mscdex mscdex Jul 15, 2017

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.

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 @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.

Copy link
Contributor Author

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) {
Copy link
Member

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

Copy link
Contributor Author

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).

Copy link
Contributor

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.

Copy link
Contributor Author

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.

@brendanashworth
Copy link
Contributor Author

brendanashworth commented Jul 18, 2017

I've added a way to avoid the instanceof check, PTAL. Please note that the only performance regression involved in this change is the new Array.isArray call, the rest helps/does not hurt previous performance.

edit CI: https://ci.nodejs.org/job/node-test-pull-request/9211/
.. edit second CI: https://ci.nodejs.org/job/node-test-pull-request/9278/

@brendanashworth
Copy link
Contributor Author

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.
@mscdex
Copy link
Contributor

mscdex commented Jul 27, 2017

LGTM for now if CI is still green: https://ci.nodejs.org/job/node-test-pull-request/9369/.

@lpinca
Copy link
Member

lpinca commented Jul 27, 2017

Still LGTM.

@brendanashworth
Copy link
Contributor Author

Thanks everyone. Two test failures:

  • ubuntu1604-arm64: not ok 1494 known_issues/test-stdout-buffer-flush-on-exit
  • win2008r2/vs2015/3: not ok 388 inspector/test-inspector-stop-profile-after-done

These failures are unrelated, so I'll merge tonight without objections.

Copy link
Member

@tniessen tniessen 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 passes.

@brendanashworth
Copy link
Contributor Author

Landed in 85a5e5a, thanks everyone.

brendanashworth added a commit that referenced this pull request Jul 29, 2017
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
addaleax pushed a commit that referenced this pull request Aug 1, 2017
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
@addaleax addaleax mentioned this pull request Aug 2, 2017
@MylesBorins
Copy link
Contributor

should this be backported to v6.x?

@brendanashworth
Copy link
Contributor Author

@MylesBorins possibly — nobody's reported it as a bug before — so it's up to you.

@MylesBorins
Copy link
Contributor

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
net Issues and PRs related to the net subsystem.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants