Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Svelte 5 unused $state memory leak #13371

Closed
JakubCzarlinski opened this issue Sep 23, 2024 · 5 comments · Fixed by #13381
Closed

Svelte 5 unused $state memory leak #13371

JakubCzarlinski opened this issue Sep 23, 2024 · 5 comments · Fixed by #13381

Comments

@JakubCzarlinski
Copy link

JakubCzarlinski commented Sep 23, 2024

Describe the bug

In a drag and drop library, where elements can be moved between nested components, be added and deleted, a lot of components might be generated with a lot of state needing to be passed around. It might be convenient to have a factory that mounts the elements with a new local state to pass around.

When doing so, one might expect that upon:

  • Unmounting the component,
  • The state not being referenced anywhere else,

that the state and it's event listeners to be garbage collectable.

This... unfortunately is a footgun.

From what I understand, the $state wraps the object in a Proxy(), rather than Proxy.revocable(). Maybe Proxy.revocable() should be used instead?

The alternative here is of course "just don't do this" and having a global state, but considering how easy the implementation of localising the state it would be awesome to be able to revoke and garbage collect unused states.

Reproduction

  1. Open REPL / below code in a new project.
  2. Open Dev tools.
  3. Go to memory tab.
  4. REPL: Observe the js vm instance beginning with the # sign / New project: there should only be one instance.
  5. Garbage collect.
  6. Press the button to generate state.
  7. Garbage collect.
  8. State is not collected.

Note: if you open this in a new project rather than in the REPL, the dev tool performance monitor will also show that these are uncollected JS event listeners.

https://svelte-5-preview.vercel.app/#H4sIAAAAAAAACo1TTY-bQAz9K6-jHhItStIr-ZCq9tJD1cO2p9IDAUOmO9hoxqQbIf57NZCw2aqVKoQw4-fnj-fpTWUdBZN-7w3nDZnUvG9bkxi9tPEnnMkpmcQE6XwRT3ah8LbVQ8aZ2qYVr-jRSMeaoOPRwIDKS4PsGp6Z7R36gzStMLHeQKv1fLS6D-BMHSk09zXpyBDf9RpfTzbABuRoPalecLQ1KhGtO8avEzGsopCGAlTQUCP-Akf5U1hdGR47T7hIh8bWJ0XhKGd0LT5--Qxy1BBrSHDsFHoi_AygM_EtvbNBickHBLXOgZ5tUORcosiZRXEk1Lk_5jWhEOeoUCrfxOCq40KtMOoYnys9aq5UdW4ewGKJfuxUK_FYxP4t9thsYbHDu80mWg8PM2oaUXP5xCwFsY6E2ONtiMaiV3rWFJk5kXOSmWF5CyuEg0Ll21Wy_aThYq4keUkRn0mF9KpGcu9qvbQh_bOIGTIstzfzuh-LOe3NNcTPkPFu_bJdvDt2qsIQLpwtnvb9P6c2jNt4c0MqhCskrsGECZF9YpzYS3vG0XKZ6smGfT81NjFFaGnPB5OYRkpbWSpNqr6jIZmvyZz8fy9LFGrUA_sXQTIdolrjCBdxGq8n0B7GiGG3bv9SzI_hN3FnZIq_AwAA

Logs

No response

System Info

Irrelevant in this case?

Severity

annoyance

@trueadm
Copy link
Contributor

trueadm commented Sep 24, 2024

I don't get what Proxy.revocable will gain us here? If you don't revoke the proxy, it behaves the same way as new Proxy.

Digging into this and the leaking seems to come from the DEV ownership logic. In prod the leak around proxies seems to be gone.

@JakubCzarlinski
Copy link
Author

Just tried this in both a brand new SvelteKit and Vite + Svelte repo: the event listeners seem to be cleaned up during dev and prod? I'm not certain where this discrepancy of event listeners / DOM nodes being cleaned up comes from.

Although there is this:

image
(no this is not 4 million checkboxes)

image

when using Google Chrome Version 129.0.6668.59 (Official Build) (64-bit).

Providing env info in case it becomes relevant: npx envinfo --system --npmPackages svelte,rollup,webpack --binaries --browsers

  System:
    OS: Windows 11 10.0.22631
    CPU: (24) x64 AMD Ryzen 9 7900 12-Core Processor
    Memory: 43.91 GB / 63.14 GB
  Binaries:
    Node: 20.11.0 - C:\Program Files\nodejs\node.EXE
    Yarn: 1.22.19 - C:\Program Files\nodejs\yarn.CMD
    npm: 10.8.2 - C:\Program Files\nodejs\npm.CMD
    bun: 1.1.25 - ~\.bun\bin\bun.EXE
  Browsers:
    Edge: Chromium (127.0.2651.74)
    Internet Explorer: 11.0.22621.3527
  npmPackages:
    svelte: ^5.0.0-next.258 => 5.0.0-next.258

(side note - is Chrome mean to be missing from the above list?)

SvelteKit repo: https://github.com/JakubCzarlinski/state-leak-test/
Vite + Svelte repo: https://github.com/JakubCzarlinski/state-leak-test-vite

@trueadm
Copy link
Contributor

trueadm commented Sep 24, 2024

It appears we are appending a load of empty text nodes (probably anchors) on mount and not cleaning them up?

Screenshot 2024-09-24 at 15 40 39

I'll dig into this separately.

Update:

Found the culprit https://github.com/sveltejs/svelte/blob/main/packages/svelte/src/internal/client/render.js#L84

@JakubCzarlinski
Copy link
Author

I don't think the following is in the scope of the Svelte project, but is relevant to my first comment on this regarding the event listeners, so might be of interest in case you are curious. Chrome seems to have some weird behaviours? Am I missing something here?

function spamEvents() {
  for (let i = 0; i < 10_000; i++) {
    // Cannot be GC'd until page reload
    document.dispatchEvent(new MouseEvent('click', {
      view: window,
      clientX: 0,
      clientY: 0
    }));

    // Cannot be GC'd until page reload
    document.dispatchEvent(new MouseEvent('click', {
      view: null,
      clientX: 0,
      clientY: 0
    }));

    // Can be GC'd
    document.dispatchEvent(new MouseEvent('mousedown', {
      view: window,
      clientX: 0,
      clientY: 0
    }));

    // Cannot be GC'd until page reload
    document.dispatchEvent(new KeyboardEvent('keydown', {
      key: 'Enter'
    }));

    // Can be GC'd
    document.dispatchEvent(new KeyboardEvent('keyup', {
      key: 'Enter'
    }));

    // Can be GC'd - (deprecated)
    document.dispatchEvent(new KeyboardEvent('keypress', {
      key: 'Enter'
    }));
  }
}

image

There is also an interesting scaling going on with the time it takes to process these events...

@trueadm
Copy link
Contributor

trueadm commented Sep 25, 2024

I don't think the following is in the scope of the Svelte project, but is relevant to my first comment on this regarding the event listeners, so might be of interest in case you are curious. Chrome seems to have some weird behaviours? Am I missing something here?

function spamEvents() {
  for (let i = 0; i < 10_000; i++) {
    // Cannot be GC'd until page reload
    document.dispatchEvent(new MouseEvent('click', {
      view: window,
      clientX: 0,
      clientY: 0
    }));

    // Cannot be GC'd until page reload
    document.dispatchEvent(new MouseEvent('click', {
      view: null,
      clientX: 0,
      clientY: 0
    }));

    // Can be GC'd
    document.dispatchEvent(new MouseEvent('mousedown', {
      view: window,
      clientX: 0,
      clientY: 0
    }));

    // Cannot be GC'd until page reload
    document.dispatchEvent(new KeyboardEvent('keydown', {
      key: 'Enter'
    }));

    // Can be GC'd
    document.dispatchEvent(new KeyboardEvent('keyup', {
      key: 'Enter'
    }));

    // Can be GC'd - (deprecated)
    document.dispatchEvent(new KeyboardEvent('keypress', {
      key: 'Enter'
    }));
  }
}

image

There is also an interesting scaling going on with the time it takes to process these events...

I also don't really understand what you're doing here and what application you're applying it to. Are you just noting a general Chrome bug? I tried this locally and I don't see the same issue locally with Chrome or Firefox – so I think more context here is required. Also, rather than using that view – which also picks up dev tools listeners, use the heap memory profiling tool that can pin point the causes.

Maybe create a new issue if this is regarding another memory leak problem though – as I also don't think this is that related to the original post.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants