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
Suggest also using a FinalizationRegistry when a Disposable holds a native handle #168
Conversation
A preview of this PR can be found at https://tc39.es/proposal-explicit-resource-management/pr/168. |
I don't think I'd want to suggest this as good practice in general - FinalizationRegistry is pretty heavy-weight, and also if you do this users may come to rely on it instead of doing their own cleanup, which would be bad. Maybe suggest that you can have a debug-only version which uses FinalizationRegistry to help locate leaks? To put it another way: this advice is basically orthogonal to the proposal, since string-named |
It is pretty heavy weight, but is only really suggested for use with native handles. I imagine most hosts will do this for native handle wrappers, though possibly not by using
I imagine most host objects that provide a |
For additional reference, here is the guidance on implementing a I've also considered simplifying this by proposing something like a |
A rough example of various implementations of a general-purpose |
I'd prefer to not have new ways to observe GC, but we can debate that when that proposal is made. |
I wouldn't classify it as a new way to observe GC, per se, since it's essentially the same way given that any userland library can implement the same thing on top of FinalizationRegistry. |
Let me rephrase, I'd prefer to avoid a new built-in API which is GC sensitive. If that API behaves the same as and can be entirely polyfilled using existing APIs, then in my opinion it belongs to userland. I do hope to someday introduce a new GC sensitive API to the language, but the purpose would be to enable distributed GC, and that cannot be polyfilled. |
|
||
## Ensuring Cleanup of Native Handles, File Descriptors, etc. | ||
|
||
When implementing a disposable that holds a native handle (i.e., a pointer, file descriptor, etc.), it is good practice |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why only a good practice for native handles? If I were to implement a Disposable to let say removeEventListener() when disposed, and use it everywhere to replace addEventListener() calls, I would definitely want to make sure it can be disposed on GC in case someone forget to use it, and print out a warning if that happens.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While that's a valid case, that leans more towards @bakkot's suggestion that such things be reserved for debug builds. Native handles are often a different story as such leaks can potentially cause system instability. Also, callbacks registered via addEventListener
would be visible in a heap dump/snapshot which makes such leaks easier to find than a stray Number
value.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You need to make it a weakRef in that scenario, however EventTarget do not hold weakRefs.
This was discussed during the July, 2023 plenary and the general sentiment expressed at that time matched @bakkot's comment above. No recommendation will be made within the specification at this time. I may still amend the explainer with some suggestions on how to handle "unused" resources, however. |
Thanks for the pointers. The GC integration still seems a little unexplored to me as a leak solution. Could engines go a step further here and call I honestly can't think of a scenario in which this would not be the desired behaviour. Pretty much all low-level object wraps in Node.js act this way too. |
That's not possible as it would be equivalent to a destructor (not a finalizer), which would still have access to object, and could introduce revival issues. Also running user code during GC is a no go. The behavior of finalizers is to optimistically run sometime after the object has been collected. With some kind of automatic disposal on finalization, you'd basically need every object ever created to automatically be probed for a finalizer, which would capture some internal data but somehow not the instance itself, and get executed after the object is collected. |
Yeah that makes sense. The problem though is that I just implemented this in https://github.com/bytecodealliance/jco/pull/357/files#diff-bbd0007a3c51b74cbe102f3e0a555d9e3b61ed661726b6e79b7b83a6152c57b3R114. |
I suppose this just is the common pattern - where the finalization token has to be stored on the instance for deregistration in Symbol.dispose. The important point from a spec perspective is that the primitives are there to do it. I've perhaps come around to this not being so bad after all! |
You can use the instance itself as the registration token. No need for a separate token. |
The issue is you need to track the deregistration token, so that calling Symbol.dispose early will deregister the finalizer and have access to this token from the instance. But I think it works out quite naturally after all. |
This exact example can be found here: and here: |
This adds a non-normative note to suggest that it is good practice to also use a
FinalizationRegistry
when using@@dispose
to implement a disposable object that holds a native handle, so as to ensure the handle is properly closed if the disposable object is garbage collected without having been disposed.Including this suggestion is based on this discussion: #159 (comment)
cc: @tc39/ecma262-editors