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: Fast path for NodeEventGenerator #17019

Closed
wants to merge 2 commits into from
Closed

Conversation

nzakas
Copy link
Member

@nzakas nzakas commented Mar 24, 2023

Prerequisites checklist

What is the purpose of this pull request? (put an "X" next to an item)

[ ] Documentation update
[ ] Bug fix (template)
[ ] New rule (template)
[ ] Changes an existing rule (template)
[ ] Add autofix to a rule
[ ] Add a CLI option
[ ] Add something to the core
[x] Other, please explain:

Perf changes

What changes did you make? (Give an overview)

Added a "fast path" for NodeEventGenerator that skips matching and parsing of selectors when they are simple.

Refs #16962

Is there anything you'd like reviewers to focus on?

I'm not seeing any noticeable improvement with this work. Did I do something wrong?

@eslint-github-bot eslint-github-bot bot added the chore This change is not user-facing label Mar 24, 2023
@netlify
Copy link

netlify bot commented Mar 24, 2023

Deploy Preview for docs-eslint canceled.

Name Link
🔨 Latest commit 99ae371
🔍 Latest deploy log https://app.netlify.com/sites/docs-eslint/deploys/641e112285ae840008ebac44

@nzakas nzakas mentioned this pull request Mar 24, 2023
Comment on lines +192 to +202
function tryParseSelector(rawSelector, knownTypes) {

const rawSelectorWithoutExit = rawSelector.replace(/:exit$/u, "");

// no need to parse known types
if (knownTypes.has(rawSelectorWithoutExit)) {
return {
type: "identifier",
value: rawSelectorWithoutExit
};
}
Copy link
Member

Choose a reason for hiding this comment

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

With the ESLint configuration for our codebase (enabled rules have 195 different selectors, out of which 113 are identifiers), this change improves performance by a couple of ms. I'm getting different results for the same run, but it's like ~16ms instead of ~18ms in total spent for running this function. Since we are caching parsed selectors, this improvement doesn't scale with the number of files being linted, so it looks like it won't bring much.

@nzakas
Copy link
Member Author

nzakas commented Mar 28, 2023

I'm going to tweet this out to see if anyone is willing to try this branch and post their results. It's possible we aren't seeing the change but others might.

@github-actions
Copy link

github-actions bot commented Apr 7, 2023

Hi everyone, it looks like we lost track of this pull request. Please review and see what the next steps are. This pull request will auto-close in 7 days without an update.

@github-actions github-actions bot added the Stale label Apr 7, 2023
@amareshsm
Copy link
Member

Don't close. Still this solution needs to be experimented.

@amareshsm amareshsm removed the Stale label Apr 7, 2023
@github-actions
Copy link

Hi everyone, it looks like we lost track of this pull request. Please review and see what the next steps are. This pull request will auto-close in 7 days without an update.

@github-actions github-actions bot added the Stale label Apr 18, 2023
@nzakas nzakas marked this pull request as ready for review April 19, 2023 20:01
@nzakas nzakas requested a review from a team as a code owner April 19, 2023 20:01
@nzakas
Copy link
Member Author

nzakas commented Apr 19, 2023

Since there is a small speedup in our own tests, maybe it's worth merging this? I wonder if it might help others in the wild more than us.

@github-actions github-actions bot removed the Stale label Apr 19, 2023
@fasttime
Copy link
Member

Thanks for working on this @nzakas! If you can show the method(s) used to measure execution time before and after the change, I'd like to try this on my machine and see the results I'm getting.

@nzakas
Copy link
Member Author

nzakas commented Apr 20, 2023

I just ran a combination of npm run perf and time eslint lib/ tests/.

@fasttime
Copy link
Member

Using VSCode profiler to time function execution in node bin/eslint.js lib/ tests/ suggests a small improvement at least in Node.js 16 and 18 (I'm getting inconclusive results in Node.js 20). For reference, here are the results of my last three runs in Node.js 16.

Before

Run 1 Run 2 Run 3 Average
(root) 36,828.56ms 36,032.43ms 34,383.37ms 35,748.12ms
NodeEventGenerator 148.99ms 112.88ms 129.83ms 130.57ms
applySelector 25,226.03ms 24,344.69ms 22,915.58ms 24,162.10ms

After

Run 1 Run 2 Run 3 Average
(root) 34,949.05ms 35,382.04ms 36,360.29ms 35,563.79ms
NodeEventGenerator 123.56ms 125.00ms 126.11ms 124.89ms
applySelector 23,787.20ms 24,016.11ms 24,909.85ms 24,237.72ms

Note that the NodeEventGenerator constructor also covers tryParseSelector().

The differences are really minimal though, and there is a large margin for errors.

@@ -260,9 +274,10 @@ class NodeEventGenerator {
this.exitSelectorsByNodeType = new Map();
this.anyTypeEnterSelectors = [];
this.anyTypeExitSelectors = [];
this.knownTypes = new Set(Object.keys(esqueryOptions.visitorKeys));
Copy link
Member

@fasttime fasttime Apr 22, 2023

Choose a reason for hiding this comment

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

I was wondering if it's really necessary to keep track of known node types when the goal is to detect simple selectors. Maybe we could just test the selector string (in tryParseSelector and applySelector) for special characters like >, +, [, ], :, ,, *, !, ~ white spaces, etc. A valid selector is a simple selector if it does not contain any special characters.

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'm not 100% sure, but I think doing such a check could be slower than keeping track of known Node types.

Copy link
Member

Choose a reason for hiding this comment

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

I was also not sure, so I decided to test it (spoiler: it didn't help)

I defined a regex

const SIMPLE_SELECTOR_REGEX = /^\w+$/;

It's not perfect, but it matches all standard ESTree node names plus the code path analysis events (which are intended to work like identifiers as it seems). Then I replaced this.knownTypes.has() with SIMPLE_SELECTOR_REGEX.test() here and here and cleared the remaining references to knownTypes.

I run the VSCode profiler on npm run lint to measure execution time and compared the results with knownTypes and with my regex. No difference though. The times are almost the same either way. I guess we can keep the knownTypes and move on.

@github-actions
Copy link

github-actions bot commented May 2, 2023

Hi everyone, it looks like we lost track of this pull request. Please review and see what the next steps are. This pull request will auto-close in 7 days without an update.

@github-actions github-actions bot added the Stale label May 2, 2023
@nzakas
Copy link
Member Author

nzakas commented May 3, 2023

@fasttime yeah, running on our own repo doesn't show any significant improvement, but there have been previous changes that didn't show a big improvement on our repo and yet did show a big improvement in someone else's. The way ESLint is configured makes a big difference so the only way we can really know is to try the change out on a few other repos that perhaps have more complicated configurations.

@github-actions github-actions bot removed the Stale label May 3, 2023
this.emitter.emit(selector.rawSelector, node);
return;
}

if (esquery.matches(node, selector.parsedSelector, this.currentAncestry, this.esqueryOptions)) {
Copy link
Member

Choose a reason for hiding this comment

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

Can we merge this condition with the next one in order to avoid repeating this.emitter.emit(selector.rawSelector, node);?

Copy link
Member Author

Choose a reason for hiding this comment

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

Hmm, I think you meant to post this on line 312?

I prefer the way I have it written because it's easier to see that there are two distinct conditions being handled. I don't mind repeating that line.

Copy link
Member

Choose a reason for hiding this comment

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

Hmm, I think you meant to post this on line 312?

Yes, I should have clicked on that line rather than the whole changed block, sorry.

I prefer the way I have it written because it's easier to see that there are two distinct conditions being handled. I don't mind repeating that line.

Fair enough. That would also not affect performance, only style.

@fasttime
Copy link
Member

Thanks! The logic LGTM, but somehow I don't see a significant improvement here, not even in applySelector() which is called very often and clearly makes a good candidate for optimization. I'll leave this to @mdjermanovic to verify.

Comment on lines 312 to 315
if (this.knownTypes.has(selector.rawSelector)) {
this.emitter.emit(selector.rawSelector, node);
return;
}
Copy link
Member

Choose a reason for hiding this comment

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

We could add a property isIdentifier: parsedSelector.type === "identifier" to selectors and just check if (selector.isIdentifier) here.

Copy link
Member Author

Choose a reason for hiding this comment

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

Sorry, I’m missing some context here. Why would we do that?

Copy link
Member

Choose a reason for hiding this comment

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

For performance reasons. Whether the selector is an identifier is already known at the parse time.

Copy link
Member Author

Choose a reason for hiding this comment

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

Oh wait, are you saying that instead of checking knownTypes we could just check to see if the selector is an identifier?

Copy link
Member

Choose a reason for hiding this comment

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

Yes, at this point if it's an identifier then it certainly matches.

Copy link
Member Author

Choose a reason for hiding this comment

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

Got it. Updated.

@github-actions
Copy link

Hi everyone, it looks like we lost track of this pull request. Please review and see what the next steps are. This pull request will auto-close in 7 days without an update.

@github-actions github-actions bot added the Stale label May 27, 2023
@Rec0iL99
Copy link
Member

Not stale. @nzakas there's a review for you to look into. Thanks.

@Rec0iL99 Rec0iL99 removed the Stale label May 28, 2023
Copy link
Member

@mdjermanovic mdjermanovic left a comment

Choose a reason for hiding this comment

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

The logic LGTM, but I'm consistently getting a worse performance after this change when running npm run test:performance and time node bin/eslint ., so I'm not sure whether we should merge this. I checked the blog post again but couldn't find performance tests for this recommendation.

@nzakas
Copy link
Member Author

nzakas commented Jun 19, 2023

Thanks for the update. Let's just close this then, it's not important.

@nzakas nzakas closed this Jun 19, 2023
@nzakas nzakas deleted the perf-node-event-generator branch July 4, 2023 18:44
@eslint-github-bot eslint-github-bot bot locked and limited conversation to collaborators Dec 17, 2023
@eslint-github-bot eslint-github-bot bot added the archived due to age This issue has been archived; please open a new issue for any further discussion label Dec 17, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
archived due to age This issue has been archived; please open a new issue for any further discussion chore This change is not user-facing
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants