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

Assertions to track garbage collection (via FinalizationRegistry) #7193

Closed
4 tasks done
mindplay-dk opened this issue Jan 8, 2025 · 3 comments
Closed
4 tasks done

Comments

@mindplay-dk
Copy link

Clear and concise description of the problem

I'm writing a test suite for a library that manages a bidirectional graph - I want to verify that nodes in the graph get correctly detached and become available for garbage collection, so as not to leave behind references that point to unused partitions of the graph... to prevent memory leaks.

Suggested solution

Here's a rough example, which (maybe) proves that it's (very maybe) possible (?) 🤷‍♂️

const registry = new FinalizationRegistry(message => {
  events.add(message)
});

const events = new Set()

function traceGC(obj, message) {
  registry.register(obj, message)
}

function example() {
    const x = {};
    traceGC(x, "x has been collected")
}

example();

function awaitGC(message) {
  return new Promise(resolve => {
    global.gc()

    const timer = setInterval(() => {
      if (events.has(message)) {
        events.delete(message)
        clearInterval(timer)
        resolve()
      }
    })
  })
}

awaitGC("x has been collected").then(() => {
  console.log("x has been collected")
})

This needs to be run with node --expose-gc so we can call global.gc() to force garbage collection.

Of course, this is just a sketch, suggesting (maybe) it's possible - we might want an API something like:

test("memory stuff", () => {
  let obj = {}
  vi.traceGC(obj, "message")
  obj = null

  expect(vi.awaitGC("message")).toHaveResolved()
})

We might want a timeout argument as well, causing the promise to get rejected.

I don't know if that's the ideal API - I'm only barely starting to understand the finalization registry.

The vitest CLI itself would need to launch node with --expose-gc or it won't work.

Alternative

Slow tests that run things thousands of times and checks the memory usage? yeesh idk.

Additional context

This nice article about the finalization registry clued me in:

https://dev.to/codux/experiments-with-the-javascript-garbage-collector-2ae3

Validations

@hi-ogawa
Copy link
Contributor

hi-ogawa commented Jan 9, 2025

Interesting, but I'm not sure if there's anything specially needed from Vitest for this 🤔

Users can simply implement such traceGC/awaitGC helpers (or use library if any) in their tests already. --expose-gc can be also enabled from poolOptions.forks.execArgv. Here is an example (this needs to be downloaded locally since stackblitz doesn't seem to have gc):
https://stackblitz.com/edit/vitest-dev-vitest-julke2ed?file=test%2Frepro.test.ts

@mindplay-dk
Copy link
Author

Users can simply implement such traceGC/awaitGC helpers (or use library if any) in their tests already.

well, it turns out, Jest has this:

https://github.com/jestjs/jest/tree/main/packages/jest-leak-detector

and as you suggested, there might not be anything specially needed from the test framework to support this - though this particular package does have a a lot of dependencies, and I don't understand if this would work outside of Jest.

--expose-gc can be also enabled from poolOptions.forks.execArgv

I found that out after doing some digging - I think this Jest package tries to get around that somehow, but I don't fully understand what it's doing, or even where these vm and v8 packages come from. (I don't think these are the NPM packages? the vm package has not been updated in 9 years, so maybe this is actually mapped to node:vm somehow? I can't see where or how.)

is there any reason --expose-gc isn't enabled by default? by Vite, I mean.

of course, it's not currently using it, so no reason to enable it - but that might be one reason to have it built-into the test framework? so you get something that "just works" without first having to figure out how to get it working.

idk, feel free to close the issue, I guess, if this feels out of scope or too rare to include as a built-in feature. 🙂

@hi-ogawa
Copy link
Contributor

It looks like Jest uses it for --detectLeaks https://jestjs.io/blog/2017/12/18/jest-22#experimental-leak-detection, but it appears experimental and have some issues/limitations https://github.com/jestjs/jest/issues?q=is%3Aissue%20state%3Aclosed%20detectLeaks

I think we can say this is a niche feature and probably people can explore in user land for now.

is there any reason --expose-gc isn't enabled by default?

I don't think it makes much sense to enable arbitrary flags which aren't enabled in a real code as that can potentially lead to inconsistency between test and production runtime.

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

No branches or pull requests

2 participants