Electron の内部構造:メッセージループの統合
これは、Electron の内部構造を説明するシリーズの最初の投稿です。この投稿では、Electron での Node のイベントループと Chromium の統合について紹介します。
GTK+バインディング用のnode-guiやQTバインディング用のnode-qtなど、GUIプログラミングにNodeを使用する試みが数多くありました。しかし、GUIツールキットには独自のメッセージループがあり、Nodeはlibuvを独自のイベントループに使用しており、メインスレッドは一度に1つのループしか実行できないため、どれも本番環境では機能しません。そのため、NodeでGUIメッセージループを実行する一般的な方法は、非常に短い間隔でタイマーでメッセージループをポンプすることです。これにより、GUIインターフェイスの応答が遅くなり、多くのCPUリソースが占有されます。
Electronの開発中に、同じ問題に遭遇しました。ただし、逆の方法で、NodeのイベントループをChromiumのメッセージループに統合する必要がありました。
メインプロセスとレンダラープロセス
メッセージループの統合の詳細に入る前に、まずChromiumのマルチプロセスアーキテクチャについて説明します。
Electronには、メインプロセスとレンダラープロセスの2種類のプロセスがあります(これは実際には非常に簡略化されたものであり、完全なビューについてはマルチプロセスアーキテクチャをご覧ください)。メインプロセスはウィンドウの作成などのGUI作業を担当し、レンダラープロセスはWebページの実行とレンダリングのみを処理します。
Electronでは、JavaScriptを使用してメインプロセスとレンダラープロセスの両方を制御できます。つまり、Nodeを両方のプロセスに統合する必要があります。
Chromiumのメッセージループをlibuvに置き換える
私の最初の試みは、Chromiumのメッセージループをlibuvで再実装することでした。
レンダラープロセスのメッセージループはファイル記述子とタイマーのみをリッスンしていたため、簡単でした。libuvとのインターフェイスを実装するだけで済みました。
しかし、メインプロセスにとっては、はるかに困難でした。各プラットフォームには、独自の種類のGUIメッセージループがあります。macOS ChromiumはNSRunLoop
を使用し、Linuxはglibを使用します。ネイティブGUIメッセージループから基になるファイル記述子を抽出して、それらを反復処理のためにlibuvにフィードするハックをたくさん試しましたが、それでも機能しないエッジケースに遭遇しました。
そこで最後に、GUIメッセージループを短い間隔でポーリングするタイマーを追加しました。その結果、プロセスは一定のCPU使用率を占め、特定の操作に長い遅延が発生しました。
別のスレッドでNodeのイベントループをポーリングする
libuvが成熟するにつれて、別のアプローチが可能になりました。
バックエンドfdの概念がlibuvに導入されました。これは、libuvがイベントループをポーリングするファイル記述子(またはハンドル)です。したがって、バックエンドfdをポーリングすることで、libuvに新しいイベントがあるときに通知を受け取ることができます。
そこで、Electronでは、バックエンドfdをポーリングする別のスレッドを作成しました。libuv APIではなく、ポーリングにシステムコールを使用していたため、スレッドセーフでした。そして、libuvのイベントループに新しいイベントがあるときはいつでも、Chromiumのメッセージループにメッセージが投稿され、libuvのイベントはメインスレッドで処理されます。
このようにして、ChromiumとNodeにパッチを当てることを避け、メインプロセスとレンダラープロセスの両方で同じコードが使用されました。
コード
メッセージループの統合の実装は、electron/atom/common/
の下のnode_bindings
ファイルにあります。Nodeを統合したいプロジェクトに簡単に再利用できます。
更新:実装はelectron/shell/common/node_bindings.cc
に移動しました。