-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
Automatic deallocation by FinalizationRegistry
does not work for fully synchronous code
#3917
Comments
FinalizationRegistry
does not work for fully synchronous code.FinalizationRegistry
does not work for fully synchronous code
Your survey is impressive, thanks so much for such a thorough test. |
Further literature review suggests that a solution to this problem was attempted by However, that proposal was withdrawn on 2023/9.
According to the meeting logs, it seems that it has been decided to attempt to resolve this problem with WebAssembly's JavaScript Promise Integration (JSPI). Will this issue be resolved in #3633? |
Interesting, if the conclusion is correct, which would seem intuitive to me (its called GC after all), it seems to me that there isn't really anything Its not clear to me yet if and how the JS Promise integration will be implemented, especially because its just a stopgap for the context switching proposal and isn't even stable yet (and only implemented in Chrome I believe). |
Thank you for your views. My gut feeling is also that there is nothing
Thanks. That answer is good enough for now. I will check after the JSPI is implemented to see what it is and if it can solve this problem. |
It is not always feasible to use async/await, our code uses some synchronous parts and we currently inject a few awaits backed with setTimeout to let the event loop (and therefor the deallocator) some breathing room. This isn't ideal but it's the only thing we currently can do. What can be done to help fix or reduce the impact of this issue? Appreciate it. |
The conclusion was that there isn't anything that can really be done here from I'm gonna go ahead and close the issue as this isn't really actionable. |
Would like to understand whose side something can be done from then if not wasm-bindgen and where the issue could be resolved before closing. Appreciated. For us it causes memory to be over 10 times what is necessary. If you have already closed then same question. |
I believe this has been sufficiently answered in the OP (see the "Workaround" section) and follow-up posts. |
Summary
This is neither a feature request nor a bug report. It is intended to share my experience with the developers of wasm-bindgen and its users.
FinalizationRegistry
is an API that calls a callback when an object is deallocated.wasm-bindgen has an opt-in feature to automatically deallocate WebAssembly memory using this API.
FinalizationRegistry
is an effective way to automatically deallocate WebAssembly memory. However, it is not perfect.Specifically, if you write code that is completely synchronous from the beginning to the end of your program, the automatic deallocation by
FinalizationRegistry
will not work. And it causes memory leaks.How to reproduce
Repository: https://github.com/mizdra/repro-wasm-bindgen-weak-ref-memory-leak
src/lib.rs
:sync.mjs
:What happened?
This behavior is due to the
FinalizationRegistry
cleanup callback being called after the synchronous execution has completed.For clarity, here is a simpler code.
minimum-repro/sync.mjs
:minimum-repro/async.mjs
:Calling
global.gc()
causes theFoo
class to be garbage-collected. However, the cleanup callback is not called at that moment; the cleanup callback is called after the synchronous execution has completed. If the synchronous execution is interrupted in a loop, as inasync.mjs
, cleanup callback is called in the loop. However, if the synchronous execution is not interrupted in the loop, as insync.mjs
, cleanup callback is not called in the loop.In wasm-bindgen, WebAssembly memory is deallocated in the cleanup callback. Therefore, if you write fully synchronous code like
sync.mjs
, the cleanup callback is not called and memory is not deallocated. And it causes memory leaks.Why is cleanup callback called after synchronous execution?
This is based on the ECMAScript specification. The cleanup callback does not interrupt the synchronous execution.
https://262.ecma-international.org/14.0/?_gl=1*9k4t1i*_ga*MTY4NzY3NTcxOC4xNzEyMzEwNDI5*_ga_TDCK4DWEPP*MTcxMjMxMDQyOC4xLjAuMTcxMjMxMDQyOC4wLjAuMA..#sec-weakref-invariants
V8's test cases also support this.
Probably, other major JavaScript engines have the same behavior.
Workaround
There are several workarounds.
1. call the
.free()
methodYou can free memory manually using the
.free()
method.2. Call other asynchronous APIs in the loop
Synchronous execution is interrupted in the loop so that cleanup callback is called.
Background of writing this document
I first recognized this behavior when I tried to implement automatic deallocation in @resvg/resvg-wasm (thx/resvg-js#318 (comment)). To make sure that automatic deallocation worked, I wrote code that repeatedly called the @resvg/resvg-wasm API using a loop (thx/resvg-js#318 (comment)). That code is completely synchronous. Therefore, it caused a memory leak.
It may be rare to write code that is completely synchronous. In addition, in my example, the code was not production code, but experimental code to check for memory leaks. However that doesn't mean it can't happen in production code. The users of wasm-bindgen may encounter this problem on rare occasions and spend a lot of time trying to figure out the cause. So I have written this document to help them.
Outro
If there is any input from maintainers on this behavior, I would like to know.
The text was updated successfully, but these errors were encountered: