Skip to content

Massive performance penalty when shouldProvideParserServices is true #243

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

Closed
dsgkirkby opened this issue Feb 11, 2019 · 16 comments
Closed
Labels
package: typescript-estree Issues related to @typescript-eslint/typescript-estree question Questions! (i.e. not a bug / enhancment / documentation)

Comments

@dsgkirkby
Copy link
Contributor

What code were you trying to parse?
I first noticed this against my company's repo (which is private), but I've replicated a similar performance penalty on this repo itself.

What happened?
When run normally, I get a pretty fast lint on my machine (2018 Macbook Pro 15-inch, i7/16GB), about 4.5s:

✔ ~/code/typescript-eslint [master|✔] 
16:34 $ time yarn lint
yarn run v1.13.0
$ eslint . --ext .js,.ts
✨  Done in 3.04s.

real    0m3.229s
user    0m4.476s
sys     0m0.260s

However, if I add "project": "./tsconfig.json" to the parser options, I notice a 10x performance decrease (which is roughly consistent with what we noticed on our own repo).

✔ ~/code/typescript-eslint [master|✚ 1] 
16:39 $ time yarn lint
yarn run v1.13.0
$ eslint . --ext .js,.ts
✨  Done in 34.64s.

real    0m34.807s
user    0m43.377s
sys     0m2.933s

Why does this happen?

I've dug into this a little bit, and it seems to come entirely from the time taken to generate the AST and ts.Program for each file; in parser.ts when shouldProvideParserServices is true (which is only when project is set), getProgramAndAST takes ~300ms/file, compared to ~1.5ms/file when it's false.

Within getProgramAndAST, the source of the slowness is almost entirely from calls to ts.createProgram. I wrapped createProgramAndAST as well as the call to createProgram with console.time/console.timeEnd and got the following (this is a small but representative sample):

createProgram: 264.493ms
getProgramAndAST: 264.883ms
createProgram: 267.681ms
getProgramAndAST: 268.112ms
createProgram: 397.579ms
getProgramAndAST: 397.939ms
createProgram: 252.260ms
getProgramAndAST: 252.598ms

How could this be fixed?

