Electron Internals: Referencias débiles
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.
Otro ejemplo es el problema desaparecido de la ventana, cuál muestra vistivamente cómo se recolecta la basura cuando todas las referencias a ella se han ido.
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:
// El código a continuación solo puede ejecutarse en Electron < v0.37.8.
var v8Util = process.atomBinding('v8_util');
var object = {};
v8Util.setDestructor(object, function () {
console.log('The object is garbage collected');
});
// Remove all references to the object.
object = undefined;
// Manually starts a GC.
gc();
// Console prints "The object is garbage collected".
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. Un ejemplo es el módulo remoto
de Electron, que es un módulo Llamada de Procedimiento Remoto (RPC) que permite usar objetos en el proceso principal de procesos de renderizado.
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. Cada vez que un objeto es solicitado, un mensaje es enviado al main process y Electron guardara el objeto en un mapa y le asignara un ID, luego envia de vuelta el ID al 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) {
// Tell the main process to return the metadata of the module.
const meta = ipcRenderer.sendSync('REQUIRE', name);
// Create a proxy object.
const object = metaToValue(meta);
// Tell the main process to free the object when the proxy object is garbage
// collected.
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);
// Add a reference to the object.
map[++id] = object;
// Convert the object to metadata.
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) {
// Tell the main process to return the meta data of the module.
...
if (remoteObjectCache.has(meta.id))
return remoteObjectCache.get(meta.id)
// Create a proxy object.
...
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: