どうも。エンジニアの篰です。普段はクライアント側の開発業務をやっています。
運用型のアプリは新しい機能を開発していくのも大事なことなのですが、機能が大きくなるにつれ、どんどんと動作が重くなるという問題がつきものです。
自らアプリを触っていても動作の重さが気になるところはしばしば見つかるもので、そういうときはユーザの皆さんも同じことを思っているのは明らかなので、なるべく早くに改善を試みることが多いです。
とはいえ、機能が大きくなるにつれコードも肥大化していることがあり、勘だけではなにが悪さをしているかはっきりわかることは少ないです。
そこで私達が普段使っているのは開発環境に合わせたパフォーマンス測定ツールです。今回はその中でも私達のプロダクトで使われているXcodeに標準搭載されているツールであるInstrumentsの中のひとつ、Time Profilerの使い方についてゲームエンジンのCocos2d-xを交えてお話しようと思います。
環境
- macOS 10.15.7
- Xcode Version 12.0.1
- Cocos2d-x v3.17.2
準備
すでにXcodeでCocos2d-xによるプロジェクトを作成している方は読み飛ばしていただいてかまいません。
まだの方はこちらを参考に環境変数の設定までおこなってください。その後、今回はTime Profilerの動作確認を簡単に行うためにMacアプリ用プロジェクトでテストを行いたいので、CMakeを使用しMacアプリ用プロジェクトを作成します。
cd cocos2d-x
mkdir mac-build && cd mac-build
cmake .. -GXcode
測定
テンプレートプロジェクトのCocos2d-x.xcodeproj
が出来上がっているのでさっそく開いてみます。いくつか、ターゲットが作られていますが、今回は一番シンプルなcpp-empty-testを利用しますのでさっそくビルドしてみましょう。成功すれば以下のようにアプリが立ち上がります。
ビルドさえ通ればすぐにTime Profilerを使用することができます。メニューバーのProductからProfileを選択しますとInstruments用のビルドを開始することができます。(または、RUNボタンを長押しすることで選択することもできます)
ビルドが終わると次のダイアログが出ます。私はLeaks(メモリリークを見つけるのに便利)やTime Profiler(実行時間がかかっているところを探すのに便利)をよく使用します。Time Profilerを選択肢して次に進みましょう。メイン画面が立ち上がります。
ここで画像左上の赤い丸のRecordボタンを押すと測定が開始されるのですが、今の状態だと測定値がわかりにくいのでHelloWorldScene.cppを以下のように変更します。
HelloWorldScene.h
class HelloWorld : public cocos2d::Scene
{
private:
bool flag = true;
...
};
HelloWorldScene.cpp
void HelloWorld::menuCloseCallback(Ref* sender)
{
auto size = Director::getInstance()->getWinSize();
if (auto node = this->getChildByName("nodes"))
node->removeFromParent();
auto layer = Layer::create();
layer->setName("nodes");
this->addChild(layer);
for (int i = 0; i < 200; i++)
{
for (int j = 0; j < 200; j++)
{
auto node = Node::create();
node->setPosition(size.width / 100 * i, size.height / 100 * j);
layer->addChild(node);
if (flag)
{
auto sprite1 = Sprite::create("CloseNormal.png");
node->addChild(sprite1);
}
else
{
auto sprite2 = Sprite::create("CloseSelected.png");
node->addChild(sprite2);
}
}
}
flag = !flag;
}
ボタンを押すと大量の画像を切替え表示する処理です。これをプロファイラーで見てみましょう。
画像で示されているアプリ画面上の右上のボタンを5回押しているのですが、Time Profilerのグラフの部分で急激に負荷が大きくなっている部分からが該当の処理になります。こちらでかかっている処理の時間を見ましょう。
まずコールツリーを見やすくするために以下のように設定します。余計な関数の表示を省くことができます。
実際にツリーを開いてみると以下のようになっています。
どうもmainLoop()とpollEvents()が時間を食っているようですが、mainLoop()は描画更新処理なので今回は無視してpollEvents()のほうを見ていきます。
選択していくと処理を変更したmenuCloseCallback()の表示があることを確認することができます。下画像の赤線部分のとおり、1.84秒ほど時間がかかっていることがわかります。
関数をダブルクリックすると実際のウェイトを確認することもできます。意図的ではありますが、やはりSpriteの追加と解放部分に負荷がかかっていることを確認することができます。
改善
これを少し改善してみます。ノードの削除処理と追加処理に時間がかかっていることがわかるので、コードを以下のように変更します。
HelloWorldScene.h
class HelloWorld : public cocos2d::Scene
{
private:
bool flag = true;
cocos2d::Layer* node1 = nullptr;
cocos2d::Layer* node2 = nullptr;
...
};
HelloWorldScene.cpp
void HelloWorld::menuCloseCallback(Ref* sender)
{
auto size = Director::getInstance()->getWinSize();
if (!layer1)
{
layer1 = Layer::create();
this->addChild(layer1);
for (int i = 0; i < 200; i++)
{
for (int j = 0; j < 200; j++)
{
auto node = Node::create();
node->setPosition(size.width / 100 * i, size.height / 100 * j);
layer1->addChild(node);
auto sprite2 = Sprite::create("CloseSelected.png");
node->addChild(sprite2);
}
}
}
if (!layer2)
{
layer2 = Layer::create();
this->addChild(layer2);
for (int i = 0; i < 200; i++)
{
for (int j = 0; j < 200; j++)
{
auto node = Node::create();
node->setPosition(size.width / 100 * i, size.height / 100 * j);
layer2->addChild(node);
auto sprite = Sprite::create("CloseNormal.png");
node->addChild(sprite);
}
}
}
layer1->setVisible(flag);
layer2->setVisible(!flag);
flag = !flag;
}
最初に2パターンのノードを先に生成しておき、ボタンを押すときには表示非表示を切り替えるだけにしました。これをプロファイラで見ると以下のようになっています。
ご覧の通り、menuCloseCallback()で1.84秒かかっていたのが0.55秒程度まで改善することができました。このようにTime Profilerを使用すると、具体的にコードのどこに時間がかかっているかわかり、改善の考案がやりやすくなります。
まとめ
今回はパフォーマンス改善ツールとしてXcodeのInstrumentsのひとつである、Time Profilerを紹介いたしました。実験で用いたコードは意図的に処理を重くしているのでツールを使わずともわかりやすいですが、肥大化したコードに対して使用すると、自分では思い当たらなかった部分が重くなっているなどがすぐにわかり非常に便利です。
Instrumentsには他にもいろいろ測定する機能がありますが、実行時間を測定するTime Profilerはパフォーマンスを改善するという点においてもっとも有用ですので、Xcodeでプロジェクトを作成している方はぜひ使用してみてください。