In comparison, tslint (which lints our internal codebase about 8x faster than typescript-eslint with project set) offers an idea for what performance here could look like, only calls createProgram once per lint run (https://github.com/palantir/tslint/blob/master/src/runner.ts#L204), and gives it the names of all the files that will be linted (https://github.com/palantir/tslint/blob/master/src/linter.ts#L97). This isn't possible for typescript-estree as it stands, as eslint only provides a single filename to a call to the parser.

Some possible approaches for a solution:

  • Try to make ts.createProgram faster
  • Change eslint to pass down all filenames to the parser each time, allowing typescript-estree to create one ts.Program and reuse it

I'm happy to undertake one of these approaches (my thinking is that the second is more promising) and file a PR myself, but I wanted to check in with you, the package maintainers, and get feedback on this before investing a lot more time into it.

Versions

package version
@typescript-eslint/typescript-estree master
TypeScript 3.3.1
node 8.12.0
yarn 1.13.0
@dsgkirkby dsgkirkby added package: typescript-estree Issues related to @typescript-eslint/typescript-estree triage Waiting for team members to take a look labels Feb 11, 2019
@nstepien
Copy link
Contributor

I wonder if it would make sense to move rules that depend on types to a typescript plugin (example: https://github.com/Microsoft/typescript-styled-plugin). Though that wouldn't be an eslint plugin anymore, and we might come full circle with tslint (https://github.com/Microsoft/typescript-tslint-plugin).

@bradzacher
Copy link
Member

Related: #134

@Swatinem
Copy link

I also noticed in a one of my projects that running createProgram is super slow, basically because it parses/typechecks all the files. I think it should be possible via the language service to do that in a lazy / incremental way.
However I haven’t figured out how to do that myself, partly because I don’t even know where all the typescript API docs are, since all the docs I have found are quite lacking.

@Djaler
Copy link

Djaler commented Feb 12, 2019

Full scan of my project takes more than 5 minutes with parserOptions.project specified. This is so long

@bradzacher
Copy link
Member

bradzacher commented Feb 12, 2019

cc @typescript-eslint/core-team

Is it possible for us to run the parser in "isolatedModules" mode?
Or would we lose too much information from that?

@platinumazure
Copy link
Contributor

There's been some discussion elsewhere about how plugins could gather some information in advance, before any linting begins, which would allow them to get some information that could then be shared among plugin rules. For example, eslint-plugin-import has a use case where they went to parse all files being linted to find imported files and construct a dependency graph. From what I've seen here, constructing a Program from all files being linted could be a similar use case.

Along those lines, I would recommend creating an RFC in the ESLint RFC repository. It would be a great way to get some opinions from the ESLint core team, as well as allow other stakeholders such as eslint-plugin-import a chance to contribute their ideas and hopefully land on a sensible design that works for all.

I'll keep an eye out on the ESLint side for when that RFC is created. 😄

@bradzacher bradzacher added question Questions! (i.e. not a bug / enhancment / documentation) and removed triage Waiting for team members to take a look labels Feb 15, 2019
@dsgkirkby
Copy link
Contributor Author

That makes sense, and the import plugin case sounds quite similar! I'll try to take a look at opening the RFC sometime over the next week or two.

@NikhilVerma
Copy link

NikhilVerma commented Feb 26, 2019

What I've also noticed is that after the first TS file is scanned the following ones (even if they are JS) slow down when linting. My solution was to lint them separately

"lint": "eslint . --ext .js,.jsx && eslint . --ext .ts,.tsx",

@NilSet
Copy link
Contributor

NilSet commented Mar 8, 2019

I believe eslint-plugin-import cheats and caches information between rules and files in module scope, in the import-map file. We could do the same and cache a compiled program.

https://github.com/benmosher/eslint-plugin-import/blob/master/src/ExportMap.js#L18

@bradzacher
Copy link
Member

Ran some tests against master.

// withProject.eslintrc.js
module.exports = {
  extends: [path.resolve(root, '.eslintrc.json')],
  parserOptions: {
    project: path.resolve(root, 'packages/typescript-estree/tsconfig.json'),
    tsconfigRootDir: path.resolve(root, 'packages/typescript-estree'),
  },
  rules: {
    [`${prefix}/promise-function-async`]: 'error',
  },
}

$ node --inspect-brk node_modules/.bin/eslint -c=./tests/perf/withProject.eslintrc.js --ext .js,.ts ./packages/typescript-estree

> Profiled time = 3670 ms
// withoutProject.eslintrc.js
module.exports = {
  extends: [path.resolve(root, '.eslintrc.json')],
  parserOptions: {},
  rules: {},
}

$ node --inspect-brk node_modules/.bin/eslint -c=./tests/perf/withoutProject.eslintrc.js --ext .js,.ts ./packages/typescript-estree

> Profiled time = 1836 ms

There is a definite slowdown present (1834 ms), however I went digging using the node profiler (--inspect-brk) to double check where the extra time was being spent.

The raw chrome profile data from the runs (you should be able to load the files into the chrome developer tools): typescript-eslint-profiles.zip

We use the typescript compiler API createWatchProgram. This uses the provided tsconfig to build and parse every file in the workspace ahead of time.
typescript-estree also caches the resulting program and reuses it. This means that we only pay the cost of parsing and generating types once per configured tsconfig.json.

Unfortunately, the time cost associated with constructing the program (even once) is going to be proportional to the size of your workspace.
For the typescript-estree codebase, that's 6000 lines of code across 12 files, the cost of constructing the program once is around 1800 ms.


At this point in time, I don't know what we can do about this cost. I don't think there's a way for us to circumvent this one off cost. We more or less need every single file in the project parsed by typescript so that we can guarantee for the rules that all imported variables are properly typed.
It makes sense to do this check all at once to make sure we don't duplicate the work, though an argument could be made for only parsing the files (and their dependency tree) as they're presented by eslint, however I don't think the typescript compiler API provides a mechanism for doing this in such an incremental way.

If anyone has any suggestions for this, I'd love to hear it.


For users that use the CLI, there's nothing at all we can do about the cost - once the lint finishes, node exits, and with it the cache is gone. This means you have to pay it once per cli run.

For watch mode (/ IDE plugins) - I haven't profiled it, but I'd assume that these modes are run more like a persistent server, meaning they should be able to take full advantage of the caching mechanism.

@mohsen1
Copy link
Contributor

mohsen1 commented Mar 9, 2019

For users that use the CLI, there's nothing at all we can do about the cost - once the lint finishes, node exits, and with it the cache is gone. This means you have to pay it once per cli run.

I think TypeScript 3.4 .tsbuildinfo might be helpful here. It's something that TypeScript ESLint should assure it not left unused.

@bradzacher
Copy link
Member

The 3.4 RC is out.

@JamesHenry
Copy link
Member

For anybody following along for the perf piece, please try the latest release - 1.5.0 :)

@Djaler
Copy link

Djaler commented Mar 27, 2019

Still very slow

@bradzacher
Copy link
Member

Hey @Djaler

What's your codebase look like?
How many files are there?
Are you linting a multi tsconfig repo?
What is the speed difference between linting with and without type information rules?
What rules are you using?
Have you got a link to the repo? Or a gist repro case?
What versions of the plugin/parser/TypeScript/node are you using?
Are you running eslint in watch mode? Via your ide? Via CLI on your entire repo?

Your comment "still very slow" contains absolutely nothing useful for us to action or investigate, and only serves as notification spam for all the participants and watchers of this issue.

In future, please give us some information, or at least something more constructive than 3 words.

@jrparish
Copy link

I am also seeing slowness in our repo compared to tslint, even without specifying a project.

tslint with project - 20s
eslint with project - 96s
eslint without project - 57s

How many files are there?
1000ish

Are you linting a multi tsconfig repo?
No

What rules are you using?
recommended + airbnb + prettier

What versions of the plugin/parser/TypeScript/node are you using?
plugin/parser - 1.5.0
typescript - 3.3.4000
node - 10.15.0

Are you running eslint in watch mode? Via your ide? Via CLI on your entire repo?
CLI, non-watch

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
package: typescript-estree Issues related to @typescript-eslint/typescript-estree question Questions! (i.e. not a bug / enhancment / documentation)
Projects
None yet
Development

No branches or pull requests