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

[Performance] Very long Typescript compile time #1135

Closed
moufmouf opened this issue Nov 13, 2024 · 6 comments
Closed

[Performance] Very long Typescript compile time #1135

moufmouf opened this issue Nov 13, 2024 · 6 comments

Comments

@moufmouf
Copy link
Contributor

Hey!

When migrating from Typescript 5.0 to Typescript 5.1, I noticed a huge performance impact that happens specifically while compiling the files generated by ts-proto.

(the file used to take 6 seconds and suddenly, type checking is taking 110 seconds, which is a complete no-go for us).

I started by opening an issue on the Typescript repository. They closed it (because Typescript 5.1 is apparently doing the right thing and it's normal that it is taking more time).

See: microsoft/TypeScript#60353

More specifically, the Typescript developers gave a look at the generated file and gave a few advices on how to speed up things (namely: replacing multiple "if" with "switch", which would both speed up Typescript compilation and runtime execution).

See: microsoft/TypeScript#60353 (comment)

I thought it might be good to open this issue to show you this feedback.

Also, since the files are auto-generated by ts-proto, doing any type checking on those files is actually useless.

Is there any reason why you don't disable type checking at all by adding a ts-nocheck?

//@ts-nocheck

@stephenh I've seen you remove it here #28 (comment) but I don't understand the rationale.

Note: Some other libraries (namely Relay, the GraphQL library) are putting @ts-nocheck by default on generated files and are considering adding a compiler flag to remove it (mostly for internally checking generated files are correctly typed: facebook/relay#4753)

Final note: thanks a lot for ts-proto, it is so much better than the default protobuf JS generation. You guys rock! ❤️

@stephenh
Copy link
Owner

Hi @moufmouf ! Thanks for linking to the typescript issue, and also relay adding the @ts-nocheck...

I had removed @ts-nocheck back in the day, just because ts-proto has so many options 😅 that users can enable/combine in myriad of ways, I think its a good idea (by default, for most users) to have tsc go ahead and type-check the output.

That said, I'm not against changes to our output that would make tsc go faster...

I.e. reading Ryan's comment on if (message.message?.$case === "batchMessage") "should be using switch", I'm very open to that change--even more so if you'd like to submit a PR! :-)

These sort of "use switch instead of if" seems like a great/do-it-for-everyone win.

Maybe we start there, and see how the tsc compile times look with a switch?

After that, happy to look at adding ts-nocheck in certain places/if the user opts in to it with a nocheck=true type option...

stephenh pushed a commit that referenced this issue Nov 26, 2024
In oneof scenarios, this commit proposes generating "else if" statements
where applicable.

The original idea was an attempt to speed up Typescript compilation by
reducing the amount of paths Typescript can analyze. Spoiler alert =>
nothing changes from that point of view.

The compilation of my [proto
file](https://github.com/workadventure/workadventure/blob/develop/messages/protos/messages.proto)
takes between 41 and 44 seconds with and without "else if" statements.

Still, the "else if" conditions should slightly speed up the runtime
execution, by avoiding unnecessary checks to the Javascript runtime.

This PR is directly linked to
#1135
Note: in #1135, @stephenh you
recommend trying to use a switch statement (instead of "else if"). This
would require a really big refactoring and I'm not familiar enough with
the code base to attempt that.
The "else if" implementation was a bit of a low hanging fruit.

I still think the "//ts-nocheck" annotation would be really useful and
is the way to go, since it would simply take "0 seconds" to typecheck
the generated file (you can't beat 0 in terms of performance 😆
)
@stephenh
Copy link
Owner

@moufmouf did you see this approach of putting //@ts-nocheck into the *.proto file as a comment, and that (At the time the comment was written anyway) would get output into the *.ts file:

#28 (comment)

Does that still work?

@moufmouf
Copy link
Contributor Author

moufmouf commented Nov 28, 2024

Hey @stephenh ,

Yes, I've seen the comment and already tested it. It doesn't exactly work in the latest version.

If I put in the proto file:

//@ts-nocheck

it becomes in the Typescript file:

/** @ts-nocheck */

This syntax is not recognized by Typescript.

So no luck this way.

Currently, compiling my file without "ts-nocheck" takes 40 seconds, vs 10 seconds with "ts-nocheck" (10 seconds is still a lot but a 4x speedup). For my own use case, I can write a small script to prepend ts-nocheck just after the proto generation, but I still think the whole community would benefit from this change by default, since there is no reason the code you generate would suddenly contain an error.

@moufmouf
Copy link
Contributor Author

I gave this another shot and managed to implement the "switch case" solution instead of the "else if" solution.

It solves the performance issue!

Opening a PR right now!

@stephenh
Copy link
Owner

stephenh commented Nov 28, 2024

Ah gotcha.

Fwiw I opened up the example *.proto file you linked to, and I'm really surprised by how small it is. I.e. I assumed you were feeding in some enterprise ~100k-line long Frankenstein of a file, but really it seems pretty reasonable:

https://github.com/workadventure/workadventure/blob/develop/messages/protos/messages.proto

Which makes the "40 seconds for tsc" even more bizarre.

You do use oneofs a lot, but 🤔 still...

(...I had this reply typed up, and then just saw your latest comment! Amazing!)

stephenh pushed a commit that referenced this issue Nov 28, 2024
…ypescript performance (#1142)

This is a follow-up (and solution) to #1135 and #1141

I generated a trace from the build process and using
[typescript-analyze-trace](https://github.com/microsoft/typescript-analyze-trace)
to analyze the hotspots.

It turns out that time is spent almost exclusively in this kind of code:

```
if (object.choice?.$case === "aNumber" && object.choice?.value !== undefined && object.choice?.value !== null) {
  message.choice = { $case: "aNumber", value: object.choice.value };
} else if (
  object.choice?.$case === "aString" && object.choice?.value !== undefined && object.choice?.value !== null
) {
  message.choice = { $case: "aString", value: object.choice.value };
} else if (
//...
```

The previous PR I opened adding "else if" does nothing regarding
Typescript perf.

With "else if", my test file trace looks like this:

```
npx analyze-trace traceDir
Hot Spots
├─ Check file /home/dan/projects/ts-proto/integration/large/messages.ts (31108ms)
│  ├─ Check deferred node from (line 6267, char 3) to (line 6539, char 4) (13123ms)
│  ├─ Check deferred node from (line 15342, char 3) to (line 15591, char 4) (7092ms)
│  ├─ Check deferred node from (line 10849, char 3) to (line 11059, char 4) (4460ms)
│  ├─ Check deferred node from (line 17320, char 3) to (line 17496, char 4) (2146ms)
│  ├─ Check deferred node from (line 3720, char 3) to (line 3832, char 4) (610ms)
│  └─ Check deferred node from (line 8791, char 3) to (line 8915, char 4) (516ms)
└─ Check file /home/dan/projects/ts-proto/node_modules/typescript/lib/lib.dom.d.ts (1109ms)
```

Now, with this commit, I'm replace `else if` with a `switch` statement.

The analysis of the build looks like this:

```
Hot Spots
├─ Check file /home/dan/projects/ts-proto/integration/large/messages.ts (3139ms)
│  └─ Check deferred node from (line 6278, char 3) to (line 6573, char 4) (525ms)
└─ Check file /home/dan/projects/ts-proto/node_modules/typescript/lib/lib.dom.d.ts (617ms)
```

The biggest `switch` statement takes 525ms to analyze (down from
13seconds with `else if`)

Implementation-wise, the hardest part is knowing when to close the
switch statement. I'm doing a test at the beginning of each loop and at
the very end of the function.
stephenh pushed a commit that referenced this issue Nov 28, 2024
## [2.4.2](v2.4.1...v2.4.2) (2024-11-28)

### Performance Improvements

* Replacing "else if" with a "switch case" statement to improve Typescript performance ([#1142](#1142)) ([de1a616](de1a616)), closes [#1135](#1135) [#1141](#1141)
@stephenh
Copy link
Owner

Should be fixed in 2.4.2; thanks @moufmouf !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants