Skip to content

Performance degradation when using many stream transforms #249

Closed

Description

@loveencounterflow

I did some stream performance benchmarking of pipelines WRT the number of stream transforms used, and quite frankly, the results are appalling.

One figure is that a pipeline with ~10 useful and realistic transforms achieves a throughput of almost 40,000 lines of text per second, which drops to ~5,000 lines when I add 100 do-nothing pass-through transforms. Add twice that number of transforms, and your throughput gets twice as bad.

In the light of how an earlier issue was dealt with, I fully expect this issue to be closed with comments like "stream transforms do not come for free" and "nobody builds pipelines that long".

Still, since I went and published the details anyway, I thought I might as well open an issue here. Here's the TL;DR (for exegesis head over to the repo):

charts

The upper chart shows time needed (y) in relation to pass-through transforms inserted (x), and lower is better. Readable-Stream is the yellow line. Pipedreams is built on top of through2, and makes heavy use of high-level composition of stream transforms, thereby compounding issues (not that that library is any of your concerns, just saying that some users may actually go and use features as if they were for free).

The lower chart shows throughput in terms of lines per second, and higher is better. Again, it's not so much about the absolute figures; what I'm concerned about here is that inserting a fairly moderate number of no-op calls betrays that time spent in actual processing quickly gets sweeped under the rug in (dis)favor of time spent in the framework.

I didn't do test it for now but my suspicion is that if I was to take out those ~tenish stream transforms that do actual work in the tests, Readable-Streams performance would approach Pull-Stream performance. But then again the whole point in data processing is processing the data, as opposed to just funneling it from input to output, right?

The conclusion of this, barring any errors I will be happy to hear about: NodeJS Readable-Stream put a heavy load on to your processing pipeline. For best results, do not build long processing chains with more than a few (~5, ~10) individually piped transforms, or the framework will totally dominate your performance (IOW destroy it).

I would love to be proven wrong on this one.

Again, details are at https://github.com/loveencounterflow/basic-stream-benchmarks-2.

Activity

  1. michaelnisi commented on Dec 16, 2016

    @michaelnisi

    Thanks for sharing this.

  2. calvinmetcalf commented on Dec 16, 2016

    @calvinmetcalf
    Contributor

    hey thanks for this, while we don't expect piping to be free, we have NO objects towards making it faster. That being said we should remember the trade offs streams are making, namely the mechanisms for back pressure so that slow async streams far down the pipe don't cause things to blow up, so having a bunch of sync transforms isn't really what we are optimizing here for but it is how people (me included) often use it which ends up being like a bulldozer making a sand castle and adding a lot of overhead.

  3. mcollina commented on Dec 16, 2016

    @mcollina
    SponsorMember

    @loveencounterflow thanks for sharing this, and to do a propert analysis I never recommend to have more than 1 or 2 transforms in the pipeline.

    I would love to be proven wrong on this one.

    Why? There are no such claim that node.js streams are a no-overhead abstraction, nor that it is the best or fastest possible abstraction. We claim it is the most common abstraction, because Node.js is built on top of streams, and the API is what it is mainly for backwards compatibility. Because that almost 100% of the ecosystem is built on top of those APIs, we cannot change them, but make them as fast as possible. As a testimony of this, readable-stream works from node v0.8 to node v7.

    Dow you want to help?

    (I would love to be able to make streams a no-overhead abstraction).

  4. mcollina commented on Dec 16, 2016

    @mcollina
    SponsorMember

    From the repository:

    It seems to tell me that the NodeJS maintainers think the drop-in-performance-per-transform is acceptable and to be expected

    we value the ecosystem a lot, and you are actually asking if we want to balance breaking 100% of Node.js vs performance. I currently do not see a way to reconcile this, but I will be very happy if you could step in and find a way.
    As a side note, I've spent 1 month in 2015 to get a 5% improvement on binary streams with _writev.

  5. loveencounterflow commented on Dec 16, 2016

    @loveencounterflow
    Author

    @calvinmetcalf "having a bunch of sync transforms isn't really what we are optimizing here for"—sure, but the no-op transforms used by me are not inherently snyc, they just push back that piece of data and call it done. An asynchronous transform would do no other (except doing something async of course). You'll still lose a lot of performance just by adding transforms.

    Tradeoffs are of course inevitable, but observe that my test implementation for Pull-Streams uses stream-to-pull-stream to convert from a NodeJS FS read stream to a Pull stream, and then back from a Pull stream to a NodeJS FS write stream. IOW somehow someone manages to use NodeJS streams on both ends and handle that backpressure thing plus hundreds of intermediate transforms without totally sacrificing performance.

    Except of course someone can show that Pull-Stream is faulty—that it does not handle back-pressure correctly or has some other likewise decisive shortcoming.

  6. calvinmetcalf commented on Dec 16, 2016

    @calvinmetcalf
    Contributor

    I wouldn't say that pull-stream is faulty per-say, but it is a different modal (from the docs) it doesn't use an internal buffer so transforms can only return a single object instead of (like node streams) return 0, 1 or many objects so they aren't necessarily an apples to apples comparison.

    Node streams try to optimize by giving each individual stream a buffer of data ready to be processed as soon as it's ready, this works great when each transform is doing something asynchronous like calling out to the fs or db or something but can add overhead when things are done synchronously.

    I wonder if it would make sense to add a sync mode to transform streams that just seamlessly pass the transformed date down the line

    sure, but the no-op transforms used by me are not inherently snyc

    I was thinking a useful test would be the difference between one transform that waits 100ms vs 5 that wait 20 ms vs 20 that wait 5ms.

  7. loveencounterflow commented on Dec 16, 2016

    @loveencounterflow
    Author

    @mcollina I fully understand that you're operating under difficult premises here. I came to NodeJS when it was at oh-four, so I've listened in on quite a few discussions surrounding the evolution of NodeJS streams; maybe it's become sort of their hallmark that any new development in this field must (and has) put up with whatever used to be there before.

    One side effect of that is that the API has become somewhat difficult to navigate; another may well be that there is not much left to gain performance-wise (I take it your remark that you've "spent 1 month in 2015 to get a 5% improvement on binary streams with _writev" was intended to communicate exactly this).

  8. loveencounterflow commented on Dec 16, 2016

    @loveencounterflow
    Author

    @calvinmetcalf "I wonder if it would make sense to add a sync mode to transform streams that just seamlessly pass the transformed date down the line"—my thinking exactly!

    "I was thinking a useful test would be the difference between one transform that waits 100ms vs 5 that wait 20 ms vs 20 that wait 5ms."—not sure I understand fully why that should be a good thing, but I can try and implement an option to make all pass-throughs asynchronous (by using setImmediate). It's certainly possible to use setTimeout as well.

  9. mcollina commented on Dec 16, 2016

    @mcollina
    SponsorMember

    @loveencounterflow Node.js has changed a lot, and it is feasible now to become collaborators. Again, I would be extremely happy to review a PR that improve streams perf, that breaks backward compatibility (breaking the ecosystem), and all the rest. I welcome new ideas, and I work on those every day.

    In this precise moment, given that what we believe is an innocent change could have the effect to disrupt a significant portion of the ecosystem, I am leaning towards maintaining the status quo.

    I can try and implement an option to make all pass-throughs synchronous (by using setImmediate)

    setImmediate is not synchronous. Not sure what you mean.

  10. calvinmetcalf commented on Dec 16, 2016

    @calvinmetcalf
    Contributor

    ll pass-throughs synchronous (by using setImmediate).

    I think you have async and sync backwards

  11. loveencounterflow commented on Dec 16, 2016

    @loveencounterflow
    Author

    @calvinmetcalf "I think you have async and sync backwards"—yep that was backwards, corrected. Easy to get wrong with 1 out of 12 of letters changed.

    What I meant to say was it makes totes sense to me to consider a way to make .pipe act synchronously (with an option or a .pipeSync method) because it should simplify a lot of things. I cannot do that for NodeJS streams, but I can add an option to make asynchronous pass-through stream transforms in my benchmarks; it shouldn't change anything for the Readable-Stream case, right? But it could change things for Pull-Stream (which has https://github.com/pull-stream/pull-stream/blob/master/docs/throughs/async-map.md).

    @mcollina thanks for your invitation and I'd love to, but I'm probably not up to it, not time-wise and not skill-wise, especially since this is about streams which so many people have already contributed to, in code and otherwise. To me the most promising direction, at this point in time, is to try and find / write / contribute to a userland implementation of the streams metaphor, one with a reasonable API that can be shown to work correctly, has good performance and is maintainable, and then go and promote that.

    Frankly, maintainability worries me when I look at the readable-stream code; there are so many ifs and whens, and the logic becomes quite convoluted. I did go there and produced flame graphs, and ran the code of my previous benchmarks repo in devtools (head over to https://github.com/loveencounterflow/basic-stream-benchmarks for some results), but I'm lacking the experience with these tools to make much sense of the outputs (other than saying, yes, these stacked towers do look scary).

  12. loveencounterflow commented on Dec 16, 2016

    @loveencounterflow
    Author

    Update added asynchronous option (use -a as in node lib/cli.js copy -a -n 200 to use). Updates to graphs &c upcoming.

    Update the results are online, but there's not much to see. I was mainly interesting to learn whether async-map would degrade the performance of Pull-Streams; it would appear that it does not, which is good.

  13. mcollina commented on Jan 2, 2017

    @mcollina
    SponsorMember

    @loveencounterflow I've reasoned a bit on this, would you mind checking https://github.com/mcollina/syncthrough as well?

  14. venkatperi commented on Jul 7, 2017

    @venkatperi

    Looks like I'm a bit late to this party -- but see similarities with stuff I've been looking into .

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions