Skip to content

Bug: no-misused-promises rule is slow to evaluate #10996

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

Open
4 tasks done
benmccann opened this issue Mar 25, 2025 · 24 comments
Open
4 tasks done

Bug: no-misused-promises rule is slow to evaluate #10996

benmccann opened this issue Mar 25, 2025 · 24 comments
Labels
awaiting response Issues waiting for a reply from the OP or another party package: eslint-plugin Issues related to @typescript-eslint/eslint-plugin performance Issues regarding performance triage Waiting for team members to take a look

Comments

@benmccann
Copy link
Contributor

benmccann commented Mar 25, 2025

Before You File a Bug Report Please Confirm You Have Done The Following...

  • I have tried restarting my IDE and the issue persists.
  • I have updated to the latest version of the packages.
  • I have searched for related issues and found none that matched my issue.
  • I have read the FAQ and my problem is not listed.

Issue Description

Set TIMING=20 in the lint script of this repo: https://github.com/immich-app/immich/tree/main/web

Took over 4 min to run while other rules completed quickly

Image

Reproduction Repository Link

https://github.com/immich-app/immich

Repro Steps

git clone git@github.com:immich-app/immich.git
cd immich/web
npm install
TIMING=20 npm run lint

Versions

This repo is currently on typescript-eslint 8 as indicated below, but I've sent a PR to upgrade it to the latest v9 and it happens there as well

package version
@typescript-eslint/eslint-plugin 8.26.1
@typescript-eslint/parser 8.26.1
@typescript-eslint/scope-manager 8.26.1
@typescript-eslint/typescript-estree 8.26.1
@typescript-eslint/type-utils 8.26.1
@typescript-eslint/utils 8.26.1
TypeScript 5.7.3
ESLint 9.18.0
node 22.11.0
@benmccann benmccann added bug Something isn't working triage Waiting for team members to take a look labels Mar 25, 2025
Copy link

github-actions bot commented Mar 25, 2025

Uh oh! @benmccann, at least one image you shared is missing helpful alt text. Check your issue body to fix the following violations:

  • Images should have meaningful alternative text (alt text) at line 14

Alt text is an invisible description that helps screen readers describe images to blind or low-vision users. If you are using markdown to display images, add your alt text inside the brackets of the markdown image.

Learn more about alt text at Basic writing and formatting syntax: images on GitHub Docs.

🤖 Beep boop! This comment was added automatically by github/accessibility-alt-text-bot.

@Josh-Cena
Copy link
Member

Hi, please see https://typescript-eslint.io/troubleshooting/typed-linting/performance:

However, because TypeScript utilizes internal caching, a project's first type-aware lint rule will almost always seem the slowest.

@Josh-Cena Josh-Cena closed this as not planned Won't fix, can't repro, duplicate, stale Mar 25, 2025
@Josh-Cena Josh-Cena added question Questions! (i.e. not a bug / enhancment / documentation) and removed bug Something isn't working triage Waiting for team members to take a look labels Mar 25, 2025
@43081j
Copy link
Contributor

43081j commented Mar 25, 2025

He's actually aware of that and has run this without that rule enabled and seen a speed up. Suggesting this rule is indeed slow

Reopen it and we can look into it properly

There are some optimisations I have in mind but I'm not at a laptop right now so will write them up once I have chance to run the repro

@Josh-Cena
Copy link
Member

Do you have benchmarks for each individual rule?

@Josh-Cena Josh-Cena reopened this Mar 25, 2025
@benmccann
Copy link
Contributor Author

benmccann commented Mar 25, 2025

Here's what I got when no-misused-promises was disabled showing that it actually did seem to be slowing things down:

Screenshot showing timings when no-misused-promises is disabled

Copy link

Uh oh! @benmccann, at least one image you shared is missing helpful alt text. Check #10996 (comment) to fix the following violations:

  • Images should have meaningful alternative text (alt text) at line 3

Alt text is an invisible description that helps screen readers describe images to blind or low-vision users. If you are using markdown to display images, add your alt text inside the brackets of the markdown image.

Learn more about alt text at Basic writing and formatting syntax: images on GitHub Docs.

🤖 Beep boop! This comment was added automatically by github/accessibility-alt-text-bot.

@Josh-Cena
Copy link
Member

Hmmm interesting. Yeah we should look at this.

@Josh-Cena Josh-Cena added performance Issues regarding performance package: eslint-plugin Issues related to @typescript-eslint/eslint-plugin and removed question Questions! (i.e. not a bug / enhancment / documentation) labels Mar 25, 2025
@bradzacher
Copy link
Member

bradzacher commented Mar 26, 2025

ESLint timing information is pretty useless in terms of determining type-aware rule performance, sadly.

Type information is computed lazily so that means that the first rule to request types for a particular location bears the cost of computing that type and all subsequent rules get if "for free".

Based on your screenshot it looks like you're not using many type-aware lint rules - which means that no-misused-promises looks like an outlier. As an example if you enabled all of the no-unsafe-* rules you would similarly see them take a lot of time.

To be clear: I'm not saying that the rule doesn't have a perf bug in it. I'm just saying that TIMINGS=1 doesn't prove anything.

@bradzacher bradzacher added the triage Waiting for team members to take a look label Mar 26, 2025
@benmccann
Copy link
Contributor Author

@pralkarz reported that it spends almost 3 minutes in checkArguments, and particularly voidFunctionArguments:

@JoshuaKGoldberg
Copy link
Member

I'd like to take a deeper look at how lint compares to check:typescript's tsc but don't know how to get it to build locally. There might be an issue with a lot of anys or error types.

~/repos/immich/web $ npm run check:typescript

> immich-web@1.130.1 check:typescript
> tsc --noEmit

src/hooks.client.ts:1:48 - error TS2307: Cannot find module '@immich/sdk' or its corresponding type declarations.

1 import { isHttpError, type ApiHttpError } from '@immich/sdk';
                                                 ~~~~~~~~~~~~~

Could you post steps we can take to get this repo to a fully built state, please?

Btw, +1 to #10996 (comment) and not posting images in general. Unless an image's alt text describes every relevant piece of information in it, a non-sighted user reading this thread would miss out. "Screenshot showing timings when no-misused-promises is disabled" doesn't say what the rules or timings are. Plus it's easier to copy & paste / generally interact with raw text. If you have a table of text to display please paste just the text or even format it as a Markdown table.

@JoshuaKGoldberg JoshuaKGoldberg added the awaiting response Issues waiting for a reply from the OP or another party label Mar 26, 2025
@43081j
Copy link
Contributor

43081j commented Mar 26, 2025

FYI it seems to be pretty much that getTypeOfSymbolAtLocation isn't the fastest and we call it a lot

i feel like there's some optimisation but not sure what exactly yet

for example, if you reference fn(someArg) a thousand times, we will currently call getTypeOfSymbolAtLocation for every location. surely there's some way we can do a first pass to get fn (once) and then look up those unique symbols?

currently we do this for every possible call expression, which is going to be pretty slow. there must be a way we can be smarter about which ones we look up 🤔

@bradzacher
Copy link
Member

bradzacher commented Mar 26, 2025

for example, if you reference fn(someArg) a thousand times, we will currently call getTypeOfSymbolAtLocation for every location. surely there's some way we can do a first pass to get fn (once) and then look up those unique symbols?

See my previous comment about type information being computed lazily. TS has its own caches for types that are filled with each new call and it serves from the cache when it can.

