Tips On Memory Management

GJS is JavaScript bindings for GNOME, which means that behind the scenes there are two types of memory management happening: reference tracing (JavaScript) and reference counting (GObject).

Most extension authors will never have to worry about GObject referencing or memory leaks, especially if writing clean, uncomplicated code. This page describes some common ways extensions fail to take scope into account or cleanup main loop sources and signal connections..

Basics

The concept of reference counting is very simple. When a GObject is first created, it has a reference count of 1. When the reference count drops to 0, all the object's resources and memory are automatically freed.

The concept of reference tracing is also quite simple, but can get confusing because it relies on external factors. When a value or object is no longer assigned to any variable, it will be garbage collected. In other words, if the JavaScript engine can not "trace" a value back to a variable it will free that value.

Put simply, as long as GJS can trace a GObject to a variable, it will ensure the reference count does not drop to 0.

Relevant Reading

If you are very new to programming, you should familiarize yourself with the concept of scope and the three types of variables in JavaScript: const, let and var. A basic understanding of these will help you a lot while trying to ensure the garbage collector can do its job.

You should also consider enabling strict mode in all your scripts, as this will prevent some mistakes that lead to uncollectable objects.

Examples

Below we create an StLabel and assign to the variable myLabel. By assigning it to a variable we are "tracing" a reference to it, preventing the JS Object from being garbage collected and the GObject from being freed:

   1 const St = imports.gi.St;
   2 
   3 let myLabel = new St.Label({
   4     text: "Some Text"
   5 });

Because let has block scope, myLabel would stop being traced when the scope is left. If the GObject is not assigned to a variable in a parent scope, it will be collected and freed.

   1 const St = imports.gi.St;
   2 
   3 // This variable is in the top-level scope, effectively making it a global
   4 // variable. It will trace whatever value is assigned to it, until it is set to
   5 // another value or the script exits
   6 let myLabel;
   7 
   8 if (true) {
   9     // This variable is only valid inside this `if` construct
  10     let myLabelScoped = new St.Label({
  11         text: "Some Text"
  12     });
  13 
  14     // After assigning `myLabelScoped` to `myLabel` the GObject is being
  15     // traced from two variables, preventing it from being collected or freed
  16     myLabel = myLabelScoped;
  17 }
  18 
  19 // The GObject is no longer being traced from `myLabelScoped`, but it is being
  20 // traced from `myLabel` so it will not be collected.
  21 log(myLabel.text);
  22 
  23 // After we set `myLabel` to `null`, it will no longer be traceable from any
  24 // variable, thus it will be collected and the GObject freed.
  25 myLabel = null;

Other GObjects can hold a reference to GObjects, such as a container object. This means that even if it can not be traced from a JavaScript variable, it will have a positive reference count and not be freed.

   1 'use strict';
   2 
   3 const St = imports.gi.St;
   4 
   5 
   6 let myLabel = new St.Label({
   7     text: "Some Text"
   8 });
   9 
  10 // Once we add `myLabel` to `myBox`, the GObject's reference count will increase
  11 // and prevent it from being freed, even if it can not longer be traced from a
  12 // JavaScript variable
  13 let myBox = new St.BoxLayout();
  14 myBox.add_child(myLabel);
  15 
  16 // We can now safely stop tracing the GObject from `myLabel` by setting it to
  17 // another value.
  18 //
  19 // It is NOT necessary to do this in most cases, as the variable will stop
  20 // tracing the GObject when it falls out of scope.
  21 myLabel = null;

However, if the only thing preventing a GObject from being collected is another GObject holding a reference, once it drops that reference it will be freed.

   1 'use strict';
   2 
   3 const St = imports.gi.St;
   4 
   5 let myBox = new St.BoxLayout();
   6 
   7 if (true) {
   8     let myLabel = new St.Label({
   9         text: "Some Text"
  10     });
  11     
  12     myBox.add_child(myLabel);
  13 }
  14 
  15 // Even though it has not been explicitly destroyed with a function like
  16 // Clutter.Actor.destroy(), the reference count of the StLabel will drop to 0
  17 // and the GObject will be freed
  18 myBox.remove_all_children();

Use After Free

Most functions that affect memory management are not available in GJS, but there are some exceptions. Functions like Gtk.Widget.destroy() and Clutter.Actor.destroy() are common functions you may use, which will force the GObject to be freed as though its reference count had dropped to 0. What it won't do is stop a JS variable from tracing that GObject.

Attempting to access a GObject after it has been finalized (freed) is a programmer's error that is not uncommon to see in extensions. Trying to use such an object will raise a critical error and print a stack trace to the log. Below is an simple example of how you can continue to trace a reference to a GObject that has already been freed:

   1 'use strict';
   2 
   3 const St = imports.gi.St;
   4 
   5 
   6 let myLabel = new St.Label({
   7     text: "Some Text"
   8 });
   9 
  10 // Here we are FORCING the GObject to be freed, as though it's reference count
  11 // had dropped to 0.
  12 myLabel.destroy();
  13 
  14 // Even though this GObject is being traced from `myLabel`, trying to call
  15 // its methods or access its properties will raise a critical error because all
  16 // it's resources have been freed.
  17 log(myLabel.text);
  18 
  19 // In this case we are in the top-level scope, so the proper thing to do is
  20 // `null` the variable to allow garbage collector to free the JS wrapper.
  21 myLabel = null;

Leaking References

Although memory is managed for you by GJS, remember that this is done by tracing to variables. If you lose track of a variable, the ID for a signal or source callback you will leak that reference.

Scope

The easiest way to leak references is to overwrite a variable or let it fall out of scope. If this variable points to a GObject with a positive reference count that you are responsible for freeing, you will effectively leak its memory.

In this example we leak our references when enable() returns. Because we no longer have a direct reference to indicator or the ID of it in the status area, we have lost our ability to destroy indicator when disable() is called.

   1 'use strict';
   2 
   3 const GLib = imports.gi.GLib;
   4 
   5 const Main = imports.ui.Main;
   6 const PanelMenu = imports.ui.panelMenu;
   7 
   8 
   9 function enable() {
  10     let indicator = new PanelMenu.Button(0.0, 'MyIndicator', false);
  11     let randomId = GLib.uuid_string_random();
  12 
  13     Main.panel.addToStatusArea(randomId, indicator);
  14 }
  15 
  16 function disable() {
  17     // When `enable()` returned, both `indicator` and `randomId` fell out of
  18     // scope and got collected. However, the panel status area is still holding
  19     // a reference to the PanelMenu.Button GObject.
  20     //
  21     // Each time the extension is disabled/enabled (eg. screen locks/unlocks)
  22     // a new, unremovable indicator will be added to the panel
  23 }

Although the principle of the code below is sound, we are leaking a reference to a GObject we are responsible for (and thus memory). The leak below is fairly easy to spot; what's important is how and why that reference was leaked. Mistakes like these are often easier to make and harder to track down.

   1 'use strict';
   2 
   3 const GLib = imports.gi.GLib;
   4 
   5 const Main = imports.ui.Main;
   6 const PanelMenu = imports.ui.panelMenu;
   7 
   8 
   9 let indicators = {};
  10 
  11 
  12 function enable() {
  13     let indicator1 = new PanelMenu.Button(0.0, 'MyIndicator1', false);
  14     let indicator2 = new PanelMenu.Button(0.0, 'MyIndicator2', false);
  15 
  16     Main.panel.addToStatusArea(GLib.uuid_string_random(), indicator1);
  17     Main.panel.addToStatusArea(GLib.uuid_string_random(), indicator2);
  18 
  19     indicators['MyIndicator1'] = indicator1;
  20     indicators['MyIndicator1'] = indicator2;
  21 }
  22 
  23 function disable() {
  24     for (let [name, indicator] of Object.entries(indicators)) {
  25         indicator.destroy();
  26     }
  27 
  28     indicators = {};
  29 }

Main Loop Sources

A very common way of leaking GSource's is recursive (repeating) sources added to the GLib event loop. These are usually repeating timeout loops, used to update something in the UI.

In the example below, the ID required to remove the GSource from the main loop has been lost and the callback will continue to be invoked even after the object has been destroyed.

So in this case, the leak is caused by the main loop holding a reference to the GSource, while the programmer has lost their ability to remove it. When the source callback is invoked, it will try to access the object after it has been destroyed, causing a critical error.

   1 'use strict';
   2 
   3 const GLib = imports.gi.GLib;
   4 const GObject = imports.gi.GObject;
   5 
   6 const PanelMenu = imports.ui.panelMenu;
   7 
   8 
   9 var MyIndicator = GObject.registerClass({
  10     GTypeName: 'MyIndicator',
  11 }, class MyIndicator extends PanelMenu.Button {
  12 
  13     startUpdating() {
  14         // When `startUpdating()` returns, we will lose our reference to
  15         // `sourceId`, so we will be unable to remove it from the the main loop
  16         let sourceId = GLib.timeout_add_seconds(
  17             GLib.PRIORITY_DEFAULT,
  18             5,
  19             this.update.bind(this)
  20         );
  21     }
  22 
  23     update() {
  24         // If the object has been destroyed, this will cause a critical error.
  25         // Note that the use of Function.bind() is what allows `this` to be
  26         // traced to the JavaScript object.
  27         this.visible = this.visible;
  28 
  29         // Returning `true` or `GLib.SOURCE_CONTINUE` causes the GSource to
  30         // persist, so the callback will run when the next timeout is reached
  31         return GLib.SOURCE_CONTINUE;
  32     }
  33 
  34     _onDestroy() {
  35         // We don't have a reference to `sourceId` so we can't remove the
  36         // source from the loop. We should have assigned the source ID to an
  37         // object property in `_init()` like `this._timeoutId`
  38 
  39         super._onDestroy();
  40     }
  41 });
  42 
  43 let indicator = null;
  44 
  45 function enable() {
  46     indicator = new MyIndicator();
  47 
  48     // Each time the extension is enabled, a new GSource will added to the main
  49     // main loop by this function
  50     indicator.startUpdating();
  51 }
  52 
  53 function disable() {
  54     indicator.destroy();
  55     indicator = null;
  56 
  57     // Even though we have destroyed the GObject and stopped tracing it from the
  58     // `indicator` variable, the GSource callback will continue to be invoked
  59     // every 5 seconds.
  60 }

Signal Callbacks

Like main loop sources, whenever a signal is connected it returns a handler ID. Failing to remove sources or disconnect signals can result in "use-after-free" scenarios, but it's also possible to trace variables and leak references in a callback.

In the example below, the callback for the changed::licensed signal is tracing the agent dictionary, which is why the callback continues to work after constructor() returns. In fact, even after we set myObject to null in disable(), the dictionary is still be traced from within the signal callback.

   1 'use strict';
   2 
   3 const ExtensionUtils = imports.misc.extensionUtils;
   4 
   5 
   6 let settings = ExtensionUtils.getSettings();
   7 let myObject;
   8 
   9 class MyObject {
  10     constructor() {
  11         let agent = {
  12             'family': 'Bond',
  13             'given': 'James',
  14             'codename': '007',
  15             'licensed': true
  16         };
  17 
  18         // Here is where we should have retained the handler ID
  19         let id = settings.connect('changed::licensed', (settings, key) => {
  20 
  21             // Here we are tracing a reference to the `agent` dictionary. Since
  22             // the signal handler ID is leaked, it is never disconnected and the
  23             // `agent` dictionary is leaked
  24             agent.licensed = settings.get_boolean(key);
  25         });
  26     }
  27 }
  28 
  29 function enable() {
  30     myObject = new MyObject();
  31 }
  32 
  33 function disable() {
  34     myObject.destroy();
  35     myObject = null;
  36 }

Cairo

Cairo is an exception in GJS, in that it is necessary to manually free the memory of a CairoContext at the end of a drawing method. This is simple to do, but forgetting to do it can leak a lot of memory because widgets can be redrawn quite often.

   1 'use strict';
   2 
   3 const GLib = imports.gi.GLib;
   4 const GObject = imports.gi.GObject;
   5 const St = imports.gi.St;
   6 
   7 
   8 let area = new St.DrawingArea({
   9     width: 32,
  10     height: 32
  11 });
  12 
  13 area.connect('repaint', (area) => {
  14     // Get the cairo context
  15     let cr = area.get_context();
  16 
  17     // Do some drawing with cairo
  18 
  19     // Explicitly tell Cairo to free the context memory
  20     cr.$dispose();
  21 });

Getting Help

Attic/GnomeShell/Extensions/TipsOnMemoryManagement (last edited 2021-04-12 03:03:31 by AndyHolmes)