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
base: main
Are you sure you want to change the base?
Conversation
|
Review requested:
|
846a306
to
15854a8
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Amazing!
|
cc @nodejs/performance |
|
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. |
|
@BridgeAR any chance to further explicitly optimise |
|
@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). |
|
I'm not going to have time to review the entire PR this week but the changes in |
There was a problem hiding this 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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
RSLGTM. Awesome work!
There was a problem hiding this 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!
There was a problem hiding this 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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm traveling and won't have time to review fully but please let's all be super careful landing these sort of changes and this one (changing stack creation and errors) in particular. They have a very high breakage potential.
Other than that great work Ruben!
Directly concat the necessary information to a string instead of allocating intermediate arrays and iterating over the array more than once. Signed-off-by: Ruben Bridgewater <ruben@bridgewater.de>
Signed-off-by: Ruben Bridgewater <ruben@bridgewater.de>
The assert module should only be used by users, not by Node.js internally. Signed-off-by: Ruben Bridgewater <ruben@bridgewater.de>
Instead of providing a similar functionality, just reuse the other method. Signed-off-by: Ruben Bridgewater <ruben@bridgewater.de>
The arguments check is moved to the error class setup time instead of error instance creation time. Signed-off-by: Ruben Bridgewater <ruben@bridgewater.de>
This improves the performance by removing the extra stack trace collection step. Instead, just change the maximum stack trace limit to the current limit plus 25. The arbitrary 25 is used as arbitrary upper bound to prevent huge overhead in recursive methods. The limit should normally not be reached and should therefore suffice. Signed-off-by: Ruben Bridgewater <ruben@bridgewater.de>
Currently, already collected user stack frames are removed during stack frame serialization to adhere to the defined maximum stack frame limit. This limit is mainly there to prevent extra cpu overhead while gathering the stack frames. Removing stack frames after already collecting them won't improve the performance and only limit the debuggability for users later. Signed-off-by: Ruben Bridgewater <ruben@bridgewater.de>
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>
c6ec54f
to
40ab9c3
Compare
| try { | ||
| Error.stackTraceLimit = limit; | ||
| } catch { | ||
| stackTraceLimitWritable = -1; |
There was a problem hiding this comment.
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 ;-)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
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?
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