はじめに
合同会社EXNOA プラットフォームシステム部の知久です。
普段はNativeアプリチームのメンバーとして、EXNOAでサービスしているDMMGamePlayerからAPIサーバまで開発を担当しています。
この記事では、EXNOAで作成したElectronアプリケーションの高速化に関するノウハウについて紹介します。
EXNOAでは過去の5年間運用したDMMGamePlayerというWindowsまたはMacOS上で動作するアプリケーションのリプレースのため、新たにデスクトップアプリケーションを開発しています。この開発作業の間に起こった紆余曲折について紹介したいと思います。
DMMGamePlayerとは
DMMGamePlayerとはゲームのランチャーです。DMM Gamesでサービスしているゲームをユーザに提供するためのデスクトップアプリケーションです。
ユーザが購入または利用登録して遊べるようにしたゲームをインストールしたり、ゲームを起動したりします。
必要な機能
ゲームのランチャーに必要な要素を大雑把に定義すると
- ゲームがダウンロード・インストールできること
- ゲームの起動ができること
- ログインなどの認証機能
です。
ブラウザではユーザのファイル操作はできないため、主な機能はユーザのPC内にあるゲーム関連ファイルの操作になります。
技術選択の理由
WindowsでもMacOSでも動作するデスクトップアプリケーションを作るにあたっては、方法はいろいろあります。
スタンダードにC++による開発でも、中間コードを生成する言語(JavaとかC#)でも良いですが、私たちはElectronを選びました。 主な理由としては
- WindowsとMacOSで二重管理となるコードを書きたくない
- アプリ作成から継続的にアップデートして配信していくフローがサポートされている
- 部署の大半がWEBエンジニアである
などです。
ElectronはWindowsとMacOS両方に対応してアプリを生成できるため、ひとつのコードで両方のOSに対処できます。
ElectronはWEB技術でデスクトップアプリケーションを作れるため、ソースコードを弄れるエンジニアがネイティブアプリケーションを開発している人に限られる、 という事態を回避することができ、WEBページと同じような感覚でアプリの画面を作成できます。
実際、Electronを採用したことで画面作成(HTMLとCSSで構成されているもの)をWEB側のフロントエンド開発のメンバーに任せることができたので、 アプリのメインプロセス(処理)を実装するメンバーとレンダープロセス(画面)を作るメンバーで分業もできました。
また、WEB開発で重宝されているReactも採用しており、多人数で開発するため型安全を考慮してTypeScriptも導入しています。
これらのメリットにより、以前は一人で2ヶ月かけて開発した機能が、画面とビジネスロジックの開発者で分業することによって3週間でできたこともありました。
苦手な分野
様々なメリットがあるElectronですが、苦手な分野もあります。それは並列処理です。
JavaScriptは他の言語でいうマルチスレッド処理が苦手なため、パフォーマンスが非常に悪くなってしまう処理が散見されます。
ゲームのランチャーの機能において並列処理は重要です。
ゲームを構成するファイルは何千ファイルにもなることがあり、 それらを順次処理をしていると非常にレスポンスの悪いアプリケーションになってしまい、ゲームを起動するまでに時間がかかるなど利便性の低下にも繋がります。
Electronの中にGo
Electronでもメインプロセスの処理で並列処理を行いたいと考えた私は、nodejsのNative Addonを利用してパフォーマンスアップを図ることにしました。
Native Addonとは一言でいうとdllです。nodejsはDLLのバイナリファイルをランタイムで呼び出して、その関数を利用することができます。
一般的にdllファイルはCまたはC++を利用して作るのですが、それだと「部署の大半がWEBエンジニアである」という問題をクリアできません。
CやC++は、WEB開発者はほとんど使わないので学習コストがかかります。できれば学習コスト0でいきなり開発に入りたいものです。
そこで、dllの作成にGo言語を採用しました。Go言語は言わずと知れたWebアプリケーションの作成で利用されている汎用言語でパフォーマンスが良く、言語の特徴であるgoroutineは強力に並列処理の実装をサポートします。また、Go言語はCGOというライブラリを利用してCと互換性のあるAPIを実装することができ、build時にbuildmodeオプションを付けることによってdllバイナリを生成することができます。
go build -buildmode=c-shared -o mylibrary.dll |
dllを作成して、メインプロセスの処理の一部をdllが行い、レンダープロセス(画面)は今までどおりTypeScriptで行うようにしました。
dllをMacOSで呼ぶにはElectronアプリ本体と同じ署名を持っていないとダメだとか、dllはasarの外に置く必要があるとか、Electronにdllを組み込むには悪戦苦闘しましたが、最終的には Electron でバックグラウンドにGoを利用したアプリができあがりました。
dllにある関数をtypescriptから呼び出せるように定義する
import * as ffi from 'ffi-napi';
const f: any = ffi.Library(dllPath, {
Init: ['void', ['string']],
LogInfo: ['void', ['string']],
LogError: ['void', ['string']],
StartGrpc: ['void', ['int']],
StopGrpc: ['void', []],
SetUser: ['void', ['string', 'string', 'string', 'string', 'string', 'string', 'string']],
FileDownload: ['string', ['string', 'string']],
FileDownloadBulk: ['string', ['string']],
});
Goのdllのエントリーポイント定義部分の一部
package main
import "C"
func main() {
}
//export FileDownload
func FileDownload(url, localPath *C.char) *C.char {
result := fileDownload(C.GoString(url), C.GoString(localPath))
return C.CString(result)
}
並列処理の効果
ElectronアプリにGo言語で作ったdllを入れました。これにより並列処理ができるようになり、重たいバックグラウンド処理も軽量化することができました。
この図は、実際にサービスされているあるゲームのダウンロードにかかる時間を計測したものです。
TypeScriptのみでゲームを構成するファイル全てを逐次ダウンロードしたものと、TypeScriptからダウンロードを並列処理するdllの処理を呼び出したものの比較です。
dll側のダウンロードはTypescriptからdllを呼び出しているので呼び出しのオーバヘッドがあるにもかかわらず複数のファイルを一斉にダウンロードする場合、dllの方が大きくTypeScriptの処理を上回っていました。
まとめ
今回の記事ではElectronにGoで作ったdllを組み込んで並列処理を行えるようにした高速化を紹介しました。
ElectronにGoのdllが利用できる利点は大きく、現在リリースされているバージョンのアプリでは他のアプリで利用したGoのライブラリも組み込まれています。
もちろんGoでできたサーバと共通のライブラリを利用して開発をすることも可能なので、今後は実行速度によるパフォーマンスだけでなくアプリの開発の速度の高速化にも寄与してくれそうです。
ることによってdllバイナリを生成することができます。
|
Go言語でdllを出力するbuildmodeオプション