Gjs garbage collection

Hello all,

I know this is a bit of a niche topic, but I’ll describe the phenomenon I just found while fooling around with both garbage collection systems. I don’t think this is a bug per se, but I do think it should either be documented somewhere or be treated as a bug (see last paragraph for a possible desired effect with minimal memory cost).

A minimal working example that explains the situation is the following one (gjs binary in /usr/bin):

#!/usr/bin/gjs -m

import Adw from 'gi://Adw'

// This registry is just for detecting garbage collection
const registry = new FinalizationRegistry(console.log)

const app = new Adw.Application()

app.connect('activate', (app)=>{
  // I use a dialog here just because it can be closed by pressing 'Esc'.
  const dialog = new Adw.Dialog({
    child: new Adw.Bin(),
  })
  // This will log a message after the bin is garbage collected
  registry.register(dialog.child, 'The bin has been garbage collected.')
  // This is for the script to run while the dialog is open
  app.hold()
  dialog.connect('closed', (dialog)=>{
    app.release()
  })
  dialog.present(null)
  // The next line is for entering the event loop frequently
  setInterval(()=>{}, 100)
})

await app.runAsync(['gjs-test'])

Upon running the script, after about 10 seconds, the Adw.Bin dialog.child gets garbage collected from the gjs side, as noted by the log. This is probably done for performance reasons (it would probably consume too much memory to have gjs objects for each widget in the hierarchy), so I just want to make it clear that this is not a complaint.

On the other hand, if one were to wish for that specific bin not to be garbage collected from the gjs side, the previous behaviour can be easily sidesteped by adding a javascript attribute to the object, like dialog.child._ = ''. This will still allow for the Adw.Bin to be garbage collected after, there are no other GObject nor gjs references.

For some reason, the dialog itself does not seem to be garbage collected in my experiments (maybe because it is been presented).

The main reason this should be more explicitly documented is because some people (at least, I do this in regular javascript) may use FinalizationRegistry for cleaning up references, or WeakMap instances to add weak references.

Another possibility would be to treat this as a bug. Of course, gjs objects that are instances of GObject.Object should still be garbage collected when possible for performance reasons, but it should be avoided if it has references from the GObject side; and either it is currently in a FinalizationRegistry, it is a key of a WeakMap, or it has a WeakRef reference. This could copy the behaviour observed when it has a javascript attribute.

Thank you for your attention, if you have read so far.

P.S.: By the way, what is the behaviour in other languages with native garbage collecion?

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.

Do you mean that if a JavaScript property is added to a GObject, it is never collected? If so, that certainly seems like a bug (but I might be misunderstanding).

So basically, you want to be able to rely on FinalizationRegistry reporting a GObject’s “real” lifetime? If I understand that right, this seems like a good issue to open since folks watching the project repository may want to add their two-cents (I hadn’t thought of this situation myself).

Hi Andy, I’ll clarify what I mean:

Do you mean that if a JavaScript property is added to a GObject, it is never collected? If so, that certainly seems like a bug (but I might be misunderstanding).

No, what I meant is that adding a javascript property results in garbage collecting happening at the end of the Object’s real lifetime. In the example code, it would mean that dialog.child is not garbage collected after 10 seconds (the log is not printed after waiting for a while), but it would be garbage collected if I were to make dialog.child=new Adw.Bin() at a certain point in the future (I added a button to test that, but I don’t remember if it was instantaneous or it took 10 seconds as well, since it was long ago that I tested it).

So basically, you want to be able to rely on FinalizationRegistry reporting a GObject’s “real” lifetime? If I understand that right, this seems like a good issue to open since folks watching the project repository may want to add their two-cents (I hadn’t thought of this situation myself).

That would be great; not only FinalizationRegistry, but also WeakRefs and WeakMap keys. Right now, whenever I want to observe their real lifetime (for instance, in order to close an unneeded file descriptor or some signal in some other object), I add them some javascript property.

Best,

Raúl.

1 Like

Gotcha, that makes sense.

Gotcha; I would recommend opening an issue then (Issues · GNOME / gjs · GitLab). I think it’s a reasonable request (and I can think of uses myself), but @ptomato would be much more familiar with performance implications and might have some ideas about implementing it as a development feature (e.g. ENV variable or CLI flag).

Ok, thank you for your feedback; I’ll open a issue later in the day.

1 Like

Just so the issue is available from this post.

This is a really interesting question, thanks for posing it.

On the one hand I was tempted to repeat what I always tell people: “don’t depend on non-deterministic behaviour.” The reason why the add-a-property behaviour can exist, is because if you haven’t added any JS properties, and there are no references from JS, we can destroy and recreate JS GObject instances belonging to any GObject at any time, and you won’t be able to tell the difference. Unless, of course, you use nondeterministic features like System.addressOf or, as is possible nowadays, FinalizationRegistry.

On the other hand when we got a JS language version that had FinalizationRegistry in it, I had the exact same expectation as you. I thought, this will be great because people can use it for cleanup instead of implementing vfunc_dispose() which is unreliable because it may get called from another thread (in badly-behaved libraries) or when the end of the GObject’s lifetime is after the end of the JS engine’s lifetime. And I didn’t realize until just now that this handwavy thought will not actually work.

And in fact this is the main use case of WeakMap: associating data with an object, that is kept private from the object, that doesn’t prolong the object’s lifetime but lasts just as long as it. So at least conceptually, that indicates that WeakMap ought to switch off the destroy-and-recreate behaviour.

A separate consideration is whether it is actually possible to implement that behaviour. Firefox has a special case in FinalizationRegistry.register() that does basically this when you put a DOM object in a FinalizationRegistry (Firefox DOM objects are also destroyed and recreated on demand) but they don’t provide a way for embedders to hook into this behaviour. So regardless of whether we want to do this, it may not be possible until we can convince Firefox to add API for it.

Yes, I suspected objects were recreated on demand if javascript didn’t mess with them because it would be such a waste to have them always there; that is why when I encountered this behaviour I tried to sidestep it by adding a javascript property and it worked.

I mostly use WeakMaps and WeakRefs for weak references and try to avoid reference cycles to make the garbage collection work easier (I think javascript detects cycles anyway, but I try to be very aware of them for self-educational purposes); and while FinalizationRegistry is non-deterministic, the spec says its callback should be called at the same time `WeakRef` stops derefering back to the object, so I used it for the minimal working example.

Since you mention the DOM objects in firefox, the same special consideration should probably also work for WeakRefs and WeakMap keys because of the spec, so I hope such API eventually becomes available. In the meantime, would it be possible to emphasize somewhere in the guide this behaviour? Although it is very niche, it may be very disconcerting upon first encountering it; and the proposed behaviour, if desired, can easily be achived by just adding a simple attribute by the developer.

Another posibility, although a very hacky one, would be to override all FinalizationRegistry, WeakRef and WeakMap. In the case of the FinalizationRegistry, it could be replaced by another class that adds a javascript symbol property (each registry would be assigned a symbol so unregistration can delete that symbol property, and symbols are less likely to crash with user code). A similar hacky override could be possible for WeakRef and WeakMap.

However, given the complexity of checking for corner cases when the unregistering token is not the object itself and whether it may or not be a GObject object, and because of the ease of obtaining the desired behaviour by just adding a javascript property, it may not be worth it to override.