As a language with garbage collection, JavaScript frees users from managing
resources manually. But because Electron hosts this environment, it has to be
very careful avoiding both memory and resources leaks.
This post introduces the concept of weak references and how they are used to
manage resources in Electron.
Weak references
In JavaScript, whenever you assign an object to a variable, you are adding a
reference to the object. As long as there is a reference to the object, it will
always be kept in memory. Once all references to the object are gone, i.e. there
are no longer variables storing the object, the JavaScript engine will recoup
the memory on next garbage collection.
A weak reference is a reference to an object that allows you to get the object
without effecting whether it will be garbage collected or not. You will also get
notified when the object is garbage collected. It then becomes possible to
manage resources with JavaScript.
Using the NativeImage
class in Electron as an example, every time you call the
nativeImage.create()
API, a NativeImage
instance is returned and it is
storing the image data in C++. Once you are done with the instance and the
JavaScript engine (V8) has garbage collected the object, code in C++ will be
called to free the image data in memory, so there is no need for users manage
this manually.
Another example is the window disappearing problem, which
visually shows how the window is garbage collected when all the references to it
are gone.
Testing weak references in Electron
There is no way to directly test weak references in raw JavaScript since the
language doesn't have a way to assign weak references. The only API in
JavaScript related to weak references is WeakMap, but since it only
creates weak-reference keys, it is impossible to know when an object has been
garbage collected.
In versions of Electron prior to v0.37.8, you can use the internal
v8Util.setDestructor
API to test weak references, which adds a weak reference
to the passed object and calls the callback when the object is garbage collected:
var v8Util = process.atomBinding('v8_util');
var object = {};
v8Util.setDestructor(object, function () {
console.log('The object is garbage collected');
});
object = undefined;
gc();
Note that you have to start Electron with the --js-flags="--expose_gc"
command
switch to expose the internal gc
function.
The API was removed in later versions because V8 actually does not allow running
JavaScript code in the destructor and in later versions doing so would cause
random crashes.
Weak references in the remote
module
Apart from managing native resources with C++, Electron also needs weak
references to manage JavaScript resources. An example is Electron's remote
module, which is a Remote Procedure Call (RPC) module
that allows using objects in the main process from renderer processes.
One key challenge with the remote
module is to avoid memory leaks. When users
acquire a remote object in the renderer process, the remote
module must
guarantee the object continues to live in the main process until the references
in the renderer process are gone. Additionally, it also has to make sure the
object can be garbage collected when there are no longer any reference to it in
renderer processes.
For example, without proper implementation, following code would cause memory
leaks quickly:
const { remote } = require('electron');
for (let i = 0; i < 10000; ++i) {
remote.nativeImage.createEmpty();
}
The resource management in the remote
module is simple. Whenever an object is
requested, a message is sent to the main process and Electron will store
the object in a map and assign an ID for it, then send the ID back to the
renderer process. In the renderer process, the remote
module will receive
the ID and wrap it with a proxy object and when the proxy object is garbage
collected, a message will be sent to the main process to free the object.
Using remote.require
API as an example, a simplified implementation looks
like this:
remote.require = function (name) {
const meta = ipcRenderer.sendSync('REQUIRE', name);
const object = metaToValue(meta);
v8Util.setDestructor(object, function () {
ipcRenderer.send('FREE', meta.id);
});
return object;
};
In the main process:
const map = {};
const id = 0;
ipcMain.on('REQUIRE', function (event, name) {
const object = require(name);
map[++id] = object;
event.returnValue = valueToMeta(id, object);
});
ipcMain.on('FREE', function (event, id) {
delete map[id];
});
Maps with weak values
With the previous simple implementation, every call in the remote
module will
return a new remote object from the main process, and each remote object
represents a reference to the object in the main process.
The design itself is fine, but the problem is when there are multiple calls to
receive the same object, multiple proxy objects will be created and for
complicated objects this can add huge pressure on memory usage and garbage
collection.
For example, the following code:
const { remote } = require('electron');
for (let i = 0; i < 10000; ++i) {
remote.getCurrentWindow();
}
It first uses a lot of memory creating proxy objects and then occupies
the CPU (Central Processing Unit) for garbage collecting them and sending IPC
messages.
An obvious optimization is to cache the remote objects: when there is already
a remote object with the same ID, the previous remote object will be returned
instead of creating a new one.
This is not possible with the API in JavaScript core. Using the normal map
to cache objects will prevent V8 from garbage collecting the objects, while the
WeakMap class can only use objects as weak keys.
To solve this, a map type with values as weak references is added, which is
perfect for caching objects with IDs. Now the remote.require
looks like
this:
const remoteObjectCache = v8Util.createIDWeakMap()
remote.require = function (name) {
...
if (remoteObjectCache.has(meta.id))
return remoteObjectCache.get(meta.id)
...
remoteObjectCache.set(meta.id, object)
return object
}
Note that the remoteObjectCache
stores objects as weak references, so there
is no need to delete the key when the object is garbage collected.
Native code
For people interested in the C++ code of weak references in Electron, it can be
found in following files:
The setDestructor
API:
The createIDWeakMap
API: