Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Performance Opportunities#6366

bradzacher announced inRFCs
Discussion options

I just wanted to have a publicly visible, centralised list of these somewhere that I can easily reference and add to without having to file an in-depth issue straight away.

Parsing

  • Don't construct a single-filets.Program for non-type-aware parses (feat: remove partial type-information program #6066 - will be released in v6)
    • This step is a leftover from the oldno-unused-vars-experimental experiment. But it's wasted effort because a single-filets.Program is literally useless. We can save time by just building ats.SourceFile directly.
  • CacheparserOptions.project glob resolution for a period of time (feat(typescript-estree): cache project glob resolution #6367).
    • right now we resolve the globs individually for each and every file. This can be really slow - ESP if there are** globs or a lot of matches!
      If we can cache this computation for a period of time, we can save a bunch of time in the type-aware parse.
      For the persistent case we should be able to use a relatively short-lived cache.
      For the single-run case we should be able to use an infinite cache.
  • Investigate building a types API that allows us to do caching (Make a "shortcut" API around TypeScript for node types #6244)
    • My hypothesis is that the TS checker APIs don't do as much caching as we'd like. I think we can more aggressively cache things and save times on repeated requests for types across rules.
    • This may be a uselessly small improvement because (a) the caching TS does is enough, (b) there may little overlap between rules' type requests that caching doesn't help and just uses more memory instead.
  • Investigate using scope analysis to fulfill some type info request (Perf research: use scope analysis to circumvent type information for some cases #6217)
    • This would be dependent on us having our own typechecker api (previous point).
    • If we can use scope analysis to return annotated variables, then we wouldn't need to calculate the TS types at all in some cases, which would save a lot of time!
  • Support project references (Support for Project References #2094)
    • If we can resolve project references then we can make config easier by automating some config.
    • If we can use.ts files instead of.d.ts files then we can save a lot of time and memory.
  • Leveragets.DocumentRegistry to share resources betweents.Programs
  • Change parser conversion logic from a switch/case to a jump table (Repo: investigate switching core TS->ESTree conversion logic from a "switch/case" to a "jump table" for performance improvements #6149)
    • TS saw significant wins from such a change - we probably will too.
  • Investigate lazily computing node location (feat(typescript-estree): lazily compute loc #6542 (comment))
    • TS currently lazily computes the source code location for nodes. This means that when we convert the AST we're hit with the cost of TS doing a binary search to convert the string range to the line/col numbers.
    • ESLint doesn't use location for many reasons
    • If we can convert the location computation to a lazy one, then maybe we can save a bunch of time

CLI Runs ("one-and-done" runs)

IDE Runs ("persistent" runs)

  • Investigate direct integration with IDE extensions likevscode-eslint. Examples of possible optimisations:
    • Add environment variables so we know that we can attach disk watchers
      • This would allow us to watch files/folders instead of relying on our (really slow) invalidation logic to check for edge-cases.
      • We could also do-away with the various file existence checks andfs.stats.
      • We could actually just rely on TS's in-built disk watching logic instead for most cases.
    • We could corral files into separate threads based on the project they belong to which would improve performance which would save us iterating over all projects in the config to check existence.
  • Usets.LanguageService to manage the program instead of managing a builder/watch program (feat(typescript-estree): add experimental mode for type-aware linting that uses a language service instead of a builder #6172)
    • Thets.LanguageService is designed to be as efficient as possible and share as much memory as possible so that IDEs are quick and low memory. We might be able to leverage this data-structure ourselves for the same result in persistent runs.

Rules

  • naming-convention is a really slow lint rule. We need to profile it to see where the bottlenecks are.
    • This might be a side-effect of the super generic way I wrote it way back when?
    • Maybe due to the fact all selectors fire all the time regardless of if there's a validator for them?
  • audit type-aware rules to ensure they're not accessing type information before it's necessary.

eslint-plugin-import

There's a lot of things we don't recommend using from this plugin, but perhaps we can improve the performance so people don't have to manually opt-out of things to avoid perf pit-falls.

  • Provide an API oncontext.parserServices to resolve an import to a module based on the file's type information.
    • For a lot of users this is done viaeslint-import-resolver-typescript, or for more basic usecases the built-innode resolver.
      These resolvers rely on disk operations which can slow things down.
      If we've got type info - then we've already got all of the required info - so we can potentially help them save time!
  • Provide an API oncontext.parserServices to resolve the exports for a file if there is type information.
    • Someeslint-plugin-import rules do out-of-band parsing of modules to determine their exports. If we've got type information then we don't need to parse - we already have that info.
  • Provide an API oncontext.parserServices to quickly parse a file if there is type information.
    • Right now the plugin will emulate ESLint's parse call in order to attempt to do an out-of-band parse of a file.
      If we've got type info then we could just directly fetch thets.SourceFile and convert it without going through any of our other pre-parse logic, potentially saving a chunk of time.

eslint-plugin-prettier

This is another case where we generally recommend against using this, however again we might be able to improve the status-quo.

  • Improve the prettier API so thatinstead of passing the raw source code text into prettier which triggers an additional parse using the@typescript-eslint/typescript-estree version included in the package, it can just reuse the existing AST that ESLint gives if the parser versions align.

TypeScript

One major path we're yet to leverage is building within TypeScript itself. Currently we're using the generic APIs that TS has for general purpose usecases, however it's there's an opportunity to work cloesly with the TS team to create a bespoke API for our usecase that more closely matches our constraints and requirements. By building into the TS codebase directly we could leverage many of the internal APIs and data structures that we cannot access externally.

ESLint

ESLint itself is unfortunately not well designed for our stateful parsing. There's opportunity to work closely with the ESLint team to improve the design so that the core logic can better support our stateful nature, which would in turn unlock other potential performance improvements internally for us.

There's two paths for us to take with this collaboration:

These paths are not mutually exclusive - we can and should improve the current state whilst also looking to the future as well.

You must be logged in to vote

Replies: 2 comments

Comment options

bradzacher
Jan 23, 2023
Maintainer Author

It's worth mentioning that we can and should leverage CPU profiling more in our investigations.
It's relatively easy to generate one:

$node--cpu-prof./node_modules/.bin/eslint.

or you can isolate to a specific lint rule:

$node--cpu-prof./node_modules/.bin/eslint--no-eslintrc--parser="@typescript-eslint/parser"--plugin"@typescript-eslint"--rule"@typescript-eslint/no-unused-vars: [error]"--ext=".ts,.tsx".

or with a slightly more complicated config in a new config file like:

$touch.temp.eslintrc.js#editfile$node--cpu-prof./node_modules/.bin/eslint--no-eslintrc-c.temp.eslintrcjs.

This will generate a.cpuprofile file in the repo root which you can then load intohttps://www.speedscope.app/ to view the profile in a nice flame graph visualisation.

I've found that switching the graph to "left heavy" mode is best for holistically analysing the performance.
Normal mode = show calls and perf in the exact timeline it occurred
Left heavy = aggregate all calls to show the total time spent in each function during the trace

You must be logged in to vote
0 replies
Comment options

bradzacher
Jul 29, 2024
Maintainer Author

Took a CPU profile off of the latest v8 commit (56ef573):

node --cpu-prof --max-old-space-size=10000 ./node_modules/.bin/eslint .

CPU.20240728.151034.53658.0.001.cpuprofile.zip

screenshot of the above profile in speedscope

At a glance we can quickly see:

  • ~25% of lint time is spent doing the type-aware parsing
  • ~70% of the lint time is spent running rules
  • The remaining 5% is spent on misc node infra stuff

Of that parse time - most of it is spent within TS's functions -- ~20% (~2.9s) of it is spent in ourastConverter function and the rest is within TS. For the moment let's assume that's all a "fixed cost" we can't reduce for now and focus on the rules.

Here are some disorganised notes from poking through the profile:


It's easy to see that we spend a whole lot of time in TS's APIs across rules. There's probably some opportunities here to make our rules use TS's APIs less (eg by answering more questions with the AST ahead of time).
It can be really hard to "optimise" type-aware lint rules because types are lazily calculated - so whilst one rule may look super expensive - it may just be the first one to run and thus the first one to bear the cost of the lazy computation. That doesn't mean it's not worth optimising!! Instead it just means that you might not get the wins you expect for a codebase as you're just moving work around rather than eliminating it.


In our codebase the biggest rule is our internal plugin formatting rule - which is understandable considering it uses prettier to check each and every test case in the codebase to ensure it's formatting. Let's ignore that for now as well.


The second biggest block of time is spent incheckVariableDeclaration fromno-misused-promises -- 9.4% of the lint run spent checking the type of variable declarations!

Looking at the code the function is specifically looking for cases likeconst x: () => void = () => Promise to find promise leakages -- simple enough. However looking at the implementation it's always getting the type of the variable declaration if the variable has an initialiser. OOF - the implementation quite naive!
We can do better here by exiting early if we can guess that the case doesn't occur.

First if the variable doesn't have an annotation - then the type of the variable will be the type of the initialiser so the case is impossible. Simply exiting early ifnode.id.typeAnnotation == null handles that case.

Second there's a number of types we can detect syntactically and we can guarantee are never function types. For exampleconst x: string = value can never report in the rule! So we can check the annotation type exit early if it looks syntactically safe.

#9656 is the result of adding these checks, and this is the resulting profile:
CPU.20240729.093048.73851.0.001.cpuprofile.zip
screenshot of the above profile in speedscope

Sadly we can see the impact of what I mentioned above - even though we've managed to reduce the time spent incheckVariableDeclaration from 5.28s to 42ms - it hasn't reduced the lint runtime by 5s -- it's not changed it at all. By removing the type calculations from here we haven't reduced the total amount of types required by the lint run -- i.e. another rule uses those types and is now "on the hook" for the lazy computation.

That being said it's still a good thing to do! If we can do this everywhere then we can slowly move the needle and reduce the cost of the lint run.

You must be logged in to vote
0 replies
Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment
Category
RFCs
Labels
performanceIssues regarding performance
1 participant
@bradzacher

[8]ページ先頭

©2009-2025 Movatter.jp