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

WHATWG Streams #9

Closed
ronag opened this issue Nov 14, 2022 · 20 comments
Closed

WHATWG Streams #9

ronag opened this issue Nov 14, 2022 · 20 comments

Comments

@ronag
Copy link
Member

ronag commented Nov 14, 2022

WHATWG Streams are slow, and one of the primary reasons for slow performance with fetch in Node.

@mcollina
Copy link
Member

Every stream creates an Error, and creating errors is slow in Node.js because of how we construct them. We also override the base types; I think we should not do that.

@RafaelGSS
Copy link
Member

RafaelGSS commented Nov 14, 2022

Further info: nodejs/undici#1203 (comment)

Every stream creates a NodeError (even in the happy path) which is slower than the common Error: https://github.com/RafaelGSS/nodejs-bench-operations/blob/main/RESULTS-v18.md#nodejs-error

@ronag ronag mentioned this issue Nov 14, 2022
@anonrig
Copy link
Member

anonrig commented Nov 16, 2022

I'm going to work on this in the next couple of weeks.

@RafaelGSS Can you share where stream creates a NodeError? I couldn't find the path.
@mcollina Can you share the reference for override the base types?

@anonrig anonrig self-assigned this Nov 16, 2022
@anonrig anonrig added the benchmark-needed Add to issues that does not have a benchmark label Nov 17, 2022
@anonrig
Copy link
Member

anonrig commented Nov 21, 2022

I did a small perf change to ReadableStream (nodejs/node#45489)

@ronag
Copy link
Member Author

ronag commented Nov 22, 2022

I did a small perf change to ReadableStream (nodejs/node#45489)

I think you are confusing Readable and ReadableStream. That PR has nothing to do with WHATWG streams as far as I can see.

@metcoder95
Copy link
Member

Please correct me if I'm wrong but are @RafaelGSS referring to these errors https://github.com/nodejs/node/blob/1bbd14eac20684a733250629c7896262c0ca4576/lib/internal/webstreams/readablestream.js#L33-L38?

An example from RedeableStream loads this set of errors, which are pre-constructed in https://github.com/nodejs/node/blob/1bbd14eac20684a733250629c7896262c0ca4576/lib/internal/errors.js.

If these are the errors, these should be somehow lazy-loaded or replaced by the common Error constructor?

@RafaelGSS
Copy link
Member

No. Actually, per spec when a default writable/readable is released an error should be propagated https://streams.spec.whatwg.org/#writable-stream-default-writer-release, and nowadays this is quite expensive. My initial analysis expands on this.

@RafaelGSS
Copy link
Member

Honestly, even changing to common Errors, we won't see much improvement.

@mcollina
Copy link
Member

Have you tried with the primoridial Error? https://github.com/nodejs/node/blob/1bbd14eac20684a733250629c7896262c0ca4576/lib/internal/errors.js#L27

@RafaelGSS
Copy link
Member

RafaelGSS commented Nov 30, 2022

Yes, it improves, but it's not a significant improvement that would make sense to break the spec. The problem isn't the kind of Error utilized, it's more about propagating an error (or rejecting a promise) even in the happy path -- which is quite expensive.

I'm tempted to consider a WebStreams implementation on C++, but this is just a guess.

@mcollina
Copy link
Member

The problem is not breaking the spec. We override all the Errors in node.. even the ones that we shouldn't.

@metcoder95
Copy link
Member

Noob question, but the primordials already contain the TypeError class, in theory, the spec specifies to just construct the TypeError and keep it until rejection if further validation fails.

Can be used bypassing the call to https://github.com/nodejs/node/blob/1bbd14eac20684a733250629c7896262c0ca4576/lib/internal/errors.js#L360 and having at least no performance regression meanwhile respecting the spec, it or will have the same impact?
And subsequently, does the error has to have all the properties that the previous method attaches to it?

@RafaelGSS
Copy link
Member

Guys, It's on my radar. I'll be working on it when I have time.

Just created a benchmark PR nodejs/node#45876.

@metcoder95
Copy link
Member

Are you planning to have some specific action items once the assessment? I’m more than happy to help you with if possible 🙂

@RafaelGSS
Copy link
Member

Are you planning to have some specific action items once the assessment? I’m more than happy to help you with if possible 🙂

I'll try to collect some insights and post them here. Eventually, it can be a list of actionable

@RafaelGSS
Copy link
Member

RafaelGSS commented Jan 3, 2023

So, I've spent some time on it again, and here's the summary:

I'll keep investigating other clear wins when I have time, but these two are great starting points.

@KhafraDev
Copy link
Member

KhafraDev commented Jan 12, 2023

ReadableStreamDefaultReader.releaseLock performance can be improved by only creating errors as needed:

diff
diff --git a/lib/internal/webstreams/readablestream.js b/lib/internal/webstreams/readablestream.js
index 5344785b90..18c86163c5 100644
--- a/lib/internal/webstreams/readablestream.js
+++ b/lib/internal/webstreams/readablestream.js
@@ -2029,15 +2029,22 @@ function readableStreamDefaultReaderRelease(reader) {
   readableStreamReaderGenericRelease(reader);
   readableStreamDefaultReaderErrorReadRequests(
     reader,
-    new ERR_INVALID_STATE.TypeError('Releasing reader')
+    'Releasing reader'
   );
 }

-function readableStreamDefaultReaderErrorReadRequests(reader, e) {
-  for (let n = 0; n < reader[kState].readRequests.length; ++n) {
-    reader[kState].readRequests[n][kError](e);
+function readableStreamDefaultReaderErrorReadRequests(reader, m) {
+  const { readRequests } = reader[kState];
+
+  if (readRequests.length) {
+    const error = new ERR_INVALID_STATE.TypeError(m);
+
+    for (let n = 0; n < readRequests.length; ++n) {
+      readRequests[n][kError](error);
+    }
+
+    readRequests.length = 0
   }
-  reader[kState].readRequests = [];
 }

 function readableStreamBYOBReaderRelease(reader) {

For a simple use case, this no longer creates an error:

const readable = getReadableOrSomething()

const reader = readable.getReader()
await reader.read()
reader.releaseLock()

Although I haven't benchmarked the code and haven't run any tests other than the WPTs (which all passed). I don't think releaseLock is a hot path either but it's the first thing I noticed while reading Rafael's comment above.

@RafaelGSS
Copy link
Member

@KhafraDev I think that might be a following up pr after nodejs/node#46086, but instead of creating an error, re-use the one mentioned. But honestly, I don't think it will increase the performance because we're now re-using the errors.

@KhafraDev
Copy link
Member

I see that is just landed! I guess my solution is a viable alternative if there is an issue with re-using errors. 😄

@rluvaton
Copy link
Member

I have hunch it's related to a lot of promise creation (for example the ready promise on writable) node streams do not uses promises extensively IIRC

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

No branches or pull requests

7 participants