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

optimize no-cycle rule using strongly connected components #2998

Merged
merged 1 commit into from
Aug 29, 2024

Conversation

soryy708
Copy link
Contributor

@soryy708 soryy708 commented Apr 8, 2024

Lets make the no-cycle rule faster by not running many unnecessary BFSes.

For each dependency graph (aka ExportMap) we can run Tarjan's SCC once (which is a derivative of DFS = O(n))

That saves us a lot of work because we run a linear-complexity algorithm once, as opposed to for each linted file (which turned us O(n^2))

#2937

@soryy708

This comment was marked as resolved.

Copy link

socket-security bot commented Apr 8, 2024

👍 Dependency issues cleared. Learn more about Socket for GitHub ↗︎

This PR previously contained dependency changes with security issues that have been resolved, removed, or ignored.

View full report↗︎

@soryy708
Copy link
Contributor Author

soryy708 commented Apr 8, 2024

Why is socket-security failing this?
The changes in the dependencies it found don't make sense.
The library I added @rtsao/scc has no dependencies at all.
Here's their package.json: https://github.com/rtsao/scc/blob/1120edc92040b0ca748ab62a882a98565c85deed/package.json

@ljharb
Copy link
Member

ljharb commented Apr 8, 2024

Good question. I think it's that this is the first new PR it's been run on since i installed it?

src/rules/no-cycle.js Outdated Show resolved Hide resolved
tests/src/rules/no-cycle.js Outdated Show resolved Hide resolved
Copy link

codecov bot commented Apr 10, 2024

Codecov Report

Attention: Patch coverage is 96.15385% with 2 lines in your changes missing coverage. Please review.

Project coverage is 95.91%. Comparing base (bdff75d) to head (55c2741).

Files Patch % Lines
src/rules/no-cycle.js 80.00% 1 Missing ⚠️
src/scc.js 97.87% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##             main    #2998       +/-   ##
===========================================
+ Coverage   85.33%   95.91%   +10.57%     
===========================================
  Files          78       79        +1     
  Lines        3300     3352       +52     
  Branches     1160     1171       +11     
===========================================
+ Hits         2816     3215      +399     
+ Misses        484      137      -347     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

src/scc.js Outdated Show resolved Hide resolved
src/scc.js Show resolved Hide resolved
tests/src/rules/no-cycle.js Outdated Show resolved Hide resolved
tests/src/rules/no-cycle.js Outdated Show resolved Hide resolved
src/rules/no-cycle.js Outdated Show resolved Hide resolved
Copy link
Member

@ljharb ljharb left a comment

Choose a reason for hiding this comment

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

Looks great! I'll look at failing tests next

src/scc.js Show resolved Hide resolved
tests/src/rules/no-cycle.js Outdated Show resolved Hide resolved
tests/src/rules/no-cycle.js Outdated Show resolved Hide resolved
@pumano
Copy link

pumano commented May 20, 2024

would love to see that optimization!

@jonahallibone
Copy link

Can't wait for this to be merged!

@pumano
Copy link

pumano commented Jun 10, 2024

@ljharb any news on this bro?

@soryy708

This comment was marked as resolved.

@soryy708
Copy link
Contributor Author

There's a pathology with SCC right now:

  • It traverses ignored modules (isExternalModule), which slows it down

@soryy708 soryy708 marked this pull request as ready for review August 23, 2024 19:48
Copy link
Member

@ljharb ljharb left a comment

Choose a reason for hiding this comment

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

I see two uncovered lines - can we add test cases to cover them?

Otherwise, this LGTM assuming all existing tests pass!

@soryy708
Copy link
Contributor Author

About the uncovered lines,

I see them in codecov. The 1st is early return marked "no-self-import territory", so unrelated to my changes.

The other is early return when files don't have the same SCC.
Unfortunately it can never happen in the tests since ExportMap is null for some reason, so both files are in an undefined scc.

src/scc.js Show resolved Hide resolved
@soryy708
Copy link
Contributor Author

After all these changes, I decided to measure performance impact.
I ran lint on a big TypeScript codebase with many circular dependencies, and looked at total command execution time (as reported by yarn).

Without SCC (disableScc: true):

367 seconds (6:07 minutes)

With SCC:

133 seconds (2:13 minutes)

(I made sure it isn't using eslint cache)

So we're talking about a 63% decrease in run time.

@JounQin
Copy link
Collaborator

JounQin commented Aug 27, 2024

Notice that we discovered performance downgrade at un-ts#113 when we ported this "optimization", I don't quite sure whether there were some improvements after we ported.

cc @SukkaW

@soryy708
Copy link
Contributor Author

Notice that we discovered performance downgrade at un-ts#113 when we ported this "optimization", I don't quite sure whether there were some improvements after we ported.

cc @SukkaW

Thanks, I have already seen this. What I did about it from when it was ported to the community fork:

  • Simplified things by ripping out "skip error message path" option. This means less things can go wrong
  • Changed SCC to differentiate between value and type imports
  • Changed BFS to look at SCC
  • Added option to disable SCC via config

In summary, this PR diverged significantly from the port discussed, in a way which should improve performance, invalidating prior benchmarks. A workaround has been built in to fall-back on in case of performance regression, without rolling-back.

@SukkaW
Copy link

SukkaW commented Aug 27, 2024

Notice that we discovered performance downgrade at un-ts#113 when we ported this "optimization", I don't quite sure whether there were some improvements after we ported.
cc @SukkaW

Thanks, I have already seen this. What I did about it from when it was ported to the community fork:

  • Simplified things by ripping out "skip error message path" option. This means less things can go wrong
  • Changed SCC to differentiate between value and type imports
  • Changed BFS to look at SCC
  • Added option to disable SCC via config

In summary, this PR diverged significantly from the port discussed, in a way which should improve performance, invalidating prior benchmarks. A workaround has been built in to fall-back on in case of performance regression, without rolling-back.

It was only after backporting the PR that I realized this was still an early experiment and a PoC.

I'm considering using @newdash/graphlib (which powers cycle-import-check) to reimplement the no-cycle rule. With an existing graph library, we can effortlessly identify any "circle" within the module graph. However, without appending extra information, we lose track of the AST node.

src/rules/no-cycle.js Outdated Show resolved Hide resolved
Copy link
Member

@ljharb ljharb left a comment

Choose a reason for hiding this comment

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

awesome, LGTM once tests pass

@ljharb ljharb merged commit 98a0991 into import-js:main Aug 29, 2024
307 checks passed
@soryy708
Copy link
Contributor Author

soryy708 commented Sep 3, 2024

Should be included in v 2.30.0

renovate bot added a commit to andrei-picus-tink/auto-renovate that referenced this pull request Sep 4, 2024
| datasource | package              | from   | to     |
| ---------- | -------------------- | ------ | ------ |
| npm        | eslint-plugin-import | 2.29.1 | 2.30.0 |


## [v2.30.0](https://github.com/import-js/eslint-plugin-import/blob/HEAD/CHANGELOG.md#2300---2024-09-02)

##### Added

-   \[`dynamic-import-chunkname`]: add `allowEmpty` option to allow empty leading comments (\[[#2942](import-js/eslint-plugin-import#2942)], thanks \[[@JiangWeixian](https://github.com/JiangWeixian)])
-   \[`dynamic-import-chunkname`]: Allow empty chunk name when webpackMode: 'eager' is set; add suggestions to remove name in eager mode (\[[#3004](import-js/eslint-plugin-import#3004)], thanks \[[@amsardesai](https://github.com/amsardesai)])
-   \[`no-unused-modules`]: Add `ignoreUnusedTypeExports` option (\[[#3011](import-js/eslint-plugin-import#3011)], thanks \[[@silverwind](https://github.com/silverwind)])
-   add support for Flat Config (\[[#3018](import-js/eslint-plugin-import#3018)], thanks \[[@michaelfaith](https://github.com/michaelfaith)])

##### Fixed

-   \[`no-extraneous-dependencies`]: allow wrong path (\[[#3012](import-js/eslint-plugin-import#3012)], thanks \[[@chabb](https://github.com/chabb)])
-   \[`no-cycle`]: use scc algorithm to optimize (\[[#2998](import-js/eslint-plugin-import#2998)], thanks \[[@soryy708](https://github.com/soryy708)])
-   \[`no-duplicates`]: Removing duplicates breaks in TypeScript (\[[#3033](import-js/eslint-plugin-import#3033)], thanks \[[@yesl-kim](https://github.com/yesl-kim)])
-   \[`newline-after-import`]: fix considerComments option when require (\[[#2952](import-js/eslint-plugin-import#2952)], thanks \[[@developer-bandi](https://github.com/developer-bandi)])
-   \[`order`]: do not compare first path segment for relative paths (\[[#2682](import-js/eslint-plugin-import#2682)]) (\[[#2885](import-js/eslint-plugin-import#2885)], thanks \[[@mihkeleidast](https://github.com/mihkeleidast)])

##### Changed

-   \[Docs] `no-extraneous-dependencies`: Make glob pattern description more explicit (\[[#2944](import-js/eslint-plugin-import#2944)], thanks \[[@mulztob](https://github.com/mulztob)])
-   \[`no-unused-modules`]: add console message to help debug \[[#2866](import-js/eslint-plugin-import#2866)]
-   \[Refactor] `ExportMap`: make procedures static instead of monkeypatching exportmap (\[[#2982](import-js/eslint-plugin-import#2982)], thanks \[[@soryy708](https://github.com/soryy708)])
-   \[Refactor] `ExportMap`: separate ExportMap instance from its builder logic (\[[#2985](import-js/eslint-plugin-import#2985)], thanks \[[@soryy708](https://github.com/soryy708)])
-   \[Docs] `order`: Add a quick note on how unbound imports and --fix (\[[#2640](import-js/eslint-plugin-import#2640)], thanks \[[@MinervaBot](https://github.com/minervabot)])
-   \[Tests] appveyor -> GHA (run tests on Windows in both pwsh and WSL + Ubuntu) (\[[#2987](import-js/eslint-plugin-import#2987)], thanks \[[@joeyguerra](https://github.com/joeyguerra)])
-   \[actions] migrate OSX tests to GHA (\[[ljharb#37](ljharb/eslint-plugin-import#37)], thanks \[[@aks-](https://github.com/aks-)])
-   \[Refactor] `exportMapBuilder`: avoid hoisting (\[[#2989](import-js/eslint-plugin-import#2989)], thanks \[[@soryy708](https://github.com/soryy708)])
-   \[Refactor] `ExportMap`: extract "builder" logic to separate files (\[[#2991](import-js/eslint-plugin-import#2991)], thanks \[[@soryy708](https://github.com/soryy708)])
-   \[Docs] \[`order`]: update the description of the `pathGroupsExcludedImportTypes` option (\[[#3036](import-js/eslint-plugin-import#3036)], thanks \[[@liby](https://github.com/liby)])
-   \[readme] Clarify how to install the plugin (\[[#2993](import-js/eslint-plugin-import#2993)], thanks \[[@jwbth](https://github.com/jwbth)])
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

Successfully merging this pull request may close these issues.

None yet

7 participants