Skip to content

RedisClientType type is slow to typecheck (TS performance issue) #2975

Open
@JulienZD

Description

@JulienZD

Description

We just upgraded redis from 4.7.0 to 5.1.0. In our CI we have a job to verify that all types are still correct (using tsc --noEmit). Since upgrading redis this job now runs out of memory:

CI log

> tsc --noEmit -p tsconfig.test.json
<--- Last few GCs --->
[47:0x7d3f4fbaf000]    79561 ms: Mark-Compact 2043.8 (2085.9) -> 2042.7 (2086.9) MB, pooled: 0 MB, 2499.84 / 0.00 ms  (average mu = 0.093, current mu = 0.008) allocation failure; scavenge might not succeed
[47:0x7d3f4fbaf000]    82274 ms: Mark-Compact 2044.7 (2086.9) -> 2043.6 (2087.9) MB, pooled: 0 MB, 2692.69 / 0.00 ms  (average mu = 0.049, current mu = 0.007) allocation failure; scavenge might not succeed
<--- JS stacktrace --->
FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory
----- Native stack trace -----
 ELIFECYCLE  Command failed.
Aborted (core dumped)

I can reproduce this locally using NODE_OPTIONS="--max-old-space-size=2048" pnpm tsc --noEmit -p tsconfig.test.json (this does also include our application code, so the max size is probably different for a small reproduction).

I analyzed the output on my local machine (Macbook M3 Pro, 36GB RAM) using @typescript/analyze-trace, which gave the following output:

Hot Spots
└─ Check file [35m/project/test/test-helpers/[36mmocks.ts[39m[35m[39m (2747ms)
   └─ Check expression from (line 21, char 5) to (line 23, char 7) (2734ms)
      └─ Check expression from (line 21, char 18) to (line 23, char 7) (2717ms)
         └─ Determine variance of type 335076 (2638ms)
            └─ Compare types 689415 and 689406 (1366ms)
               └─ Compare types 689415 and 689398 (1366ms)
                  └─ Compare types 689407 and 689398 (1330ms)
                     ├─ {"id":689407,"kind":"GenericTypeAlias","name":"WithCommands","aliasTypeArguments":[335026,335036,335037,335038,47,335040],"location":{"path":"[35m/project/node_modules/[36m.pnpm[39m[35m/@redis+client@5.1.0/node_modules/[36m@redis/client[39m[35m/dist/lib/client/multi-command.d.ts[39m","line":9,"char":1}}
                     │  ├─ {"id":335026,"kind":"TypeParameter","name":"REPLIES","location":{"path":"[35m/project/node_modules/[36m.pnpm[39m[35m/@redis+client@5.1.0/node_modules/[36m@redis/client[39m[35m/dist/lib/client/multi-command.d.ts[39m","line":25,"char":41}}
                     │  ├─ {"id":335036,"kind":"TypeParameter","name":"M","location":{"path":"[35m/project/node_modules/[36m.pnpm[39m[35m/@redis+client@5.1.0/node_modules/[36m@redis/client[39m[35m/dist/lib/client/multi-command.d.ts[39m","line":25,"char":69}}
                     │  ├─ {"id":335037,"kind":"TypeParameter","name":"F","location":{"path":"[35m/project/node_modules/[36m.pnpm[39m[35m/@redis+client@5.1.0/node_modules/[36m@redis/client[39m[35m/dist/lib/client/multi-command.d.ts[39m","line":25,"char":93}}
                     │  ├─ {"id":335038,"kind":"TypeParameter","name":"S","location":{"path":"[35m/project/node_modules/[36m.pnpm[39m[35m/@redis+client@5.1.0/node_modules/[36m@redis/client[39m[35m/dist/lib/client/multi-command.d.ts[39m","line":25,"char":119}}
                     │  ├─ {"id":47,"kind":"TypeParameter"}
                     │  └─ {"id":335040,"kind":"TypeParameter","name":"TYPE_MAPPING","location":{"path":"[35m/project/node_modules/[36m.pnpm[39m[35m/@redis+client@5.1.0/node_modules/[36m@redis/client[39m[35m/dist/lib/client/multi-command.d.ts[39m","line":25,"char":170}}
                     └─ {"id":689398,"kind":"GenericTypeAlias","name":"WithCommands","aliasTypeArguments":[335026,335036,335037,335038,46,335040],"location":{"path":"[35m/project/node_modules/[36m.pnpm[39m[35m/@redis+client@5.1.0/node_modules/[36m@redis/client[39m[35m/dist/lib/client/multi-command.d.ts[39m","line":9,"char":1}}
                        ├─ {"id":335026,"kind":"TypeParameter","name":"REPLIES","location":{"path":"[35m/project/node_modules/[36m.pnpm[39m[35m/@redis+client@5.1.0/node_modules/[36m@redis/client[39m[35m/dist/lib/client/multi-command.d.ts[39m","line":25,"char":41}}
                        ├─ {"id":335036,"kind":"TypeParameter","name":"M","location":{"path":"[35m/project/node_modules/[36m.pnpm[39m[35m/@redis+client@5.1.0/node_modules/[36m@redis/client[39m[35m/dist/lib/client/multi-command.d.ts[39m","line":25,"char":69}}
                        ├─ {"id":335037,"kind":"TypeParameter","name":"F","location":{"path":"[35m/project/node_modules/[36m.pnpm[39m[35m/@redis+client@5.1.0/node_modules/[36m@redis/client[39m[35m/dist/lib/client/multi-command.d.ts[39m","line":25,"char":93}}
                        ├─ {"id":335038,"kind":"TypeParameter","name":"S","location":{"path":"[35m/project/node_modules/[36m.pnpm[39m[35m/@redis+client@5.1.0/node_modules/[36m@redis/client[39m[35m/dist/lib/client/multi-command.d.ts[39m","line":25,"char":119}}
                        ├─ {"id":46,"kind":"TypeParameter"}
                        └─ {"id":335040,"kind":"TypeParameter","name":"TYPE_MAPPING","location":{"path":"[35m/project/node_modules/[36m.pnpm[39m[35m/@redis+client@5.1.0/node_modules/[36m@redis/client[39m[35m/dist/lib/client/multi-command.d.ts[39m","line":25,"char":170}}

This led me to a file used in our tests:
Affected code (unrelated code omitted for brevity)

// test/test-helpers/mocks.ts
import { createClient, type RedisClientType } from 'redis';

export class RedisTestContainer {
  private redis: RedisClientType | undefined;
  
  public async init() {
    this.redis = createClient({ /* config */ });

    await this.redis.connect();
    
    return this.redis;
  }

  public async stop() {
    await this.redis?.stop();
  }
}

It seems that type checking RedisClientType is taking 2.7s locally, most likely taking up a bunch of memory as it does so. From the output it seems the culprit is somewhere in multi-command.d.ts.

Replacing RedisClientType with { connect: () => Promise<unknown>; quit: () => Promise<unknown> } fixes the issue for us, since those are the only two methods we use anyway.

This fixes the issue for us, as running NODE_OPTIONS="--max-old-space-size=2048" pnpm tsc --noEmit -p tsconfig.test.json now finishes without errors. Our CI also no longer runs out of memory.

Node.js Version

22.14.0

Redis Server Version

Node Redis Version

5.1.0

Platform

MacOS, Linux

Logs

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions