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

errors: improve error creation performance #46648

Closed
wants to merge 15 commits into from

Conversation

BridgeAR
Copy link
Member

This is a first step to significantly faster NodeErorrs. The code itself should also be cleaned up in a following PR but it already became bigger as I worked upon different parts related to errors in Node.js.

This does change stack traces for users in few cases: originally we added extra stack frames to compensate for stack frames we want to hide. The mechanism as it was implemented had a high cost on all Node.js errors in case they were created in a deeply nested function. Stack frames that are removed are now not going to be compensated for anymore. I am going to think about other ways to do this later on again.

Refs: nodejs/performance#40

                                                                        confidence improvement accuracy (*)   (**)   (***)
error/hidestackframes.js nested=0 n=10000 type='direct-call-noerr'                      0.52 %       ±5.47% ±7.30%  ±9.56%
error/hidestackframes.js nested=0 n=10000 type='direct-call-throw'             ***     81.31 %       ±7.21% ±9.66% ±12.72%
error/hidestackframes.js nested=0 n=10000 type='hide-stackframes-noerr'                 2.66 %       ±6.63% ±8.83% ±11.49%
error/hidestackframes.js nested=0 n=10000 type='hide-stackframes-throw'        ***     79.61 %       ±6.63% ±8.87% ±11.66%
error/hidestackframes.js nested=1 n=10000 type='direct-call-noerr'                      0.21 %       ±3.57% ±4.76%  ±6.19%
error/hidestackframes.js nested=1 n=10000 type='direct-call-throw'             ***     90.10 %       ±5.93% ±7.93% ±10.40%
error/hidestackframes.js nested=1 n=10000 type='hide-stackframes-noerr'                -2.34 %       ±4.07% ±5.42%  ±7.06%
error/hidestackframes.js nested=1 n=10000 type='hide-stackframes-throw'        ***     85.71 %       ±6.18% ±8.23% ±10.73%
error/node-error.js type='node' n=100000                                       ***    166.64 %       ±7.01% ±9.39% ±12.35%
error/node-error.js type='regular' n=100000                                            -2.29 %       ±2.98% ±3.97%  ±5.16%

@nodejs-github-bot
Copy link
Collaborator

Review requested:

  • @nodejs/crypto
  • @nodejs/http2
  • @nodejs/loaders
  • @nodejs/modules
  • @nodejs/net
  • @nodejs/streams
  • @nodejs/test_runner

@nodejs-github-bot nodejs-github-bot added lib / src Issues and PRs related to general changes in the lib or src directory. needs-ci PRs that need a full CI run. labels Feb 14, 2023
@BridgeAR BridgeAR force-pushed the improve-error-performance branch from 846a306 to 15854a8 Compare February 14, 2023 12:21
Copy link
Member

@anonrig anonrig left a comment

Choose a reason for hiding this comment

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

Amazing!

lib/assert.js Outdated Show resolved Hide resolved
@anonrig anonrig added the performance Issues and PRs related to the performance of Node.js. label Feb 14, 2023
@anonrig
Copy link
Member

anonrig commented Feb 14, 2023

cc @nodejs/performance

@BridgeAR
Copy link
Member Author

I checked how much potential is left for further improvements and it turns out this is almost an ideal state.

Our main overhead is due to creating subclasses. ~50% over the most simple errors without subclass. The other ~15-20% overhead are related to our error message creation. It would actually be possible to roughly cut the latter costs into half by creating a formatPreparation method that pre calculates states and caches these for the later usage. It would however be a non-trivial code part and I believe this is already pretty nice.

@ronag
Copy link
Member

ronag commented Feb 14, 2023

@BridgeAR any chance to further explicitly optimise AbortError as that often occurs also in non exceptional logic (i.e. fast paths).

@BridgeAR
Copy link
Member Author

@ronag the current implementation for AbortErrors is pretty much the bare minimum of a regular error.

The question is what are these used for? If their stack trace is important, we can't improve them further. If not, then there's room for improvement by hiding the frames (which are ~75% of all CPU cycles for a regular error).
To me it seems like the main stack frames that would be relevant for users are mostly coming from the signal.reason. These stack frames would not be touched, only the newly created AbortError ones. I had a short discussion if they are important or not with @benjamingr and I have the feeling the answer is: it depends.
If you therefore have some concrete spots that need some love, please let me know and I am going to check if it's possible to further minimize the overhead.

@jasnell
Copy link
Member

jasnell commented Feb 14, 2023

I'm not going to have time to review the entire PR this week but the changes in internal/errors.js LGTM.

lib/internal/errors.js Outdated Show resolved Hide resolved
lib/internal/errors.js Outdated Show resolved Hide resolved
lib/assert.js Outdated Show resolved Hide resolved
lib/internal/console/constructor.js Show resolved Hide resolved
lib/internal/errors.js Outdated Show resolved Hide resolved
lib/internal/errors.js Show resolved Hide resolved
lib/internal/errors.js Show resolved Hide resolved
lib/internal/errors.js Outdated Show resolved Hide resolved
lib/internal/source_map/prepare_stack_trace.js Outdated Show resolved Hide resolved
lib/internal/streams/readable.js Outdated Show resolved Hide resolved
lib/internal/test_runner/utils.js Show resolved Hide resolved
Copy link
Member

@mcollina mcollina left a comment

Choose a reason for hiding this comment

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

lgtm, this is truly amazing

Copy link
Member

@RafaelGSS RafaelGSS left a comment

Choose a reason for hiding this comment

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

RSLGTM. Awesome work!

Copy link
Member

@VoltrexKeyva VoltrexKeyva left a comment

Choose a reason for hiding this comment

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

RSLGTM, this is absolutely incredible!

Copy link
Member

@JakobJingleheimer JakobJingleheimer left a comment

Choose a reason for hiding this comment

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

🙌

general nit: there are several large numbers in messages for humans, but they're inconsistently delimited. for example 'The value of "port" is out of range. It must be >= 0 && <= 65535. Received 99_999'. Why not delimit all human-friendly numbers?

benchmark/error/node-error.js Outdated Show resolved Hide resolved
benchmark/error/node-error.js Outdated Show resolved Hide resolved
lib/internal/streams/readable.js Outdated Show resolved Hide resolved
@mcollina mcollina added the request-ci Add this label to start a Jenkins CI on a PR. label Feb 17, 2023
@github-actions github-actions bot removed the request-ci Add this label to start a Jenkins CI on a PR. label Feb 17, 2023
@nodejs-github-bot
Copy link
Collaborator

BridgeAR added 8 commits March 2, 2023 01:06
This simplifies the implementation of the OUT_OF_RANGE error by
reusing the current inspect functionality.

Signed-off-by: Ruben Bridgewater <ruben@bridgewater.de>
The former implementation over allocated stack frames during error
creation to substitute for internal frames that should be hidden
to the user. This could have caused a significant performance
overhead in deeply nested code.

As side effect also simplify the setting of stackTraceLimits and
improve the error related benchmarks.

Signed-off-by: Ruben Bridgewater <ruben@bridgewater.de>
This makes sure all error instances use `new` to call the
constructor.

Signed-off-by: Ruben Bridgewater <ruben@bridgewater.de>
The improvement comes from the following changes:

1. Less properties are defined on the error instances, especially
   no Symbol.
2. The check if something is a NodeError or not is not needed
   anymore.
3. The setup of NodeError now makes sure that all heavy lifting is
   done up front instead of doing that during instance creation.

To align with the WPT tests, the NodeErrors must have their
constructor set to their base class. This was not the case for
SystemErrors and is now aligned.

Signed-off-by: Ruben Bridgewater <ruben@bridgewater.de>
This improves the error recreation performance due to limiting the
stack trace that is generated and it makes sure NodeErrors only set
properties inside of the actual error method.

Signed-off-by: Ruben Bridgewater <ruben@bridgewater.de>
This fixes the issue that multiple prepareStackTrace functions
existed that had to duplicate code in multiple spots. Instead, the
diverging part is now replaced during runtime. This reduces the
code overhead and makes sure there is no duplication.

It also fixes the issue that `overrideStackTrace` usage would not
adhere to the `hideStackFrame` calls. Frames are now removed before
passing them to the override method.

A second fix is for the repl: the stack frames passed to a user
defined Error.prepareStackTrace() method are now in the correct
order.
As drive-by it also improves the performance for repl errors
marginally.

Signed-off-by: Ruben Bridgewater <ruben@bridgewater.de>
Signed-off-by: Ruben Bridgewater <ruben@bridgewater.de>
The function calls are more expensive than using the error printf
format style.

Signed-off-by: Ruben Bridgewater <ruben@bridgewater.de>
@BridgeAR BridgeAR force-pushed the improve-error-performance branch from c6ec54f to 40ab9c3 Compare March 2, 2023 00:24
@BridgeAR BridgeAR added the request-ci Add this label to start a Jenkins CI on a PR. label Mar 2, 2023
@github-actions github-actions bot removed the request-ci Add this label to start a Jenkins CI on a PR. label Mar 2, 2023
@nodejs-github-bot
Copy link
Collaborator

try {
Error.stackTraceLimit = limit;
} catch {
stackTraceLimitWritable = -1;
Copy link
Member

Choose a reason for hiding this comment

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

Nit: A comment here on why this would throw would be helpful for future generations ;-)

Copy link
Member Author

Choose a reason for hiding this comment

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

I suggest to address that in a follow-up PR? :)

      // Setting the property in strict mode would cause this to throw an error
      // while using the --frozen-intrinsics flag due to the writable attribute
      // being set to false.

Copy link
Member Author

Choose a reason for hiding this comment

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

I actually wonder, if we want to keep that property writable even when using frozen intrinsics.
@aduh95 @bmeck you might have an opinion about that?

Copy link
Contributor

@aduh95 aduh95 Mar 2, 2023

Choose a reason for hiding this comment

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

To fill the contract of --frozen-intrinsics (opt-out of all possible prototype mutation), we need the property to be non-configurable, it's true that it could still be writable without introducing too much risk I believe. (but the flag name would be kinda lying, not the end of the world of course, but a bit harder to document in simple terms)

But even if we did that, we would probably still want the try catch in case some user code has changed this to non-writable for some reason, unless it's introducing too much maintenance burden. wdyt?

@nodejs-github-bot
Copy link
Collaborator

@nodejs-github-bot
Copy link
Collaborator

@nodejs-github-bot
Copy link
Collaborator

@nodejs-github-bot
Copy link
Collaborator

@anonrig
Copy link
Member

anonrig commented Apr 12, 2023

@BridgeAR It seems like there are some conflicts on the PR. Would you like to rebase it?

@Uzlopak
Copy link
Contributor

Uzlopak commented Sep 14, 2023

I would actually say, that we should split this PR in multiple PRs so that it can be easier reviewed.

@aduh95 aduh95 added the stalled Issues and PRs that are stalled. label May 11, 2024
Copy link
Contributor

This issue/PR was marked as stalled, it will be automatically closed in 30 days. If it should remain open, please leave a comment explaining why it should remain open.

Copy link
Contributor

Closing this because it has stalled. Feel free to reopen if this issue/PR is still relevant, or to ping the collaborator who labelled it stalled if you have any questions.

@github-actions github-actions bot closed this Jun 12, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
lib / src Issues and PRs related to general changes in the lib or src directory. needs-ci PRs that need a full CI run. performance Issues and PRs related to the performance of Node.js. stalled Issues and PRs that are stalled.
Projects
None yet
Development

Successfully merging this pull request may close these issues.