vuejs / vue-test-utils

Component Test Utils for Vue 2
https://vue-test-utils.vuejs.org
MIT License
3.57k stars 669 forks source link

Memory Leak with shallowMount / mount #2041

Open CaptainFalcoGX opened 1 year ago

CaptainFalcoGX commented 1 year ago

Subject of the issue

Seeing a 2MB heap size increase after each test using shallowMount / mount.

Steps to reproduce

Run this command with the below setup: ./node_modules/.bin/jest -- "DummyTest-test.js"

Vue Version 2.7.10 Vue Test Utils Version 1.3.3 Jest Version 29.0.3 Node Version 14.20.0 vue/vue2-jest Version 28.1.0

DummyTest-test.js

import { shallowMount } from "@vue/test-utils";
import DummyComponent from "../DummyComponent.vue";

let iterations = [...Array(9999).keys()];
let wrapper;
let heap;

afterAll(() => {
  iterations = null;
});

afterEach(() => {
  wrapper.destroy();
  wrapper = null;
  heap = null;
});

it.each(iterations)(`Test`, () => {
  wrapper = shallowMount(DummyComponent);
  heap = (process.memoryUsage().heapUsed / 1024 / 1024).toFixed(2);
  console.log(`Heap Size: ${heap} MB`);
  expect(true).toBe(true);
});

DummyComponent.vue

<template>
  <div>
    <p>Hi</p>
  </div>
</template>

<script>
export default {
  name: "DummyComponent",
};
</script>

jest.config.js

module.exports = {
  testEnvironment: "jsdom",
  transform: {
    "^.+\\.js$": "babel-jest",
    "^.+\\.vue$": "@vue/vue2-jest",
  },
};

Expected behaviour

Heap Size should remain consistent after each iteration of the test.

Actual behaviour

Seeing a 2MB memory leak.

CaptainFalcoGX commented 1 year ago

I have posted this same question on Stack Overflow.

Changing node versions (12, 16, 18) did not resolve the issue.

This is a Vue 2 project, so I cannot use vue-test-utils v2. However, I did a brief test on Vue 3 with Vue Test Utils v2, and it looked the issue was still present.

Looking for help on what to try next, or if others are experiencing the same issue.

To prevent CI from grinding to a halt, I'm using the workerIdleMemoryLimit option from the latest version of Jest as a band-aid.

CaptainFalcoGX commented 1 year ago

If anyone else is experiencing this issue or can reproduce with the steps listed in the original post, that would be great to know. The project I'm working on has ~6000 tests and this memory leak is causing trouble.

leo-coco commented 1 year ago

I am able to reproduce the issue

lmiller1990 commented 1 year ago

Uh oh... anyone up to dig into this and find the problem?

luddwichr commented 1 year ago

Hey there, I deleved a bit into this issue and I think I found a potential culprit. However, I don't know how to solve the issue ;-)

I can reproduce the issue by:

describe('mount and destroy components endlessly', () => { it('should not run out of memory', () => { const compiled = compileToFunctions('

') while (true) { const wrapper = mount(compiled) wrapper.destroy() } }) })


- run the test via `node --max-old-space-size=200 ./node_modules/jest/bin/jest.js test/specs/mount.leak.spec.js --runInBand` with 200 MB max heap to ensure the heap is not simply increasing because the GC does not need to do its work yet...
- observe that the process crashes after a short time

After having failed to utilize `node --inject` + Chrome Dev Tools, I ended up with the following strategy:
Gradually re-implement `mount` by re-using the original code and observe whether the test panics or not.

This eventually led me to [createStubFromComponent](https://github.com/vuejs/vue-test-utils/blob/a989458422a97fe690a52493d5acea9e454eaf6c/packages/create-instance/create-component-stubs.js#L89) as source of the leak. Here, two things are unexpected:
1. [here](https://github.com/vuejs/vue-test-utils/blob/a989458422a97fe690a52493d5acea9e454eaf6c/packages/create-instance/create-component-stubs.js#L99C5-L99C5) `Vue` is directly modified, instead of `_Vue` and tagName is pushed to an array `ignoreElements` even if it is already ignored. Since `transition` and `transition-group` are [default stubs](https://github.com/vuejs/vue-test-utils/blob/a989458422a97fe690a52493d5acea9e454eaf6c/packages/test-utils/src/config.js#L2), this will make the list huge if there are many mounts with many stubs... *However*, this did not cause my test to panic even after 3 min... 
2. [here](https://github.com/vuejs/vue-test-utils/blob/a989458422a97fe690a52493d5acea9e454eaf6c/packages/create-instance/create-component-stubs.js#L73) `_Vue.extend(component)` is called to get hold of its options. This method is invoked for each and every stub that is created. Changing this line to `{}` stops the memory leak.

So I don't really understand why calling `_Vue.extend(component)` causes the leak. And I don't know if there is another way of getting hold of the component props.

I hope that this might be of help for you to find a solution :-)