Skip to content

Increased memory usage when creating a watch program that includes local JavaScript files in node_modulesΒ #58011

Closed
@reduckted

Description

@reduckted

πŸ”Ž Search Terms

Reached heap limit Allocation failed out of memory createWatchCompilerHost createWatchProgram

πŸ•— Version & Regression Information

⏯ Playground Link

https://github.com/reduckted/repro-typescript-memory-usage

πŸ’» Code

This cannot be reproduced with a few lines of code. It needs a large TypeScript project. Use these steps to see the increased memory usage:

  1. Clone the repository git clone https://github.com/reduckted/repro-typescript-memory-usage
  2. Run npm i
  3. Run node run.js and see the memory usage (this will be for TypeScript 5.4.3).
  4. Install TypeScript 5.3.3 npm i typescript@5.3.3 -D
  5. Run node run.js and see the memory usage.

For the linked repository, I am seeing 525 MB used in 5.3.3 and 775 MB used in 5.4.3. The larger the TypeScript project, the worse the memory usage is.

πŸ™ Actual behavior

The memory consumed by TypeScript 5.4 is much larger compared to 5.3. The larger the TypeScript project, the worse the memory usage is. Large projects can cause Node.js to run out of memory.

πŸ™‚ Expected behavior

TypeScript should have equivalent memory usage to v5.3.

Additional information about the issue

What I've been able to determine is that when you have a JavaScript project in the repository and install it as a node module, creating a "watch" program that includes files that require files from that project will consume significantly more memory in 5.4.3 compared to 5.3.3.

I encountered this problem when running ESLint with a config that uses the @typescript-eslint/* plugins. Using TypeScript 5.3.3, ESLint would run fine. After upgrading to 5.4.3, ESLint would crash with an out of memory error:

FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory

The repository that this was occurring on is an internal, closed-source company project, so I can't share it and signing an NDA will not be possible. However, I have been able to see an increased memory usage with a similar project setup in other projects. The repository I have linked above is a clone of ionic-team/ionic-framework with a few additions to it.

This is roughly the setup in the closed-source repository:

  • All TypeScript code is contained under a source/ directory.
  • We have some JavaScript projects that are for various build tools. One of them is for our Storybook configuration.
  • We install those JavaScript projects as node modules so that we can refer to them using @my/project (for example) rather than use relative paths.
  • We have a .storybook/ folder at the root. The files in that folder require some of the JavaScript projects.
  • We have a TypeScript configuration file that is specifically used by ESLint. It includes all of the TypeScript code as well as the .storybook/ directory, as well as a few other files that aren't relevant to this issue.

I've created a similar setup in the linked repository.

I've created a script called run.js that does a simplified version of what the @typescript-eslint plugin does. It creates a "watch" compiler host, then creates a "watch" program:

let tsconfigPath = path.join(__dirname, "tsconfig.memory.json");

let host = ts.createWatchCompilerHost(
  tsconfigPath,
  {
    noEmit: true,
    noUnusedLocals: true,
    noUnusedParameters: true,
    allowNonTsExtensions: true,
    allowJs: true,
    checkJs: true,
    extendedDiagnostics: true,
  },
  ts.sys,
  ts.createAbstractBuilder,
  () => {},
  () => {}
);

host.onUnRecoverableConfigFileDiagnostic = (diagnostic) => {
  console.log(
    ts.flattenDiagnosticMessageText(diagnostic.messageText, ts.sys.newLine)
  );
};

host.trace = () => {};

console.log("TypeScript v" + ts.version);

console.log(
  `Memory before: ${Math.trunc(process.memoryUsage().heapUsed / 1048576)} MB`
);
let program = ts.createWatchProgram(host);

console.log(
  `Memory after:  ${Math.trunc(process.memoryUsage().heapUsed / 1048576)} MB`
);
program.close();

Using the linked repository, I'm seeing this when using TypeScript 5.3.3:

TypeScript v5.3.3
Memory before: 20 MB
Memory after:  525 MB

And this for TypeScript 5.4.3:

TypeScript v5.4.3
Memory before: 20 MB
Memory after:  775 MB

An extra 250 MB of memory used. If you remove ".storybook/**/*.js" from the TypeScript configuration, the amount of memory used by TypeScript 5.4.3 is about 580 MB, so still a little bit higher that 5.3.3, but significantly less that 5.4.3.

The larger the project, the worse the memory usage is.

In the closed-source repository, if I remove about half the code, I get these results:

Some of the extended diagnostics
--------------------------------
Files:                6726
Lines of Library:     37748
Lines of Definitions: 200580
Lines of TypeScript:  468949
Lines of JavaScript:  0
Lines of JSON:        0
Lines of Other:       0

Memory usage
------------
TypeScript v5.3.3:
Memory before: 20 MB
Memory after:  1313 MB

TypeScript v5.4.3:
Memory before: 20 MB
Memory after:  3532 MB

If I include all of the code and increase the heap size, I get these results:

Some of the extended diagnostics
--------------------------------
Files:                8800
Lines of Library:     37748
Lines of Definitions: 202172
Lines of TypeScript:  1118038
Lines of JavaScript:  0
Lines of JSON:        0
Lines of Other:       0

Memory usage
------------
TypeScript v5.3.3:
Memory before: 20 MB
Memory after:  2254 MB

TypeScript v5.4.3:
Memory before: 20 MB
Memory after:  8180 MB

As you can see, that's an extra 6 GB of memory compared to 5.3.3! 🀯

Here's a summary of the increased memory usage:

Files Memory Usage in v5.3.3 Memory Usage in v5.4.3
1605 525 MB 775 MB
6726 1313 MB 3532 MB
8800 2254 MB 8180 MB

Metadata

Metadata

Assignees

Labels

Needs InvestigationThis issue needs a team member to investigate its status.

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions