Skip to content

Conversation

Andarist
Copy link
Contributor

@Andarist Andarist commented Mar 30, 2025

@ssalbdivad found out that a chain of somewhat trivial operations can easily end up with dreaded "Type instantiation is excessively deep and possibly infinite".

Two different reproductions were created:

  1. standalone signature receiving its previous output as input in a chain: TS playground
  2. a dotted chain where a method returns a new instance of its own enclosing type alias: TS playground

We were able to bisect both to two different past PRs - one of which is quite old:

  1. the first one bisects to this PR and we can see it performing better in TS 5.4 here
  2. the other one bisects to this PR and we can see it performing better in TS 4.1 here

⚠️ that said - the "chain setup" avoids the error better but the property type lookup can still lead to the error. So, in some sense, the error just shifted place. this has been solved

I dug into this and what I found out is, those types refer to the same "base" types - they are chained from them and instantiateType repeats the work on them a lot. At least part of the problem is that getObjectTypeInstantiation calls instantiateTypes(type.aliasTypeArguments, mapper) to create part of its cache key. So it can't even check if the result is cached before it instantiates those. This problem compounds heavily in longer chains like this.

So I toyed with possible solutions and I realized that caching results on mappers could help here... to some extent at least. I don't think it's the best solution but I also don't yet understand this problem to its core to propose anything better.

It looks like this has very positive impact on instantiation counts across the test suite. It surely trades some memory for it though. It would be interesting to see perf results for this and the extended test suite run (even if only to get new data points for further investigation).

EDIT:// @ssalbdivad has tested this in some trpc-based repositories and the check time got halved for them. The current version is also able to typecheck the 50-item deep "chains" as it can be seen in the added long* tests. Those previously were only instantiable to a certain depth (smth between 15-25, depending on the test case) and the last items in the chain were being instantiated really slow. Now those longer ones are handled pretty fast.

@github-project-automation github-project-automation bot moved this to Not started in PR Backlog Mar 30, 2025
@typescript-bot typescript-bot added the For Uncommitted Bug PR for untriaged, rejected, closed or missing bug label Mar 30, 2025
@typescript-bot
Copy link
Collaborator

This PR doesn't have any linked issues. Please open an issue that references this PR. From there we can discuss and prioritise.

@jakebailey
Copy link
Member

@typescript-bot test it

@typescript-bot
Copy link
Collaborator

typescript-bot commented Mar 30, 2025

Starting jobs; this comment will be updated as builds start and complete.

Command Status Results
test top400 ✅ Started
user test this ✅ Started
run dt ✅ Started
perf test this faster ✅ Started
@jakebailey
Copy link
Member

@typescript-bot test it

@typescript-bot
Copy link
Collaborator

typescript-bot commented Mar 31, 2025

Starting jobs; this comment will be updated as builds start and complete.

Command Status Results
test top400 ✅ Started ✅ Results
user test this ✅ Started 👀 Results
run dt ✅ Started ✅ Results
perf test this faster ✅ Started 👀 Results
@typescript-bot
Copy link
Collaborator

Hey @jakebailey, the results of running the DT tests are ready.

Everything looks the same!

You can check the log here.

@typescript-bot
Copy link
Collaborator

@jakebailey Here are the results of running the user tests with tsc comparing main and refs/pull/61505/merge:

Something interesting changed - please have a look.

Details

effect

packages/effect/benchmark/tsconfig.json

tsconfig.json

tsconfig.build.json

tsconfig.base.json

packages/typeclass/dtslint/tsconfig.json

packages/platform/dtslint/tsconfig.json

packages/effect/dtslint/tsconfig.json

@typescript-bot
Copy link
Collaborator

@jakebailey
The results of the perf run you requested are in!

Here they are:

tsc

Comparison Report - baseline..pr
Metric baseline pr Delta Best Worst p-value
Compiler-Unions - node (v18.15.0, x64)
Errors 34 34 ~ ~ ~ p=1.000 n=6
Symbols 62,390 62,390 ~ ~ ~ p=1.000 n=6
Types 50,395 50,395 ~ ~ ~ p=1.000 n=6
Memory used 194,709k (± 1.01%) 196,843k (± 0.98%) ~ 194,935k 198,710k p=0.173 n=6
Parse Time 1.31s (± 0.31%) 1.31s (± 0.57%) ~ 1.30s 1.32s p=1.000 n=6
Bind Time 0.73s 0.73s ~ ~ ~ p=1.000 n=6
Check Time 9.73s (± 0.49%) 9.77s (± 0.35%) ~ 9.71s 9.81s p=0.228 n=6
Emit Time 2.73s (± 0.54%) 2.73s (± 0.65%) ~ 2.71s 2.75s p=0.935 n=6
Total Time 14.51s (± 0.37%) 14.54s (± 0.16%) ~ 14.50s 14.56s p=0.253 n=6
angular-1 - node (v18.15.0, x64)
Errors 56 56 ~ ~ ~ p=1.000 n=6
Symbols 948,670 948,670 ~ ~ ~ p=1.000 n=6
Types 410,947 410,947 ~ ~ ~ p=1.000 n=6
Memory used 1,224,296k (± 0.00%) 1,248,572k (± 0.00%) +24,276k (+ 1.98%) 1,248,514k 1,248,618k p=0.005 n=6
Parse Time 6.63s (± 1.21%) 6.63s (± 0.67%) ~ 6.56s 6.70s p=1.000 n=6
Bind Time 1.88s (± 0.67%) 1.88s (± 0.73%) ~ 1.87s 1.90s p=0.557 n=6
Check Time 31.93s (± 0.37%) 32.31s (± 0.31%) +0.39s (+ 1.21%) 32.18s 32.47s p=0.005 n=6
Emit Time 15.23s (± 0.22%) 15.26s (± 0.24%) ~ 15.21s 15.32s p=0.147 n=6
Total Time 55.67s (± 0.25%) 56.09s (± 0.21%) +0.42s (+ 0.75%) 55.98s 56.31s p=0.005 n=6
mui-docs - node (v18.15.0, x64)
Errors 0 0 ~ ~ ~ p=1.000 n=6
Symbols 2,461,267 2,461,267 ~ ~ ~ p=1.000 n=6
Types 893,340 893,340 ~ ~ ~ p=1.000 n=6
Memory used 2,310,804k (± 0.00%) 2,342,249k (± 0.00%) +31,445k (+ 1.36%) 2,342,222k 2,342,302k p=0.005 n=6
Parse Time 9.04s (± 0.22%) 9.04s (± 0.17%) ~ 9.02s 9.06s p=1.000 n=6
Bind Time 2.27s (± 0.36%) 2.26s (± 0.53%) ~ 2.25s 2.28s p=0.652 n=6
Check Time 75.67s (± 0.38%) 75.95s (± 0.42%) ~ 75.43s 76.42s p=0.173 n=6
Emit Time 0.29s (± 1.92%) 0.29s (± 1.80%) ~ 0.28s 0.29s p=0.640 n=6
Total Time 87.26s (± 0.34%) 87.55s (± 0.35%) ~ 87.03s 87.99s p=0.173 n=6
self-build-src - node (v18.15.0, x64)
Errors 0 0 ~ ~ ~ p=1.000 n=6
Symbols 1,226,713 1,226,807 +94 (+ 0.01%) ~ ~ p=0.001 n=6
Types 266,991 267,000 +9 (+ 0.00%) ~ ~ p=0.001 n=6
Memory used 2,357,616k (± 0.03%) 2,376,351k (± 0.02%) +18,735k (+ 0.79%) 2,375,702k 2,377,087k p=0.005 n=6
Parse Time 5.20s (± 1.17%) 5.17s (± 0.50%) ~ 5.14s 5.21s p=0.297 n=6
Bind Time 1.79s (± 1.91%) 1.78s (± 0.68%) ~ 1.77s 1.80s p=0.744 n=6
Check Time 35.40s (± 0.56%) 35.58s (± 0.33%) ~ 35.41s 35.75s p=0.066 n=6
Emit Time 2.99s (± 2.22%) 2.99s (± 1.20%) ~ 2.96s 3.06s p=0.630 n=6
Total Time 45.40s (± 0.72%) 45.53s (± 0.17%) ~ 45.45s 45.62s p=0.066 n=6
self-build-src-public-api - node (v18.15.0, x64)
Errors 0 0 ~ ~ ~ p=1.000 n=6
Symbols 1,226,713 1,226,807 +94 (+ 0.01%) ~ ~ p=0.001 n=6
Types 266,991 267,000 +9 (+ 0.00%) ~ ~ p=0.001 n=6
Memory used 2,911,961k (±12.87%) 3,067,098k (± 9.90%) 🔻+155,137k (+ 5.33%) 2,446,535k 3,191,944k p=0.031 n=6
Parse Time 6.94s (± 1.45%) 6.94s (± 1.80%) ~ 6.70s 7.04s p=1.000 n=6
Bind Time 2.14s (± 2.28%) 2.12s (± 1.06%) ~ 2.08s 2.14s p=0.810 n=6
Check Time 42.65s (± 0.57%) 42.85s (± 0.37%) ~ 42.57s 43.01s p=0.128 n=6
Emit Time 3.45s (± 2.05%) 3.53s (± 0.82%) +0.07s (+ 2.17%) 3.49s 3.58s p=0.044 n=6
Total Time 55.20s (± 0.59%) 55.45s (± 0.48%) ~ 54.92s 55.69s p=0.128 n=6
self-compiler - node (v18.15.0, x64)
Errors 0 0 ~ ~ ~ p=1.000 n=6
Symbols 262,674 262,691 +17 (+ 0.01%) ~ ~ p=0.001 n=6
Types 106,849 106,858 +9 (+ 0.01%) ~ ~ p=0.001 n=6
Memory used 440,486k (± 0.01%) 449,610k (± 0.02%) +9,124k (+ 2.07%) 449,509k 449,723k p=0.005 n=6
Parse Time 3.54s (± 0.42%) 3.51s (± 0.91%) ~ 3.48s 3.57s p=0.052 n=6
Bind Time 1.31s (± 0.39%) 1.32s (± 0.80%) ~ 1.30s 1.33s p=0.794 n=6
Check Time 19.05s (± 0.31%) 19.04s (± 0.36%) ~ 18.93s 19.13s p=0.810 n=6
Emit Time 1.52s (± 0.65%) 1.53s (± 0.79%) ~ 1.51s 1.54s p=0.246 n=6
Total Time 25.43s (± 0.27%) 25.39s (± 0.32%) ~ 25.29s 25.53s p=0.423 n=6
ts-pre-modules - node (v18.15.0, x64)
Errors 71 71 ~ ~ ~ p=1.000 n=6
Symbols 225,981 225,981 ~ ~ ~ p=1.000 n=6
Types 94,356 94,356 ~ ~ ~ p=1.000 n=6
Memory used 371,214k (± 0.01%) 379,734k (± 0.01%) +8,519k (+ 2.29%) 379,686k 379,811k p=0.005 n=6
Parse Time 2.89s (± 0.60%) 2.91s (± 0.96%) ~ 2.86s 2.93s p=0.192 n=6
Bind Time 1.61s (± 1.69%) 1.60s (± 1.46%) ~ 1.57s 1.63s p=0.333 n=6
Check Time 16.51s (± 0.33%) 16.54s (± 0.26%) ~ 16.49s 16.61s p=0.574 n=6
Emit Time 0.00s 0.00s ~ ~ ~ p=1.000 n=6
Total Time 21.02s (± 0.26%) 21.04s (± 0.25%) ~ 20.99s 21.12s p=0.628 n=6
vscode - node (v18.15.0, x64)
Errors 3 3 ~ ~ ~ p=1.000 n=6
Symbols 3,336,059 3,336,059 ~ ~ ~ p=1.000 n=6
Types 1,130,507 1,130,507 ~ ~ ~ p=1.000 n=6
Memory used 3,394,122k (± 0.01%) 3,466,746k (± 0.01%) +72,624k (+ 2.14%) 3,466,227k 3,466,964k p=0.005 n=6
Parse Time 14.70s (± 0.34%) 14.60s (± 0.53%) -0.10s (- 0.71%) 14.51s 14.69s p=0.030 n=6
Bind Time 4.67s (± 0.62%) 4.69s (± 0.61%) ~ 4.66s 4.74s p=0.514 n=6
Check Time 93.33s (± 2.44%) 93.32s (± 3.35%) ~ 91.13s 98.75s p=0.936 n=6
Emit Time 29.88s (± 2.66%) 30.10s (± 2.92%) ~ 29.44s 31.36s p=0.575 n=6
Total Time 142.58s (± 2.06%) 142.71s (± 2.81%) ~ 139.84s 149.43s p=1.000 n=6
webpack - node (v18.15.0, x64)
Errors 2 2 ~ ~ ~ p=1.000 n=6
Symbols 310,441 310,441 ~ ~ ~ p=1.000 n=6
Types 136,030 136,030 ~ ~ ~ p=1.000 n=6
Memory used 465,164k (± 0.02%) 475,453k (± 0.01%) +10,290k (+ 2.21%) 475,362k 475,517k p=0.005 n=6
Parse Time 5.15s (± 0.56%) 5.16s (± 0.94%) ~ 5.13s 5.26s p=0.418 n=6
Bind Time 2.26s (± 0.99%) 2.26s (± 1.23%) ~ 2.24s 2.31s p=0.934 n=6
Check Time 25.48s (± 0.42%) 25.67s (± 0.18%) +0.19s (+ 0.73%) 25.62s 25.74s p=0.005 n=6
Emit Time 0.00s 0.00s ~ ~ ~ p=1.000 n=6
Total Time 32.89s (± 0.34%) 33.09s (± 0.27%) +0.21s (+ 0.63%) 33.00s 33.25s p=0.013 n=6
xstate-main - node (v18.15.0, x64)
Errors 5 5 ~ ~ ~ p=1.000 n=6
Symbols 567,041 567,041 ~ ~ ~ p=1.000 n=6
Types 189,889 189,889 ~ ~ ~ p=1.000 n=6
Memory used 497,780k (± 0.02%) 519,637k (± 0.02%) 🔻+21,857k (+ 4.39%) 519,555k 519,800k p=0.005 n=6
Parse Time 3.36s (± 0.81%) 3.37s (± 0.62%) ~ 3.34s 3.40s p=0.570 n=6
Bind Time 1.21s (± 1.10%) 1.20s (± 1.14%) ~ 1.19s 1.23s p=0.323 n=6
Check Time 19.99s (± 0.10%) 19.81s (± 0.38%) -0.18s (- 0.91%) 19.67s 19.88s p=0.004 n=6
Emit Time 0.00s 0.00s ~ ~ ~ p=1.000 n=6
Total Time 24.56s (± 0.12%) 24.38s (± 0.42%) -0.18s (- 0.75%) 24.21s 24.51s p=0.006 n=6
System info unknown
Hosts
  • node (v18.15.0, x64)
Scenarios
  • Compiler-Unions - node (v18.15.0, x64)
  • angular-1 - node (v18.15.0, x64)
  • mui-docs - node (v18.15.0, x64)
  • self-build-src - node (v18.15.0, x64)
  • self-build-src-public-api - node (v18.15.0, x64)
  • self-compiler - node (v18.15.0, x64)
  • ts-pre-modules - node (v18.15.0, x64)
  • vscode - node (v18.15.0, x64)
  • webpack - node (v18.15.0, x64)
  • xstate-main - node (v18.15.0, x64)
Benchmark Name Iterations
Current pr 6
Baseline baseline 6

Developer Information:

Download Benchmarks

@jakebailey
Copy link
Member

@typescript-bot pack this

@typescript-bot
Copy link
Collaborator

typescript-bot commented Mar 31, 2025

Starting jobs; this comment will be updated as builds start and complete.

Command Status Results
pack this ✅ Started ❌ Results
@typescript-bot
Copy link
Collaborator

Hey @jakebailey, something went wrong when looking for the build artifact. (You can check the log here).

@typescript-bot
Copy link
Collaborator

@jakebailey Here are the results of running the top 400 repos with tsc comparing main and refs/pull/61505/merge:

Everything looks good!

@ssalbdivad
Copy link

This is a set of instantiation benchmarks with results from 5.8.2 for related scenarios.

These can be run against the installed TS version by executing this with node or similar, or could just be used as a reference when constructing benchmarks for TS directly:

import { bench } from "@ark/attest" type merge<base, props> = Omit<base, keyof props & keyof base> & props declare const merge: <l, r>(l: l, r: r) => merge<l, r> bench("functional(10)", () => { const a = merge({ a: 1 }, { b: 2 }) const b = merge(a, { c: 3 }) const c = merge(b, { d: 4 }) const d = merge(c, { e: 5 }) const e = merge(d, { f: 6 }) const f = merge(e, { g: 7 }) const g = merge(f, { h: 8 }) const h = merge(g, { i: 9 }) const i = merge(h, { j: 10 }) }).types([59386, "instantiations"]) bench("functional(11)", () => { const a = merge({ a: 1 }, { b: 2 }) const b = merge(a, { c: 3 }) const c = merge(b, { d: 4 }) const d = merge(c, { e: 5 }) const e = merge(d, { f: 6 }) const f = merge(e, { g: 7 }) const g = merge(f, { h: 8 }) const h = merge(g, { i: 9 }) const i = merge(h, { j: 10 }) const j = merge(i, { k: 11 }) }).types([177537, "instantiations"]) bench("functional(12)", () => { const a = merge({ a: 1 }, { b: 2 }) const b = merge(a, { c: 3 }) const c = merge(b, { d: 4 }) const d = merge(c, { e: 5 }) const e = merge(d, { f: 6 }) const f = merge(e, { g: 7 }) const g = merge(f, { h: 8 }) const h = merge(g, { i: 9 }) const i = merge(h, { j: 10 }) const j = merge(i, { k: 11 }) const k = merge(j, { l: 12 }) // should be linear relative to functional(10) and functional(11) }).types([531887, "instantiations"]) bench("functional(12) with spread", () => { const a = merge({ a: 1 }, { b: 2 }) const b = merge(a, { c: 3 }) const c = merge(b, { d: 4 }) const d = merge(c, { e: 5 }) const e = merge(d, { f: 6 }) const f = merge(e, { g: 7 }) const g = merge(f, { h: 8 }) const h = merge(g, { i: 9 }) const i = merge(h, { j: 10 }) const j = merge(i, { k: 11 }) const k = merge(j, { l: 12 }) return {	...i,	...j,	...k } // should be similar to functional(12) }).types([797849, "instantiations"]) type Type<t> = { merge: <r>(r: r) => Type<merge<t, r>> unwrapped: t } declare const a: Type<{ a: 1 }> bench("chained(10)", () => { const b = a.merge({ b: 2 }) const c = b.merge({ c: 3 }) const d = c.merge({ d: 4 }) const e = d.merge({ e: 5 }) const f = e.merge({ f: 6 }) const g = f.merge({ g: 7 }) const h = g.merge({ h: 8 }) const i = h.merge({ i: 9 }) const j = i.merge({ j: 10 }) }).types([178394, "instantiations"]) bench("chained(11)", () => { const b = a.merge({ b: 2 }) const c = b.merge({ c: 3 }) const d = c.merge({ d: 4 }) const e = d.merge({ e: 5 }) const f = e.merge({ f: 6 }) const g = f.merge({ g: 7 }) const h = g.merge({ h: 8 }) const i = h.merge({ i: 9 }) const j = i.merge({ j: 10 }) const k = j.merge({ k: 11 }) }).types([532898, "instantiations"]) bench("chained(12)", () => { const b = a.merge({ b: 2 }) const c = b.merge({ c: 3 }) const d = c.merge({ d: 4 }) const e = d.merge({ e: 5 }) const f = e.merge({ f: 6 }) const g = f.merge({ g: 7 }) const h = g.merge({ h: 8 }) const i = h.merge({ i: 9 }) const j = i.merge({ j: 10 }) const k = j.merge({ k: 11 }) const l = k.merge({ l: 12 }) // should be linear relative to chained(10) and chained(11) }).types([1596004, "instantiations"]) bench("chained(12) with spread", () => { const b = a.merge({ b: 2 }) const c = b.merge({ c: 3 }) const d = c.merge({ d: 4 }) const e = d.merge({ e: 5 }) const f = e.merge({ f: 6 }) const g = f.merge({ g: 7 }) const h = g.merge({ h: 8 }) const i = h.merge({ i: 9 }) const j = i.merge({ j: 10 }) const k = j.merge({ k: 11 }) const l = k.merge({ l: 12 }) return {	...i.unwrapped,	...j.unwrapped,	...k.unwrapped } // should be similar to chained(12) }).types([1684778, "instantiations"])

I would expect the instantiation scaling to not be purely linear for this sort of operation as the number of props is increasing, but it should be relatively close. None of these scenarios should result in the exponential scaling we see currently.

I'm also including the original end-to-end type benchmarks that identified this issue here. Once the minimal repo scaling has been addressed, we should sanity check that the scaling here is also fixed:

import { bench } from '@ark/attest'; import { initTRPC } from '@trpc/server'; import { type } from 'arktype'; import { z } from 'zod'; const t = initTRPC.create(); // avoid pollution from one-time library setup bench.baseline(() => { const router = t.router({ baseline: t.procedure .input( z.object({ baseline: z.string(), }), ) .query(({ input }) => `hello ${input.baseline}`), arkBaseline: t.procedure .input( type({ baseline: 'string', }), ) .query(({ input }) => `hello ${input.baseline}`), }); }); const base = z.object({ a: z.string(), b: z.string(), c: z.string(), }); bench('non-sequential Zod type', async () => { const nonSequentialRouter = t.router({ query1: t.procedure.input(base).query(({ input }) => `hello ${input.a}`), mutation1: t.procedure .input(base) .mutation(({ input }) => `hello ${input.a}`), }); // this is relatively cheap }).types([1659, 'instantiations']); // even though sequential is totally equivalent to nonSequential, its // Zod representation is not reduced and still includes intermediate operations const sequential = base .partial() .merge(base) .pick({ a: true, b: true, c: true }) .omit({}) .merge(base); const base2 = z.object({ d: z.string(), e: z.string(), f: z.string(), }); bench('isolated sequential zod', () => { const sequential = base2 .partial() .merge(base2) .pick({ d: true, e: true, f: true }) .omit({}) .merge(base2); // this is expensive }).types([11420, 'instantiations']); bench('sequential Zod type', async () => { const sequentialRouter = t.router({ query1: t.procedure .input(sequential) .query(({ input }) => `hello ${input.a}`), mutation1: t.procedure .input(sequential) .mutation(({ input }) => `hello ${input.a}`), }); // but it's in combination with trpc that these sequentially evaluated // Zod types get out of control. instead of incurring a 1-time evaluation // cost, it seems it can't be cached and the extra inference cost // is incurred multiple times (even worse with deepPartial) }).types([49173, 'instantiations']); const arkBase = type({ a: 'string', b: 'string', c: 'string', }); bench('non-sequential arktype', async () => { const sequentialRouter = t.router({ query1: t.procedure.input(arkBase).query(({ input }) => `hello ${input.a}`), mutation1: t.procedure .input(arkBase) .mutation(({ input }) => `hello ${input.a}`), }); // realtively cheap }).types([2961, 'instantiations']); const arkBase2 = type({ d: 'string', e: 'string', f: 'string', }); bench('isolated sequential arktype', () => { arkBase2.partial().merge(arkBase2).pick('d', 'e', 'f').omit().merge(arkBase2); // these kind of operations are much cheaper in ArkType than Zod }).types([2336, 'instantiations']); const arkSequential = arkBase .partial() .merge(arkBase) .pick('a', 'b', 'c') .omit() .merge(arkBase); bench('sequential arktype', async () => { const sequentialRouter = t.router({ query1: t.procedure .input(arkSequential) .query(({ input }) => `hello ${input.a}`), mutation1: t.procedure .input(arkSequential) .mutation(({ input }) => `hello ${input.a}`), }); // even though hovering arkSequential is identical to hovering arkBase, // TS still seems to do a lot of repeated work inferring it somehow (though less than Zod) }).types([17906, 'instantiations']);
@Andarist Andarist force-pushed the cache-mapper-instaniations branch from 688f5d5 to b0f5c70 Compare April 1, 2025 08:00
@Andarist
Copy link
Contributor Author

Andarist commented Apr 1, 2025

@ssalbdivad tested this in a trpc-based repo.

TS 5.8:

Types: 139629 Instantiations: 12133763 Memory used: 798162K Check time: 13.04s 

This branch (commit b0f5c70):

Types: 139629 Instantiations: 817105 Memory used: 819990K Check time: 6.00s 
@jakebailey
Copy link
Member

@typescript-bot test it
@typescript-bot pack this

@typescript-bot
Copy link
Collaborator

typescript-bot commented Apr 1, 2025

Starting jobs; this comment will be updated as builds start and complete.

Command Status Results
pack this ✅ Started ✅ Results
test top400 ✅ Started ✅ Results
user test this ✅ Started ✅ Results
run dt ✅ Started ✅ Results
perf test this faster ✅ Started 👀 Results
@ahejlsberg
Copy link
Member

I took some of the numbers reported earlier to mean that the additional caching only decreases instantiations by half, but I see how it is much more dramatic in the micro benchmarks. So, I agree there's some merit to the additional caching, but ideally I'd like us to target it at the expensive instantiations instead of just caching all instantiations (which adds unnecessary overhead). I experimented with only caching instantiations of union and intersection types and that seems to do the trick with lower cost. So I'd suggest changing the PR to do that.

I'm still slightly concerned that caches might not always be invalidated. For example, if the nonFixingMapper associated with an inference context is combined with another mapper, then the cache of that combined mapper isn't cleared by clearCachedInferences, and it's not easy to see how it could be. This hypothetical issue doesn't show up in any of our tests, but I do think it could possibly happen.

@ssalbdivad
Copy link

@ahejlsberg This merge implementation was never meant to be the example of exponential instantiation scaling. It just happened to be easy to repro.

What concerns me is that whether or not individual instantiations are expensive, if there are O(3^N) of them, they will eventually dominate the check time of the entire type.

IMO given @Andarist's second PR has no clear downsides in terms of check time or memory consumption, it seems worthwhile to just eliminate the logic that could lead to this scaling altogether.

@Andarist
Copy link
Contributor Author

I'm still slightly concerned that caches might not always be invalidated. For example, if the nonFixingMapper associated with an inference context is combined with another mapper, then the cache of that combined mapper isn't cleared by clearCachedInferences, and it's not easy to see how it could be. This hypothetical issue doesn't show up in any of our tests, but I do think it could possibly happen.

This is a valid concern 👍 I'll see what I can do to address it

@ahejlsberg
Copy link
Member

This is a valid concern 👍 I'll see what I can do to address it

You might experiment with clearing all caches whenever a new inference is made. If that doesn't adversely affect performance I think that might be the best option. Also, as I mentioned earlier, you can remove some overhead by only caching results of union and intersection instantiations.

@jakebailey
Copy link
Member

@typescript-bot test it
@typescript-bot pack this

@typescript-bot
Copy link
Collaborator

typescript-bot commented May 2, 2025

Starting jobs; this comment will be updated as builds start and complete.

Command Status Results
pack this ✅ Started ✅ Results
test top400 ✅ Started ✅ Results
user test this ✅ Started ✅ Results
run dt ✅ Started ✅ Results
perf test this faster ✅ Started 👀 Results
@typescript-bot
Copy link
Collaborator

typescript-bot commented May 2, 2025

Hey @jakebailey, I've packed this into an installable tgz. You can install it for testing by referencing it in your package.json like so:

{ "devDependencies": { "typescript": "https://typescript.visualstudio.com/cf7ac146-d525-443c-b23c-0d58337efebc/_apis/build/builds/165066/artifacts?artifactName=tgz&fileId=78AE6840F3C45DEE689460611669A8466E20BF2A28EC3B53C7765071FF04AB7A02&fileName=/typescript-5.9.0-insiders.20250502.tgz" } } 

and then running npm install.


There is also a playground for this build and an npm module you can use via "typescript": "npm:@typescript-deploys/pr-build@5.9.0-pr-61505-58".;

@typescript-bot
Copy link
Collaborator

@jakebailey Here are the results of running the user tests with tsc comparing main and refs/pull/61505/merge:

There were infrastructure failures potentially unrelated to your change:

  • 1 instance of "Timeout"

Otherwise...

Everything looks good!

@typescript-bot
Copy link
Collaborator

Hey @jakebailey, the results of running the DT tests are ready.

Everything looks the same!

You can check the log here.

@typescript-bot
Copy link
Collaborator

@jakebailey
The results of the perf run you requested are in!

Here they are:

tsc

Comparison Report - baseline..pr
Metric baseline pr Delta Best Worst p-value
Compiler-Unions - node (v18.15.0, x64)
Errors 34 34 ~ ~ ~ p=1.000 n=6
Symbols 62,390 62,390 ~ ~ ~ p=1.000 n=6
Types 50,395 50,395 ~ ~ ~ p=1.000 n=6
Memory used 195,358k (± 0.94%) 193,632k (± 0.75%) ~ 192,921k 196,597k p=0.230 n=6
Parse Time 1.31s (± 1.08%) 1.31s (± 0.79%) ~ 1.29s 1.32s p=0.738 n=6
Bind Time 0.73s 0.73s ~ ~ ~ p=1.000 n=6
Check Time 9.79s (± 0.38%) 9.78s (± 0.33%) ~ 9.73s 9.81s p=0.517 n=6
Emit Time 2.73s (± 0.67%) 2.73s (± 0.68%) ~ 2.71s 2.75s p=1.000 n=6
Total Time 14.55s (± 0.23%) 14.54s (± 0.38%) ~ 14.47s 14.59s p=1.000 n=6
angular-1 - node (v18.15.0, x64)
Errors 56 56 ~ ~ ~ p=1.000 n=6
Symbols 948,670 948,670 ~ ~ ~ p=1.000 n=6
Types 410,947 410,947 ~ ~ ~ p=1.000 n=6
Memory used 1,224,351k (± 0.00%) 1,224,412k (± 0.00%) ~ 1,224,360k 1,224,514k p=0.128 n=6
Parse Time 6.67s (± 0.45%) 6.65s (± 0.77%) ~ 6.57s 6.70s p=0.628 n=6
Bind Time 1.90s (± 0.40%) 1.88s (± 0.58%) -0.02s (- 0.97%) 1.87s 1.89s p=0.015 n=6
Check Time 31.90s (± 0.24%) 32.10s (± 0.30%) +0.20s (+ 0.64%) 31.96s 32.23s p=0.010 n=6
Emit Time 15.23s (± 0.90%) 15.28s (± 0.36%) ~ 15.21s 15.37s p=0.573 n=6
Total Time 55.71s (± 0.15%) 55.92s (± 0.17%) +0.22s (+ 0.39%) 55.82s 56.07s p=0.005 n=6
mui-docs - node (v18.15.0, x64)
Errors 0 0 ~ ~ ~ p=1.000 n=6
Symbols 2,361,667 2,361,667 ~ ~ ~ p=1.000 n=6
Types 868,625 868,625 ~ ~ ~ p=1.000 n=6
Memory used 2,317,105k (± 0.00%) 2,318,136k (± 0.00%) +1,030k (+ 0.04%) 2,318,073k 2,318,171k p=0.005 n=6
Parse Time 8.76s (± 0.21%) 8.76s (± 0.29%) ~ 8.73s 8.80s p=0.465 n=6
Bind Time 2.21s (± 0.78%) 2.21s (± 1.01%) ~ 2.19s 2.25s p=0.934 n=6
Check Time 69.90s (± 0.33%) 69.85s (± 0.33%) ~ 69.63s 70.19s p=0.748 n=6
Emit Time 0.30s (± 2.11%) 0.30s (± 3.40%) ~ 0.29s 0.32s p=0.654 n=6
Total Time 81.17s (± 0.30%) 81.12s (± 0.28%) ~ 80.89s 81.46s p=0.630 n=6
self-build-src - node (v18.15.0, x64)
Errors 0 0 ~ ~ ~ p=1.000 n=6
Symbols 1,228,044 1,228,093 +49 (+ 0.00%) ~ ~ p=0.001 n=6
Types 267,260 267,268 +8 (+ 0.00%) ~ ~ p=0.001 n=6
Memory used 2,973,179k (±10.03%) 2,608,144k (±14.46%) ~ 2,363,403k 3,095,909k p=0.230 n=6
Parse Time 6.69s (± 1.32%) 6.62s (± 1.46%) ~ 6.50s 6.75s p=0.173 n=6
Bind Time 2.18s (± 2.19%) 2.17s (± 0.95%) ~ 2.13s 2.19s p=0.936 n=6
Check Time 42.87s (± 0.43%) 42.90s (± 0.46%) ~ 42.70s 43.21s p=0.936 n=6
Emit Time 3.43s (± 3.59%) 3.56s (± 5.02%) ~ 3.34s 3.85s p=0.230 n=6
Total Time 55.17s (± 0.47%) 55.27s (± 0.42%) ~ 54.96s 55.47s p=0.471 n=6
self-build-src-public-api - node (v18.15.0, x64)
Errors 0 0 ~ ~ ~ p=1.000 n=6
Symbols 1,228,044 1,228,093 +49 (+ 0.00%) ~ ~ p=0.001 n=6
Types 267,260 267,268 +8 (+ 0.00%) ~ ~ p=0.001 n=6
Memory used 2,427,922k (± 0.03%) 2,427,536k (± 0.01%) ~ 2,427,071k 2,428,053k p=0.298 n=6
Parse Time 5.46s (± 0.88%) 5.47s (± 0.74%) ~ 5.40s 5.51s p=0.573 n=6
Bind Time 1.83s (± 1.02%) 1.81s (± 1.03%) ~ 1.79s 1.84s p=0.331 n=6
Check Time 35.40s (± 0.51%) 35.52s (± 0.19%) ~ 35.46s 35.64s p=0.378 n=6
Emit Time 3.04s (± 0.69%) 3.06s (± 2.16%) ~ 2.99s 3.15s p=1.000 n=6
Total Time 45.73s (± 0.47%) 45.88s (± 0.29%) ~ 45.75s 46.08s p=0.298 n=6
self-compiler - node (v18.15.0, x64)
Errors 0 0 ~ ~ ~ p=1.000 n=6
Symbols 263,411 263,460 +49 (+ 0.02%) ~ ~ p=0.001 n=6
Types 107,096 107,104 +8 (+ 0.01%) ~ ~ p=0.001 n=6
Memory used 441,780k (± 0.03%) 441,847k (± 0.02%) ~ 441,725k 441,909k p=0.128 n=6
Parse Time 3.55s (± 0.81%) 3.53s (± 0.85%) ~ 3.50s 3.57s p=0.515 n=6
Bind Time 1.31s (± 0.39%) 1.31s (± 0.93%) ~ 1.29s 1.32s p=0.351 n=6
Check Time 18.96s (± 0.42%) 19.05s (± 0.33%) ~ 18.98s 19.12s p=0.064 n=6
Emit Time 1.53s (± 1.64%) 1.53s (± 0.72%) ~ 1.52s 1.55s p=0.805 n=6
Total Time 25.35s (± 0.27%) 25.42s (± 0.40%) ~ 25.30s 25.53s p=0.378 n=6
ts-pre-modules - node (v18.15.0, x64)
Errors 71 71 ~ ~ ~ p=1.000 n=6
Symbols 225,981 225,981 ~ ~ ~ p=1.000 n=6
Types 94,356 94,356 ~ ~ ~ p=1.000 n=6
Memory used 371,253k (± 0.01%) 371,323k (± 0.03%) ~ 371,253k 371,531k p=0.109 n=6
Parse Time 2.89s (± 1.21%) 2.89s (± 0.92%) ~ 2.84s 2.92s p=0.872 n=6
Bind Time 1.58s (± 1.67%) 1.59s (± 1.63%) ~ 1.55s 1.62s p=0.629 n=6
Check Time 16.50s (± 0.27%) 16.55s (± 0.26%) ~ 16.49s 16.59s p=0.147 n=6
Emit Time 0.00s 0.00s (±244.70%) ~ 0.00s 0.01s p=0.405 n=6
Total Time 20.98s (± 0.30%) 21.02s (± 0.17%) ~ 20.98s 21.07s p=0.148 n=6
vscode - node (v18.15.0, x64)
Errors 4 4 ~ ~ ~ p=1.000 n=6
Symbols 3,391,649 3,391,649 ~ ~ ~ p=1.000 n=6
Types 1,148,594 1,148,594 ~ ~ ~ p=1.000 n=6
Memory used 3,443,382k (± 0.00%) 3,443,190k (± 0.01%) ~ 3,442,813k 3,443,554k p=0.230 n=6
Parse Time 14.69s (± 0.92%) 14.70s (± 0.25%) ~ 14.66s 14.75s p=0.627 n=6
Bind Time 5.01s (±11.96%) 4.72s (± 0.39%) ~ 4.70s 4.75s p=0.569 n=6
Check Time 92.54s (± 0.72%) 94.92s (± 2.83%) ~ 92.17s 99.26s p=0.078 n=6
Emit Time 29.79s (± 1.08%) 30.06s (± 2.03%) ~ 29.50s 31.15s p=0.423 n=6
Total Time 142.03s (± 0.68%) 144.41s (± 1.93%) ~ 141.31s 148.14s p=0.128 n=6
webpack - node (v18.15.0, x64)
Errors 2 2 ~ ~ ~ p=1.000 n=6
Symbols 317,345 317,345 ~ ~ ~ p=1.000 n=6
Types 140,331 140,331 ~ ~ ~ p=1.000 n=6
Memory used 472,396k (± 0.03%) 472,299k (± 0.02%) ~ 472,200k 472,471k p=0.173 n=6
Parse Time 5.16s (± 0.54%) 5.17s (± 0.82%) ~ 5.09s 5.20s p=0.418 n=6
Bind Time 2.25s (± 1.12%) 2.25s (± 1.42%) ~ 2.20s 2.30s p=0.934 n=6
Check Time 25.79s (± 0.56%) 25.89s (± 0.34%) ~ 25.78s 26.00s p=0.173 n=6
Emit Time 0.00s (±244.70%) 0.00s (±244.70%) ~ 0.00s 0.01s p=1.000 n=6
Total Time 33.20s (± 0.43%) 33.32s (± 0.26%) ~ 33.18s 33.38s p=0.065 n=6
xstate-main - node (v18.15.0, x64)
Errors 5 5 ~ ~ ~ p=1.000 n=6
Symbols 570,517 570,517 ~ ~ ~ p=1.000 n=6
Types 191,417 191,417 ~ ~ ~ p=1.000 n=6
Memory used 500,886k (± 0.03%) 502,543k (± 0.01%) +1,657k (+ 0.33%) 502,444k 502,603k p=0.005 n=6
Parse Time 4.32s (± 0.73%) 4.31s (± 0.31%) ~ 4.30s 4.33s p=0.801 n=6
Bind Time 1.53s (± 1.13%) 1.53s (± 0.72%) ~ 1.51s 1.54s p=0.933 n=6
Check Time 25.09s (± 1.86%) 24.52s (± 0.27%) -0.57s (- 2.27%) 24.44s 24.59s p=0.005 n=6
Emit Time 0.00s 0.00s ~ ~ ~ p=1.000 n=6
Total Time 30.93s (± 1.59%) 30.36s (± 0.24%) -0.57s (- 1.84%) 30.28s 30.44s p=0.005 n=6
System info unknown
Hosts
  • node (v18.15.0, x64)
Scenarios
  • Compiler-Unions - node (v18.15.0, x64)
  • angular-1 - node (v18.15.0, x64)
  • mui-docs - node (v18.15.0, x64)
  • self-build-src - node (v18.15.0, x64)
  • self-build-src-public-api - node (v18.15.0, x64)
  • self-compiler - node (v18.15.0, x64)
  • ts-pre-modules - node (v18.15.0, x64)
  • vscode - node (v18.15.0, x64)
  • webpack - node (v18.15.0, x64)
  • xstate-main - node (v18.15.0, x64)
Benchmark Name Iterations
Current pr 6
Baseline baseline 6

Developer Information:

Download Benchmarks

@typescript-bot
Copy link
Collaborator

@jakebailey Here are the results of running the top 400 repos with tsc comparing main and refs/pull/61505/merge:

Everything looks good!

@Andarist
Copy link
Contributor Author

Andarist commented May 6, 2025

You might experiment with clearing all caches whenever a new inference is made. If that doesn't adversely affect performance I think that might be the best option.

I've followed this advice and the numbers are still pretty much the same as the ones I mentioned here. Instantiations went up a little bit but by such a negligible number (+341), it doesn't matter at all.

Also, as I mentioned earlier, you can remove some overhead by only caching results of union and intersection instantiations.

The current version doesn't come with any noticeable overhead, from what I can tell. It seems to me doing this just for union/intersections would only complicate the code right now. If you feel strongly about it though, I could certainly run an extra experiment like this.

Copy link
Member

@jakebailey jakebailey left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, I feel like this must be good to go at this point. The code looks pretty simple, the benchmarks are good, and it should be straightforward to port into the Go codebase (eventually) as there aren't any goofy uses of maps / prototype modification anymore.

Are there any remaining concerns here?

@github-project-automation github-project-automation bot moved this from Not started to Needs merge in PR Backlog Jun 5, 2025
@jakebailey jakebailey merged commit 51dcd90 into microsoft:main Jun 6, 2025
32 checks passed
@github-project-automation github-project-automation bot moved this from Needs merge to Done in PR Backlog Jun 6, 2025
@mikeduminy
Copy link

This was a game-changer PR for us. Thank you!

Before After Improvement
Instantiations 704,556,467 10,119,793 98.56%
Check time 395.48 132.09 66.60%
Total time 425.86 164.77 61.31%
@jakebailey
Copy link
Member

What repo is that? Is it public?

@mikeduminy
Copy link

mikeduminy commented Jul 22, 2025

It's a private repo unfortunately. It is for the Klarna app (react-native, web, etc). But we'd be happy to test out anything internally and get back to you if want 😄

@jakebailey
Copy link
Member

jakebailey commented Jul 22, 2025

I want a public repo for our benchmarks, unfortunately

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

For Uncommitted Bug PR for untriaged, rejected, closed or missing bug

9 participants