vuejs / core

🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
https://vuejs.org/
MIT License
46.79k stars 8.21k forks source link

Memory leak #11901

Closed ppyl-datBoi closed 1 week ago

ppyl-datBoi commented 1 week ago

Vue version

3.5.4

Link to minimal reproduction

http://cant.insert.code/commercial_project

Steps to reproduce

The problem started to appear in Vue 3.5.* (including version 3.5.4). There are no problems in version 3.4.38. In my case, the approximate source of the problem is an active websocket connection.

What is expected?

No memory leak

What is actually happening?

After establishing a socket connection from which data is coming, the memory grows infinitely. Moreover, the more socket connections, the more the memory grows without stopping. With 10 connections in 5-10 minutes, the heap size consumption is more than 3 GB. An example of how a leak starts after the application starts: image

At the same time, taking memory snapshots is very difficult because the snapshot formation often gets stuck at the stage (Building dominator tree). image

If you still manage to take a memory snapshot, it turns out that the preliminary cause is deps (I may be wrong). image

I want to clarify that in case of a leak: DOM Nodes - do not change. JS Event Listeners - do not change.

System Info

System:
    OS: Windows 10 10.0.19044
    CPU: (12) x64 12th Gen Intel(R) Core(TM) i5-12400
    Memory: 13.66 GB / 31.77 GB
  Binaries:
    Node: 18.12.0 - C:\Program Files\nodejs\node.EXE
    Yarn: 1.22.4 - C:\Program Files (x86)\Yarn\bin\yarn.CMD
    npm: 8.19.2 - C:\Program Files\nodejs\npm.CMD
  Electron": 32.0.1
  Browsers:
    Chrome: 128.0.6613.120
  npmPackages:
    vue: ^3.5.4 => 3.5.4

Any additional comments?

No response

ppyl-datBoi commented 1 week ago

Solution: After I removed {deep: true} in all "watch" functions the problem disappeared. But because of the many such "watch " now I will have to reconsider the logic in the application.

jh-leong commented 1 week ago

Could you please check the reproduction link? It seems to be inaccessible.

ppyl-datBoi commented 1 week ago

I can't provide full repro code because I have a complex commercial project from which it is difficult to extract repro code. The fact is that "watch" with {deep: true} starts to leak.

ppyl-datBoi commented 1 week ago

I also noticed that it's not just watch that's leaking. I have a couple of computed functions that are also leaking (the events array is updated several times per second). I compared several snapshots and noticed that the deps class is not being cleared, and that's a problem on your end. image

Example of comparing snapshots 3 minutes apart: image

edison1105 commented 1 week ago

We can't analyze the bug without a runnable minimal reproduction. Please follow the issue requirement.

ppyl-datBoi commented 1 week ago

I don't know if this mini demo is what you wanted to see, but even from it you can see that when comparing memory snapshots, the Dep class is not cleared by GC. But the computed leak could not be confirmed. Here is the link

edison1105 commented 1 week ago

Still unable to reproduce

image

gkubisa commented 1 week ago

I'm not sure if it is related but when debugging a reactivity issue in one of my vitest tests I noticed an issue with the double-linked list for tracking dependencies:

image

I wonder if a memory leak could be caused by the redundant Dep objects referenced by depsTail.prevDep.prevDep.prevDep.*? Maybe in some situations they could be accumulating much more than in my test code?

Unfortunately I'm still struggling to isolate the issue. :-/

PS. The issue I'm debugging is about a computed not registering as a dependency of a shallowRef, even though I can see that the track function is called by Vue as expected.

ppyl-datBoi commented 1 week ago

Still unable to reproduce

  • in Chrome incognito mode, ensure there are no browser extensions
  • manually collect garbage
  • JS heap size, DOM Nodes, JS event listeners. Their numbers will not continue to grow.

image

I also deliberately launched the demo in incognito mode. As you can see from the screenshot, the memory is gradually growing from snapshot to snapshot. Deps is also growing and is not cleared. Even deliberately calling the garbage collector from devtools does not improve the situation.

image

rxdiscovery commented 6 days ago

Thank you very much for this patch, it explains why all my applications made with the Quasar framework lagged, and the browser froze.

rxdiscovery commented 3 days ago

Hello, @yyx990803 @ppyl-datBoi : Version 3.5.5 had solved the problem, the new version 3.5.6 brought back the problem, but now my application made with Vue lag after 15 min of use, and crashes totally after 1h to 2h, is this a new memory leak?

Important note: this happens in dev mode, in release mode everything's fine (for now), I'm using the Quasar framework and Vite/yarn for the build.

edison1105 commented 3 days ago

@rxdiscovery Does the reproduction of this issue reproduce your problem? if not, please create a new issue with a minimal reproduction.

yyx990803 commented 3 days ago

Might be related to c74bb8c2d

@rxdiscovery can you try the commit release here (set vue dependency in package.json to https://pkg.pr.new/vue@cbc39d5) to see if it works?

rxdiscovery commented 1 day ago

Hello,

@yyx990803 I'll test the c74bb8c version and give you feedback as soon as possible.

I'm currently investigating the cause, there are 3 potential causes :

@edison1105 @yyx990803 I've also reported the problem here (the link below) with a video demonstration and the steps to reproduce it. https://github.com/quasarframework/quasar/issues/17520

(in the video, the first minute the application is reactive, but in minute 4 and +, it lags until it freezes.)

edison1105 commented 1 day ago

@rxdiscovery please also try the preview packages from https://github.com/vuejs/core/pull/11971 to confirm if it can resolve your problem. It fix a memory leak which happens in dev.

rxdiscovery commented 1 day ago

Hello,

to avoid any pollution, I created a new virtual machine with debian and the latest version of nodeJS and the latest version of Firefox. The same problem occurred

then I tested your suggestions:

@yyx990803

  1. modification of the dependense
    "dependecies" : {
    ...
    "vue" : "https://pkg.pr.new/vue@cbc39d5"
    ...
    }
  2. yarn cache clean
  3. rm yarn.lock
  4. yarn install
  5. quasar clean
  6. quasar dev
  7. clean browser cache
  8. check ===> same problem

@edison1105

  1. modification of the dependense
    "dependecies" : {
    ...
    "vue" : "https://pkg.pr.new/vue@11971"
    ...
    }
  2. yarn cache clean
  3. rm yarn.lock
  4. yarn install
  5. quasar clean
  6. quasar dev
  7. clean browser cache
  8. check ===> same problem

even if I force my Vue version, does quasar still use 3.5.5 internally?

Is the problem related to this issue? Or are we on the wrong track and I have to open another issue?

edison1105 commented 20 hours ago

@rxdiscovery Please try to provide a minimal reproduction without quasar. I will take a deep look then.

rxdiscovery commented 8 hours ago

@edison1105 There is a leak in Vue, it is located at the level of :

node_modules/@vue/shared/dist/shared.esm-bundler.js

Chrome memory profiling :

Screenshot from 2024-09-20 23-07-42

Firefox memory profiling :

Screenshot from 2024-09-20 23-08-29

it's like there's a loop !

Screenshot from 2024-09-20 23-11-02

perhaps the problem lies here:

const patchKeyedChildren = (c1, c2, container, parentAnchor, parentComponent, parentSuspense, namespace, slotScopeIds, optimized) => {
    let i = 0;
    const l2 = c2.length;
    let e1 = c1.length - 1;
    let e2 = l2 - 1;
    while (i <= e1 && i <= e2) {
      const n1 = c1[i];
      const n2 = c2[i] = optimized ? cloneIfMounted(c2[i]) : normalizeVNode(c2[i]);
      if (isSameVNodeType(n1, n2)) {
        patch(
          n1,
          n2,
          container,
          null,
          parentComponent,
          parentSuspense,
          namespace,
          slotScopeIds,
          optimized
        );
      } else {
        break;
      }
      i++;
    }
    while (i <= e1 && i <= e2) {
      const n1 = c1[e1];
      const n2 = c2[e2] = optimized ? cloneIfMounted(c2[e2]) : normalizeVNode(c2[e2]);
      if (isSameVNodeType(n1, n2)) {
        patch(
          n1,
          n2,
          container,
          null,
          parentComponent,
          parentSuspense,
          namespace,
          slotScopeIds,
          optimized
        );
      } else {
        break;
      }
      e1--;
      e2--;
    }
    if (i > e1) {
      if (i <= e2) {
        const nextPos = e2 + 1;
        const anchor = nextPos < l2 ? c2[nextPos].el : parentAnchor;
        while (i <= e2) {
          patch(
            null,
            c2[i] = optimized ? cloneIfMounted(c2[i]) : normalizeVNode(c2[i]),
            container,
            anchor,
            parentComponent,
            parentSuspense,
            namespace,
            slotScopeIds,
            optimized
          );
          i++;
        }
      }
    } else if (i > e2) {
      while (i <= e1) {
        unmount(c1[i], parentComponent, parentSuspense, true);
        i++;
      }
    } else {
      const s1 = i;
      const s2 = i;
      const keyToNewIndexMap = /* @__PURE__ */ new Map();
      for (i = s2; i <= e2; i++) {
        const nextChild = c2[i] = optimized ? cloneIfMounted(c2[i]) : normalizeVNode(c2[i]);
        if (nextChild.key != null) {
          if (keyToNewIndexMap.has(nextChild.key)) {
            warn$1(
              `Duplicate keys found during update:`,
              JSON.stringify(nextChild.key),
              `Make sure keys are unique.`
            );
          }
          keyToNewIndexMap.set(nextChild.key, i);
        }
      }
      let j;
      let patched = 0;
      const toBePatched = e2 - s2 + 1;
      let moved = false;
      let maxNewIndexSoFar = 0;
      const newIndexToOldIndexMap = new Array(toBePatched);
      for (i = 0; i < toBePatched; i++)
        newIndexToOldIndexMap[i] = 0;
      for (i = s1; i <= e1; i++) {
        const prevChild = c1[i];
        if (patched >= toBePatched) {
          unmount(prevChild, parentComponent, parentSuspense, true);
          continue;
        }
        let newIndex;
        if (prevChild.key != null) {
          newIndex = keyToNewIndexMap.get(prevChild.key);
        } else {
          for (j = s2; j <= e2; j++) {
            if (newIndexToOldIndexMap[j - s2] === 0 && isSameVNodeType(prevChild, c2[j])) {
              newIndex = j;
              break;
            }
          }
        }
        if (newIndex === void 0) {
          unmount(prevChild, parentComponent, parentSuspense, true);
        } else {
          newIndexToOldIndexMap[newIndex - s2] = i + 1;
          if (newIndex >= maxNewIndexSoFar) {
            maxNewIndexSoFar = newIndex;
          } else {
            moved = true;
          }
          patch(
            prevChild,
            c2[newIndex],
            container,
            null,
            parentComponent,
            parentSuspense,
            namespace,
            slotScopeIds,
            optimized
          );
          patched++;
        }
      }
      const increasingNewIndexSequence = moved ? getSequence(newIndexToOldIndexMap) : EMPTY_ARR;
      j = increasingNewIndexSequence.length - 1;
      for (i = toBePatched - 1; i >= 0; i--) {
        const nextIndex = s2 + i;
        const nextChild = c2[nextIndex];
        const anchor = nextIndex + 1 < l2 ? c2[nextIndex + 1].el : parentAnchor;
        if (newIndexToOldIndexMap[i] === 0) {
          patch(
            null,
            nextChild,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            namespace,
            slotScopeIds,
            optimized
          );
        } else if (moved) {
          if (j < 0 || i !== increasingNewIndexSequence[j]) {
            move(nextChild, container, anchor, 2);
          } else {
            j--;
          }
        }
      }
    }
  };
edison1105 commented 7 hours ago

the only thing we need is a minimal reproduction not screenshots

rxdiscovery commented 7 hours ago

@edison1105 I'm currently analyzing the source code of Quasar's “QSelect”, to reproduce something similar in Vue, then I'll be able to give you a code without any dependencies.

edison1105 commented 5 hours ago

@rxdiscovery try v3.5.7 by the way. We fixed some memory leaks yesterday.

rxdiscovery commented 5 hours ago

@rxdiscovery try v3.5.7 by the way. We fixed some memory leaks yesterday.

I've already done the test, but unfortunately the problem persists.