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

Typescript performance issue with large queries in a single file #89

Closed
3 tasks done
deathemperor opened this issue Feb 28, 2024 · 12 comments
Closed
3 tasks done
Assignees
Labels
help wanted ⛏️ Extra attention is needed

Comments

@deathemperor
Copy link
Contributor

deathemperor commented Feb 28, 2024

Describe the bug

It takes 10+ seconds to show types (any type, not just types from graphql queries) and cause eslint the about the same amount to run fixes (I assume it's because eslint typescript waits for the type server to result). This means our DX at the moment basically reverts to Javascript world (we're using it on our production code base).

In the video, I tried to make a change on line 13 and then hover on data to see the type. once shown, data has any which is caused by Type instantiation is excessively deep and possibly infinite.

Screen.Recording.2024-02-25.at.5.57.04.PM.mov

This issue is more about @0no-co/graphqlsp than gql.tada I suppose?

Reproduction

https://github.com/deathemperor/gql.tada.sample

gql.tada version

gql.tada 1.2.1
@0no-co/graphqlsp 1.4.1

Validations

  • I can confirm that this is a bug report, and not a feature request, RFC, question, or discussion, for which GitHub Discussions should be used
  • Read the docs.
  • Follow our Code of Conduct
@JoviDeCroock
Copy link
Member

This issue is appropriately in the gql.tada repository, the LSP facilitates diagnostics/... nothing types-related (apart from the optional boostrapping). I am assuming that using @_unmask on a lot of these fragments might be attributing to having deeper types 😅 did you try without it just to see whether it makes a difference?

@deathemperor
Copy link
Contributor Author

deathemperor commented Feb 28, 2024

@JoviDeCroock I tried that, errors the same.

While trying that I see different behavior worth mentioning:

  1. If I use direct field on root query instead of fragment, error vanishes but returns data is still any, a bit better than the whole variable CLAIM_DETAIL_QUERY is any

https://github.com/deathemperor/gql.tada.sample/blob/main/src/queries.ts#L1266

image

  1. simple query returns types accordingly as intended

image

@kitten
Copy link
Member

kitten commented Feb 28, 2024

Hm, so there's a couple of issues:

  • FRAGMENTS, as a definition, is somehow broken. When it's removed from the queries that refuse to infer types then things start working and these queries are inferred normally
  • trackFieldUsage: false helps a lot. It should really disable itself in files like these and not rendering that many diagnostics helps with baseline performance

Past these two issues it comes down to the simple stuff: the file contains a lot of queries.
This really isn't a usage pattern we recommend or endorse. Assembling huge queries centrally doesn't scale, is hard to impossible to optimise for and goes against fragment colocation principles: https://gql-tada.0no.co/guides/fragment-colocation/

It's basically irrelevant whether fragment masking is turned on or off. This is likely just a bad experience overall as all queries in one file are all checked at once.

We can likely try to find bottlenecks here and optimise, but it's hard to imagine this being a supported case 🤔 since nothing is split up and separated properly.

Currently, my tracing modifications to tsserver aren't good enough though to be granular enough to catch the bottlenecks

I'll have to put in further work to try to narrow down which type inferences are the most expensive ones for us

@kitten kitten self-assigned this Mar 1, 2024
@kitten kitten changed the title Typescript performance issue with large schema generated by Hasua Typescript performance issue with large schema generated by Hasura Mar 2, 2024
@kitten kitten changed the title Typescript performance issue with large schema generated by Hasura Typescript performance issue with large queries in a single file Mar 2, 2024
@kitten
Copy link
Member

kitten commented Mar 2, 2024

Renaming here, since I have a suspicion this is a general issue with queries of this size, especially if they're in the same file.

Where I'm currently at is that some type inferences seem to take longer than I expect (trace showing some work suddenly being spent pushing minimum time in instantiateType to 5-8ms, which adds up quickly).

The other problem that's pretty visible is that infer Result seems to take some extra time. This may simply be misannotated in my traces, but it's worth looking into, since only parseDocument and getDocumentType should take up the bulk of time spent across all instantiateType calls.

Marking as “help wanted”

We'll basically need more examples and reproductions that exhibit this behaviour, potentially with smaller schemas to narrow the problem down.

@kitten
Copy link
Member

kitten commented Mar 5, 2024

We found a small piece of the puzzle and addressed this in: #107
This doesn't yet address all performance cliffs/issues, but it significantly helps already.

@deathemperor
Copy link
Contributor Author

I have to remedy this performance issue using codegen, most API like ResultOf/VariablesOf/graphql.scalar still works and the same graphql() usage is kind of the same so it's a good solution while waiting for this to improve.

@kitten
Copy link
Member

kitten commented Mar 6, 2024

Another piece of the puzzle found and addressed in: #111 (Releasing as v1.3.1)

We refactored code in the parser that could cause bottlenecks but found that these code paths were not actually changing performance at all. But this basically narrowed the issues down to string literal performance.
This meant that the only way to get rid of the biggest bottleneck was to eliminate as many string-reallocations in the TS Type Checker as possible. So, #111 is basically a change that splits out a tokenization stage from the parser, which is optimised to be tail recursive as well (This also led me to believe that maybe some other types which look tail recursive may not fully be optimised yet; anyway...).
This reduces the amount of time TS spends in this phase and seems to help performance quite a bit.

This doesn't seem to be a perfect solution just yet and there's some more things to do. But I didn't want to wait with releasing this, since it already helps quite a bit.

@kitten
Copy link
Member

kitten commented Mar 11, 2024

I'd like to put a bit of a time-cap on this issue and close it for now. The performance over the last few versions should have improved, and — at least for now — I don't see many paths that could lead to improved type inference performance.

Some situations are still worsened by TypeScript eagerly evaluating types it doesn't need to; either on type hints, which seems to re-evaluate several levels deep, or on making changes/editing, which causes TypeScript to partially disregard its type cache as well, it seems.

For now, the remaining situations that I've seen that cause issues often have repeating patterns of:

  • non-co-located fragments causing a lot of fragments in the “execution path” to be re-evaluated
  • no fragments or many documents in general, in a single file, causing a huge amount of evaluation time in that single file
  • non-hoisted fragments/documents or multiple union-documents with complex arguments, fields, and overloads

However, over the last few versions we've improved performance substantially, and I think we've reached a point where I'd like to focus on improving the experience overall first.
Namely, so far, we've completed:

We'd of course re-open this issue, or replace it, once we have more information, more input from TypeScript team members, or more ideas for a path forward to further improving this.

@kitten kitten closed this as completed Mar 11, 2024
@deathemperor
Copy link
Contributor Author

Thanks @kitten , I value your efforts on the matter. I admit that our set up isn't the best one as I'm still on learning path. Having said that, I really want to adopt gql.tada for our projects as we move toward that path after working with codegen for almost 3 years.

I frequently retry the performance and typings with every releases but it's still a blocking issue for us so it's a stay with codegen for now. Looking forward for more updates on the subject!

@kitten
Copy link
Member

kitten commented Mar 21, 2024

This is a little unrelated to query-parsing performance in general, but seems to cause a general uplift in performance across the board.

We just released gql.tada@1.4.0 together with @0no-co/graphqlsp@1.7.0.
This implements a new .d.ts introspection output format that skips a lot of the processing that would previously fall onto the type checker to do, which is now instead done ahead of time.

This reduces memory usage and decreases start-up type checking performance, but due to decreased pressure on the type checker, also seems to help with general updates after the initial startup/checking process.

The new output format can also be generated with the CLI ($ gql.tada generate-output)

(Related PRs: #147, #150)

@kitten kitten added this to the Are We Fast Yet? milestone Mar 22, 2024
@Maximaximum
Copy link

Using "@0no-co/graphqlsp": "^1.12.1", "gql.tada": "^1.6.3", still getting the error
Type instantiation is excessively deep and possibly infinite. when using many fragments in a query, namely for union types.

When using direct graphql type declarations (without fragments), the issue goes away.

@kitten
Copy link
Member

kitten commented May 2, 2024

@Maximaximum: There is a length limit to each document, and if you mean you're using fragments in the exact document you're defining, you may have reached the token limit of what can be parsed in the TypeScript checker efficiently.
There's more information in the linked issues.

In short, there's an assumption to how large a single document can be, and that's now basically enforced through TypeScript's limits.

If you have anything more concrete and do believe this is an issue and this isn't related to the length of your individual documents, I'm happy to take a look at a reproduction ❤️

Side-note: Locking this issue, since new replies won't be related to the milestone created, linked issue, or the original issue here

@0no-co 0no-co locked as resolved and limited conversation to collaborators May 2, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
help wanted ⛏️ Extra attention is needed
Projects
None yet
Development

No branches or pull requests

4 participants