TypeScript: [Performance] Adding just one more method causes check time to jump from 2s to 100+s
TypeScript Version: 3.5.0-dev.20190523
Search Terms: method, generic, interface, slow check time, this
Code
Scroll to the bottom to see the repository link and reproduction steps. For now, I’ll post a rough example of what I am experiencing,
I first had something like this,
type Foo<U, Blah> = /* some complicated type */;
type Bar<V, Blah> = /* some complicated type */;
interface Interface1<T> { /* some complicated type */ }
type Baz<B> = /* some complicated type */;
interface Interface0<T> extends Interface1<T> {
foo<U> (u : U) : (Baz<Foo<U, this>>);
//A bunch of other methods.
}
The above compiles in 2s. Nothing suspicious.
Files: 130
Lines: 38454
Nodes: 171047
Identifiers: 56900
Symbols: 54146
Types: 18772
Memory used: 111991K
I/O read: 0.01s
I/O write: 0.03s
Parse time: 0.50s
Bind time: 0.27s
Check time: 1.35s
Emit time: 0.48s
Total time: 2.59s
Then, I added one more method,
type Foo<U, Blah> = /* some complicated type */;
type Bar<V, Blah> = /* some complicated type */;
interface Interface1<T> { /* some complicated type */ }
type Baz<B> = /* some complicated type */;
interface Interface0<T> extends Interface1<T> {
foo<U> (u : U) : (Baz<Foo<U, this>>);
//The new method
bar<V> (v : V) : (Baz<Bar<V, this>>);
//A bunch of other methods.
}
And then it took 102s!
Files: 130
Lines: 38452
Nodes: 171976
Identifiers: 57291
Symbols: 120444
Types: 61118
Memory used: 174232K
I/O read: 0.01s
I/O write: 0.03s
Parse time: 0.52s
Bind time: 0.34s
Check time: 101.11s
Emit time: 0.53s
Total time: 102.51s
This was very suspicious. I looked at the output .d.ts file but there was nothing out of place.
In fact, emit only took 0.53s
I assumed this new method must be the culprit.
However, I decided to comment out everything but the new method,
type Foo<U, Blah> = /* some complicated type */;
type Bar<V, Blah> = /* some complicated type */;
interface Interface1<T> { /* some complicated type */ }
type Baz<B> = /* some complicated type */;
interface Interface0<T> extends Interface1<T> {
//commented out
//foo<U> (u : U) : (Baz<Foo<U, this>>);
//The new method
bar<V> (v : V) : (Baz<Bar<V, this>>);
/*
//commented out
//A bunch of other methods.
*/
}
This time, it only took 3s.
Files: 130
Lines: 38452
Nodes: 171950
Identifiers: 57280
Symbols: 88571
Types: 38360
Memory used: 126678K
I/O read: 0.01s
I/O write: 0.24s
Parse time: 0.47s
Bind time: 0.26s
Check time: 1.79s
Emit time: 0.71s
Total time: 3.23s
So, the problem wasn’t this new method, it seemed.
I was so confused. This didn’t make sense. Then, I got to thinking, “What if it was the number of methods?” Like, just add one more arbitrary method and see what happens.
Copy+paste foo<>() and rename it to foo2. Change nothing else,
interface Interface0<T> extends Interface1<T> {
//No longer commented
foo<U> (u : U) : (Baz<Foo<U, this>>);
foo2<U> (u : U) : (Baz<Foo<U, this>>);
//Notice bar<V>() is now deleted
//No longer commented
//A bunch of other methods.
}
The above now compiles in 47+s. What?
Files: 130
Lines: 38461
Nodes: 171976
Identifiers: 57291
Symbols: 118614
Types: 57314
Memory used: 139677K
I/O read: 0.01s
I/O write: 0.04s
Parse time: 0.51s
Bind time: 0.29s
Check time: 45.83s
Emit time: 0.56s
Total time: 47.20s
I looked at the generated .d.ts file and I didn’t see any explosion of types. It looked exactly the way I predicted it should and only took up 29 lines.
So, without foo2<>(), it takes 2s. With it, it takes 47s.
And foo2<>() is exactly the same as foo<>()
Copy+paste foo<>() (again) and rename it to foo3. Change nothing else,
interface Interface0<T> extends Interface1<T> {
foo<U> (u : U) : (Baz<Foo<U, this>>);
foo2<U> (u : U) : (Baz<Foo<U, this>>);
foo3<U> (u : U) : (Baz<Foo<U, this>>);
//A bunch of other methods.
}
I started this build at 22:20, 2019 May 27. Well, it’s been checking for 40 minutes and hasn’t finished yet.
Expected behavior:
- Adding copies of
foo<>()without any changes shouldn’t increase check time. - If
bar<>()alone compiles in 3s, andx<>(), y<>(), z<>()compile in 2s, compiling all of these methods should not take 102s.
Actual behavior:
- Check time is crazy long.
- It’s still checking as of this writing.
- My laptop, a ThinkPad P52, is making scary exhaust fan noises.
- My laptop is the hottest it has ever been, I think the CPU might melt.
Github Link:
https://github.com/AnyhowStep/type-mapping/tree/d09851dc437aa25b702a27fa7b6eb5060dd476c5/src
- Clone the repository
npm installnpm run build- Grab coffee
The problem is specifically in src/fluent.ts
You’ll see derive2<>(), derive3<>(). Uncommented, it’ll take forever to check.
https://github.com/AnyhowStep/type-mapping/blob/d09851dc437aa25b702a27fa7b6eb5060dd476c5/src/fluent.ts#L64
You’ll see rename<>(). Uncommented, it’ll take 100s to check.
But if you comment everything else but rename<>(), it’ll take 3s to check.
https://github.com/AnyhowStep/type-mapping/blob/d09851dc437aa25b702a27fa7b6eb5060dd476c5/src/fluent.ts#L86
Related Issues:
I know there are a bunch of performance related issues around here. I’ve even opened a few myself in the past. But I’m not even sure what is related to this.
As a side note, I usually implement my libraries with just a function composition API. Then, when I want to make it more usable, I add a fluent API on top of it.
But it seems like, with TypeScript, whenever I start to build the fluent API wrapper, I get weird problems like reaching the max instantiation depth, or super-long check times.
I guess function composition > fluent?
I feel like this problem might have to do with the fact that it’s a generic interface and the return type of the methods reference the this type.
This reminds me of TREE(3).
- TREE(1) = 1
- TREE(2) = 3
- TREE(3) > googolplex
Except,
- foo(1) = 2
- foo(2) = 47
- foo(3) > TREE(3) (Proof is left as an exercise to the reader)
About this issue
- Original URL
- State: closed
- Created 5 years ago
- Comments: 15 (12 by maintainers)
@AnyhowStep nowadays we issue a
Type instantiation is excessively deep and possibly infinite.error and terminate pretty quickly on the code given here, yeah? So this has been “fixed” by one of our other limiters - at least we acknowledge our own limitations now.In this example the types under consideration are… complex:
What seems to be going on is that every
FluentMapper<DeriveMapper<SrcKeyT, DstKeyT, F>>we encounter as we descend through the comparison expands - namely it becomes aIFluentMapper<T> & T- and since there’s no “base case” there as we keep descending on generic types - we’ll look at aIFluentMapper<IFluentMapper<T> & T> & IFluentMapper<T> & T, then aIFluentMapper<IFluentMapper<IFluentMapper<T> & T> & IFluentMapper<T> & T> & IFluentMapper<IFluentMapper<T> & T> & IFluentMapper<T> & Tand so on until we hit a depth limiter, then crawl back out and continue down another branch doing the same. That depth limit is pretty high, and gets expontentiated in comparison time when the comparison is re-performed multiple times with different type parameters (as each copy ofderivedhas unique type parameters).Fixing this may require something similar to an insight I has while working on https://github.com/microsoft/TypeScript/pull/31633 - namely that while we’re measuring weather
IFoo<T>is assignable toIFoo<U>, asking ifIFoo<Q>is assignable toIFoo<J>should already be assumed to betrue, assumingQis related toJasTis related toU.Okay, even shorter repro, removing
rename<>(),Playground
Comment out both
as anyparts,Playground
Now, it takes forever.