Skip to content

stream: improve performance for sync write finishes - #30710

Closed
addaleax wants to merge 2 commits into
nodejs:masterfrom
addaleax:writable-nocb
Closed

addaleax wants to merge 2 commits into
nodejs:masterfrom
addaleax:writable-nocb

Conversation

@addaleax

Copy link
Copy Markdown
Member

Improve performance and reduce memory usage when a writable stream
is written to with the same callback (which is the most common case)
and when the write operation finishes synchronously (which is also
often the case).

                                                     confidence improvement accuracy (*)    (**)   (***)
streams/writable-manywrites.js sync='no' n=2000000                  0.99 %       ±3.20%  ±4.28%  ±5.61%
streams/writable-manywrites.js sync='yes' n=2000000        ***    710.69 %      ±19.65% ±26.47% ±35.09%

Refs: #18013
Refs: #18367

@nodejs/streams @ronag

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

Improve performance and reduce memory usage when a writable stream
is written to with the same callback (which is the most common case)
and when the write operation finishes synchronously (which is also
often the case).

                                                         confidence improvement accuracy (*)    (**)   (***)
    streams/writable-manywrites.js sync='no' n=2000000                  0.99 %       ±3.20%  ±4.28%  ±5.61%
    streams/writable-manywrites.js sync='yes' n=2000000        ***    710.69 %      ±19.65% ±26.47% ±35.09%

Refs: nodejs#18013
Refs: nodejs#18367
@nodejs-github-bot nodejs-github-bot added the stream Issues and PRs related to Node.js streams. label Nov 29, 2019
Comment thread lib/_stream_writable.js
state.afterWriteTickInfo.count++;
} else {
state.afterWriteTickInfo = { count: 1, cb, stream, state };
process.nextTick(afterWriteTick, state.afterWriteTickInfo);

ghost Nov 29, 2019

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is there any difference in just using afterWrite directly here (process.nextTick(afterWrite, stream, ...))?

ghost Nov 29, 2019

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@mscdex We need to allocate an object anyway so that we can modify count later, so that’s why it’s not just spreading the arguments right now

ghost left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

lgtm

how did you find out about this problem? Was this tied with a specific use case? I'm almost never pass a callback to write.

@addaleax

ghost commented Nov 29, 2019

Copy link
Copy Markdown
Member Author

how did you find out about this problem? Was this tied with a specific use case? I'm almost never pass a callback to write.

Well … Somebody asked for help privately because they were running a Node.js program, which consisted of a single synchronous loop and printed output using console.log() on each iteration, which lead to memory exhaustion after a few hours because the process.nextTick() queue was filling up 🙃

@ronag

ghost commented Nov 29, 2019

Copy link
Copy Markdown
Member

LGTM

Using the callback is very uncommon in my experience. I'm not sure the extra complexity is worth it? I'm a little worried about the maintenance cost (in general) of streams.

I would maybe instead consider looking into why console.log uses a callback and whether it's strictly necessary.

@ronag

ghost commented Nov 29, 2019

Copy link
Copy Markdown
Member

Actually looking into this further this is not an optimization just for the callback case, but in general when doing multiple write calls in the same tick.

Comment thread lib/_stream_writable.js Outdated
@addaleax

ghost commented Nov 29, 2019

Copy link
Copy Markdown
Member Author

Using the callback is very uncommon in my experience. I'm not sure the extra complexity is worth it? I'm a little worried about the maintenance cost (in general) of streams.

Yeah, this also applies when no callback is passed -- that being said, I would understand if people were concerned about this working only for streams that potentially call write callbacks synchronously (although a number of builtin streams do that).

@nodejs-github-bot

ghost commented Nov 29, 2019

Copy link
Copy Markdown
Collaborator

CI: https://ci.nodejs.org/job/node-test-pull-request/27173/ (:white_check_mark:)

Comment thread lib/_stream_writable.js
}

function afterWrite(stream, state, cb) {
function afterWriteTick({ stream, state, count, cb }) {

ghost Nov 29, 2019

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This might clear the wrong object. I think clearing the count and cb of the passed object is safer then modifying state?

function afterWriteTick(info) {
  const { stream, state, count, cb } = info;
  info.cb = null;
  return afterWrite(stream, state, count, cb);

This would also allow reusing the object and avoiding allocations:

if (!state.afterWriteTickInfo || state.afterWriteTickInfo.cb) {
  state.afterWriteTickInfo = { stream, state, cb, count: 1 };
} else {
  state.afterWriteTickInfo.cb = cb;
  state.afterWriteTickInfo.count = 1;
}

ghost Nov 29, 2019

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Not sure if it matter though

ghost Nov 29, 2019

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This comment is for the row below.

ghost Nov 29, 2019

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@ronag So … the effect of setting afterWriteTickInfo to null is that the next time the code above is reached, a new process.nextTick() call with a new afterWriteTickInfo object is made. That’s always safe, right?

I think setting .cb to null would have the same effect, and .count is cleared anyway. I can do that instead, if you prefer, although it might screw with the map/hidden class of afterWriteTickInfo, as .cb is always a function right now.

ghost Nov 30, 2019

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I was more thinking of the case where you have two different cbs, e.g.

write('a', cba) // schedule tick a
write('b', cbb) // clear info a, schedule tick b

// ...

// tick a
// clear info b

// tick b
// clear nothing

The a tick will actually clear the info for the b tick.

Probably not a problem, but maybe a little weird... I don't have a strong opinion if you think it's fine.

ghost Nov 30, 2019

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

it might screw with the map/hidden class

Oh, I didn't know that null could cause a problems with that once it's been a function type.

ghost Nov 30, 2019

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The a tick will actually clear the info for the b tick.

Probably not a problem, but maybe a little weird... I don't have a strong opinion if you think it's fine.

Yeah, I think that’s fine, because it would only make a difference if there’s a write('b', cbb) inside cba(), and that seems like a somewhat unlikely scenario, and even then it would only affect performance, not behaviour.

@ronag ronag mentioned this pull request Nov 30, 2019
4 tasks
@addaleax addaleax added the author ready PRs with CI started, the required approvals, and no outstanding review comments. label Nov 30, 2019
@addaleax

ghost commented Dec 1, 2019

Copy link
Copy Markdown
Member Author

Landed in 2205f85

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

author ready PRs with CI started, the required approvals, and no outstanding review comments. stream Issues and PRs related to Node.js streams.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants