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

perf: avoid excessive linear scanning for large functions #1399

Open
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

JoostK
Copy link

@JoostK JoostK commented May 20, 2024

This commit restores the token indices for a node after they have been refined
to span the node, instead of before the refinement. This avoids the potential for
excessive searches for token indices, which is implemented using a linear scan.

It was attempted to convert the linear scans into binary searches (which an
accompanying comment on findTokenRange indicates shouldn't be needed) and that
also avoids the problem, but using the refined positions as implemented in this
commit is even more effective, to the point where using a binary search is less
efficient in my testing.

This change can achieve a substantial improvement, where an ~2.6MB file used to be
parsed and cloned in ~120s, now reduced to ~1.5s, almost two orders of magnitude
faster.

The perf test that tests the backbone.js file shows that performance improves
from ~60ms to ~53ms, only a slight improvement.


The offending file I used for testing was ElkJS bundled into a single file: elkjs.js.zip. It used to take ~120s on a MacBook Pro M1 Max to clone the parsed tree, which has now been reduced to 1.2s for a 100x improvement.

This commit restores the token indices for a node after they have been refined
to span the node, instead of before the refinement. This avoids the potential for
excessive searches for token indices, which is implemented using a linear scan.

It was attempted to convert the linear scans into binary searches (which an
accompanying comment on `findTokenRange` indicates shouldn't be needed) and that
also avoids the problem, but using the refined positions as implemented in this
commit is even more effective, to the point where using a binary search is less
efficient in my testing.

This change can achieve a substantial improvement, where an ~2.6MB file used to be
parsed and cloned in ~120s, now reduced to ~1.5s, almost two orders of magnitude
faster.

The perf test that tests the `backbone.js` file shows that performance improves
from ~60ms to ~53ms, only a slight improvement.
This commit makes various micro optimizations to improve node cloning performance.

In particular, `Object.create` with property descriptors appears to be slower than
`Object.defineProperty` after the object has been created (measured in Node 18 and 20).

The other bigger factor was the early bailout in `TCp.copy` for non-object values, as
such primitives will never hit any of the remaining logic.

For a 2.6MB large file, this improves parsing and cloning times from ~1.5s to ~1.2s.
The performance test using `backbone.js` sess about 10% improvement.
@JoostK JoostK marked this pull request as ready for review May 20, 2024 18:18
JoostK added a commit to JoostK/cypress that referenced this pull request May 20, 2024
This change is in addition to benjamn/recast#1399. This commit
focuses on Cypress' use of recast, with the following optimizations:

1. Avoid printing the source file again if no change was made.
   Printing an AST using recast does reuse the original text, but identifying
   for which parts of the AST reuse is possible comes with noticeable overhead.
   By tracking changes within the visitor it becomes possible to skip this
   phase entirely if no changes were made.
2. Avoid a scope lookup (`path.scope.declares(node.name)`) for all identifiers in
   the program, doing it only for identifiers that may have to be rewritten.

With these changes, a 2.6MB bundle that does not need rewriting has improved
from 4.4s to 2.3s, provided that the above-mentioned recast PR has been applied.
JoostK added a commit to JoostK/cypress that referenced this pull request May 20, 2024
This change is in addition to benjamn/recast#1399. This commit
focuses on Cypress' use of recast, with the following optimizations:

1. Avoid printing the source file again if no change was made.
   Printing an AST using recast does reuse the original text, but identifying
   for which parts of the AST reuse is possible comes with noticeable overhead.
   By tracking changes within the visitor it becomes possible to skip this
   phase entirely if no changes were made.
2. Avoid a scope lookup (`path.scope.declares(node.name)`) for all identifiers in
   the program, doing it only for identifiers that may have to be rewritten.

With these changes, a 2.6MB bundle that does not need rewriting has improved
from 4.4s to 2.3s, provided that the above-mentioned recast PR has been applied.
JoostK added a commit to JoostK/cypress that referenced this pull request May 20, 2024
This change is in addition to benjamn/recast#1399. This commit
focuses on Cypress' use of recast, with the following optimizations:

1. Avoid printing the source file again if no change was made.
   Printing an AST using recast does reuse the original text, but identifying
   for which parts of the AST reuse is possible comes with noticeable overhead.
   By tracking changes within the visitor it becomes possible to skip this
   phase entirely if no changes were made.
2. Avoid a scope lookup (`path.scope.declares(node.name)`) for all identifiers in
   the program, doing it only for identifiers that may have to be rewritten.

With these changes, a 2.6MB bundle that does not need rewriting has improved
from 4.4s to 2.3s, provided that the above-mentioned recast PR has been applied.
jennifer-shehane added a commit to cypress-io/cypress that referenced this pull request Jun 21, 2024
* perf: improve performance of `experimentalSourceRewriting`

This change is in addition to benjamn/recast#1399. This commit
focuses on Cypress' use of recast, with the following optimizations:

1. Avoid printing the source file again if no change was made.
   Printing an AST using recast does reuse the original text, but identifying
   for which parts of the AST reuse is possible comes with noticeable overhead.
   By tracking changes within the visitor it becomes possible to skip this
   phase entirely if no changes were made.
2. Avoid a scope lookup (`path.scope.declares(node.name)`) for all identifiers in
   the program, doing it only for identifiers that may have to be rewritten.

With these changes, a 2.6MB bundle that does not need rewriting has improved
from 4.4s to 2.3s, provided that the above-mentioned recast PR has been applied.

* chore: move `experimentalSourceRewriting` release note to pending release section

---------

Co-authored-by: Bill Glesias <[email protected]>
Co-authored-by: Jennifer Shehane <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant