Electronの内部: 弱参照
ガベージコレクションを備えた言語として、JavaScriptはユーザーが手動でリソースを管理する必要性をなくします。しかし、Electronはこの環境をホストしているため、メモリリークとリソースリークの両方を回避するために非常に注意する必要があります。
この記事では、弱参照の概念と、Electronでリソースを管理するためにどのように使用されるかについて説明します。
弱参照
JavaScriptでは、オブジェクトを変数に割り当てるたびに、オブジェクトへの参照を追加しています。オブジェクトへの参照がある限り、オブジェクトは常にメモリに保持されます。オブジェクトへのすべての参照がなくなると、つまり、オブジェクトを格納している変数がなくなると、JavaScriptエンジンは次回のガベージコレクションでメモリを回収します。
弱参照とは、オブジェクトへの参照であり、ガベージコレクションされるかどうかに影響を与えることなくオブジェクトを取得できます。また、オブジェクトがガベージコレクションされるときに通知を受け取ります。これにより、JavaScriptでリソースを管理することが可能になります。
Electronの`NativeImage`クラスを例として、`nativeImage.create()` APIを呼び出すたびに、`NativeImage`インスタンスが返され、画像データがC++に格納されます。インスタンスの使用が完了し、JavaScriptエンジン(V8)がオブジェクトをガベージコレクションすると、C++のコードが呼び出されてメモリ内の画像データが解放されるため、ユーザーが手動で管理する必要はありません。
もう1つの例は、ウィンドウが消える問題です。これは、ウィンドウへのすべての参照がなくなると、ウィンドウがガベージコレクションされる様子を視覚的に示しています。
Electronでの弱参照のテスト
生のJavaScriptでは、弱参照を直接テストする方法はありません。これは、言語に弱参照を割り当てる方法がないためです。弱参照に関連するJavaScriptの唯一のAPIはWeakMapですが、弱参照キーのみを作成するため、オブジェクトがガベージコレクションされた Zeitpunkt を知ることはできません。
v0.37.8より前のバージョンのElectronでは、内部の`v8Util.setDestructor` APIを使用して弱参照をテストできます。これにより、渡されたオブジェクトに弱参照が追加され、オブジェクトがガベージコレクションされるときにコールバックが呼び出されます。
// Code below can only run on 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".
内部の`gc`関数を公開するには、`--js-flags="--expose_gc"`コマンドスイッチを付けてElectronを起動する必要があることに注意してください。
このAPIは、V8が実際にはデストラクタでJavaScriptコードの実行を許可しておらず、後のバージョンではランダムなクラッシュが発生するため、後のバージョンで削除されました。
`remote`モジュールにおける弱参照
C++でネイティブリソースを管理するだけでなく、ElectronはJavaScriptリソースを管理するためにも弱参照を必要とします。例として、Electronの`remote`モジュールがあります。これは、リモートプロシージャコール(RPC)モジュールであり、レンダラープロセスからメインプロセス内のオブジェクトを使用できます。
`remote`モジュールにおける重要な課題の1つは、メモリリークを回避することです。ユーザーがレンダラープロセスでリモートオブジェクトを取得すると、`remote`モジュールは、レンダラープロセス内の参照がなくなるまで、オブジェクトがメインプロセスで存続し続けることを保証する必要があります。さらに、レンダラープロセスに参照がなくなったときに、オブジェクトがガベージコレクションできることも確認する必要があります。
たとえば、適切に実装しないと、次のコードはすぐにメモリリークを引き起こします。
const { remote } = require('electron');
for (let i = 0; i < 10000; ++i) {
remote.nativeImage.createEmpty();
}
`remote`モジュールにおけるリソース管理はシンプルです。オブジェクトがリクエストされるたびに、メインプロセスにメッセージが送信され、Electronはオブジェクトをマップに格納してIDを割り当て、IDをレンダラープロセスに送り返します。レンダラープロセスでは、`remote`モジュールがIDを受信してプロキシオブジェクトでラップし、プロキシオブジェクトがガベージコレクションされると、メインプロセスにメッセージが送信されてオブジェクトが解放されます。
`remote.require` APIを例として、簡略化された実装は次のようになります。
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;
};
メインプロセスでは
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];
});
弱参照値を持つマップ
前の単純な実装では、`remote`モジュール内のすべての呼び出しは、メインプロセスから新しいリモートオブジェクトを返し、各リモートオブジェクトはメインプロセス内のオブジェクトへの参照を表します。
設計自体は問題ありませんが、問題は、同じオブジェクトを受信するための複数の呼び出しがある場合、複数プロキシオブジェクトが作成され、複雑なオブジェクトの場合、メモリ使用量とガベージコレクションに大きな負担がかかることです。
たとえば、次のコード
const { remote } = require('electron');
for (let i = 0; i < 10000; ++i) {
remote.getCurrentWindow();
}
最初にプロキシオブジェクトの作成に多くのメモリを使用し、次にCPU(中央処理装置)を占有してガベージコレクションを行い、IPCメッセージを送信します。
明らかな最適化は、リモートオブジェクトをキャッシュすることです。同じIDを持つリモートオブジェクトが既に存在する場合、新しいオブジェクトを作成する代わりに、以前のリモートオブジェクトが返されます。
これは、JavaScriptコアのAPIでは不可能です。通常のマップを使用してオブジェクトをキャッシュすると、V8がオブジェクトをガベージコレクションできなくなりますが、WeakMapクラスはオブジェクトを弱キーとしてのみ使用できます。
これを解決するために、値が弱参照であるマップ型が追加されました。これは、IDを持つオブジェクトをキャッシュするのに最適です。これで、`remote.require`は次のようになります。
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
}
`remoteObjectCache`はオブジェクトを弱参照として格納するため、オブジェクトがガベージコレクションされるときにキーを削除する必要がないことに注意してください。
ネイティブコード
Electronの弱参照のC++コードに興味のある方のために、次のファイルにあります。
`setDestructor` API
`createIDWeakMap` API