We have tried adding a cache in front of the APIs but we found there was a negligible improvement in perf (on mobile so can't dig up the PR but look for closed and unmerged PRs by Josh).

If you could collate the perf trace and break it down into P50/70/95/99 then you'd likely see that there's there's a large skew in the runtime per call and most calls would be fast (if the codebase has repeated types, that is).

@43081j
Copy link
Contributor

43081j commented Mar 26, 2025

it isn't to do with the type information being computed lazily unfortunately

getTypeOfSymbolAtLocation is just expensive, every time. calling it for every possible CallExpression is therefore slow

in a CPU profile you can quite easily see that every occurrence of it takes more time than most other calls

@43081j
Copy link
Contributor

43081j commented Mar 26, 2025

a screenshot of a CPU profile showing that the call to getTypeOfSymbolAtLocation took ~700ms

you can see here, every sibling entry around the same width is also a call to getTypeOfSymbolAtLocation

this is during a run on immich with everything except these rules enabled:

    rules: {
      '@typescript-eslint/await-thenable': 'error',
      '@typescript-eslint/no-floating-promises': 'error',
      '@typescript-eslint/no-misused-promises': 'error',
      '@typescript-eslint/require-await': 'error',
    },

we can certainly move on from the usual "its just slow to start up". it isn't that in this case

@JoshuaKGoldberg
Copy link
Member

@43081j caaaaan you please post the CPU profile file itself? And any other files that you are referencing that we don't have ourselves please? 🙂

@43081j
Copy link
Contributor

43081j commented Mar 26, 2025

i won't have time to run it again until maybe later today or tomorrow

but once i do i can try attach one. unless ben or pralkarz beat me to it

@pralkarz
Copy link

I'm not home at the moment to share the file, but after npm installing https://github.com/immich-app/immich/tree/main/web you can generate it with node --cpu-prof node_modules/eslint/bin/eslint.js ., and then investigate it in Speedscope, for example.

@benmccann
Copy link
Contributor Author

tsc takes only about 15s for me on this project. I just posted the steps above to run the lint command on this project, but if you'd like to run tsc then you also need to build the open-api project that it depends on. Here are the full steps for running tsc:

git clone git@github.com:immich-app/immich.git
cd immich
make open-api
cd web
npm install
time npm run check:typescript

@bradzacher
Copy link
Member

You could try adding a global WeakMap cache to this loop to save the "thenable" status of each subType which might add up and save some calls at scale?

https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/src/rules/no-misused-promises.ts#L705

Otherwise I'd hazard a guess that each time we call getTypeOfSymbolAtLocation it's highly likely to be a unique symbol and so you can't cache it. My guess though yeah would be that the repeated calls are cheap but the uniqe calls are expensive.
But would be worth doing some local modifications to see if that is actually true.

@benmccann
Copy link
Contributor Author

I wonder why tsc is able to run so much more quickly. Presumably they'd have to be getting the types too, but they must be doing something differently...

@bradzacher
Copy link
Member

bradzacher commented Mar 26, 2025

We need to do different checks to TS and get more types that it doesn't need to. For example we need to check every type to see if it is a promise by way of looking for a .then method that has a call signature we expect.

Often times TS can short-circuit its checks by way of simple assignability checks like "are the property names a match". It can often also short-circuit with simple left === right checks (which indicate they're the same type).

TS also has access to internal APIs that we don't. For example where we call methods they can check @internal properties directly.

There's also chances we're doing the wrong things and getting more types than we need. It's hard to know for sure.

As one example for no-floating-promises we check the type of every single expression statement to see if it is a promise. TS does not check expression statements at all - so there are many thousands of types that rule computes that TS never will.

@43081j
Copy link
Contributor

43081j commented Mar 26, 2025

Otherwise I'd hazard a guess that each time we call getTypeOfSymbolAtLocation it's highly likely to be a unique symbol and so you can't cache

according to the TS team, it does cache the lookup per symbol at least (inside typescript)

@bradzacher
Copy link
Member

bradzacher commented Mar 26, 2025

I'd still say that you're not wrong and we could probably see wins by doing some caching of all the functions that are at the top-level of the rule module. Many of the utils do work like iterating call signatures to figure out if a type matches some shape. But we don't need to do that every time for all types - there's likely room for caching and short-circuiting to save some time there.

It would be worth someone giving it a whirl by editing your local install in your repo to see if it shifts the needle.

@benmccann
Copy link
Contributor Author

I've created a cpuprofile as requested. It can be found here: https://github.com/benmccann/eslint-cpuprofile

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
awaiting response Issues waiting for a reply from the OP or another party package: eslint-plugin Issues related to @typescript-eslint/eslint-plugin performance Issues regarding performance triage Waiting for team members to take a look
Projects
None yet
Development

No branches or pull requests

6 participants