標準Profilerだけに頼らないUnityのプロファイリングに挑んでみる

はじめに

こんにちは
KLab Advent Calendar8日目の記事です。
KLabGames事業部エンジニアの@knsh14です。

みなさんはUnityで開発をする際にどのようにしてアプリのパフォーマンスを計測していますか?
今回はある案件で今までと違うメモリ監視の方法を使ってみたら、デバッグが捗った話を紹介しようと思います。

KLabgamesでは幾つかのゲームでUnityを用いて開発を行っています。
よりよい品質でユーザ様に遊んでいただくために、常にパフォーマンス・チューニングを行っています。
その中でも各シチュエーションでのメモリ使用量を計測し、改善することは低スペック端末などで動作させるために必須です。
ですが、Unity標準のProfilerでは適切にメモリ使用量を計測できない問題がありました。

今までの方法

これまでは以下の様なコードでメモリ使用量を取得して画面に表示していました。

uint totalUsed = Profiler.GetTotalAllocatedMemory();
uint totalSize = Profiler.GetTotalReservedMemory();

System.Text.StringBuilder text = new System.Text.StringBuilder();

text.Append((totalUsed / (1024f * 1024f)).ToString("0.0"));
text.Append("/");
text.Append((totalSize / (1024f * 1024f)).ToString("0.0"));
text.Append(" MB(");
text.Append((100f * totalUsed / totalSize).ToString("0"));
text.Append("%) ");

これをOnGUIで表示するなり、NGUIのラベルに出力するなりしていました。

シンプルですね。
ここで使っているメソッドを調べてみましょう。
Profiler.GetTotalAllocatedMemory()
Profiler.GetTotalReservedMemory()

公式のリファレンスにもなにも書いてない...
実際に動かした結果とプロファイラを見比べてみると
device-screenshot
unity-profiler-screenshot
instruments-screenshot

全ての計測値にかなり開きがありますね。これでは計測の目安とするには不十分です。

ネイティブプラグインを書いてみる

ではどうすればいいのでしょうか?

僕らが見たいメモリ使用量は各OSから見てUnityAppがどれほどのメモリを使っているかというものです。
なのでUnityから利用できるネイティブプラグインを書いてそこから各OSに問い合わせればいいわけです。

こうして書いたネイティブプラグインコードがこちらです。

iOSの場合

#import <Foundation/Foundation.h>
#import <mach/mach.h>

unsigned int getUsedMemorySize() {
    struct task_basic_info basic_info;
    mach_msg_type_number_t t_info_count = TASK_BASIC_INFO_COUNT;
    kern_return_t status;

    status = task_info(current_task(), TASK_BASIC_INFO, (task_info_t)&basic_info, &t_info_count);

    if (status != KERN_SUCCESS)
    {
        NSLog(@"%s(): Error in task_info(): %s", __FUNCTION__, strerror(errno));
        return 0;
    }

    return (unsigned int)basic_info.resident_size;
}

このコードをAssets/Plugins/iOSに適切に配置して読み込むとiOSからUnityAppが使用している物理メモリ量を取得することができます。
ネイティブプラグインを読み込む方法は世の中にたくさんあるのでここでは割愛します。

task_info関数は公式リファレンスによると、現在のtaskに関する情報の配列を返すとあります。
第2引数のflavorにTASK_BASIC_INFOを指定すると第3引数に与えたtask_infoに以下のような構造体が入ります。

struct task_basic_info {
        integer_t       suspend_count;  /* suspend count for task */
        vm_size_t       virtual_size;   /* virtual memory size (bytes) */
        vm_size_t       resident_size;  /* resident memory size (bytes) */
        time_value_t    user_time;      /* total user run time for
                                           terminated threads */
        time_value_t    system_time;    /* total system run time for
                                           terminated threads */
    policy_t    policy;     /* default policy for new threads */
};

この中のresident_sizeがtaskが使っているメモリ量です。
iOSではtask = プロセス = アプリなのでこれでiOSから見たUnityアプリがどれ位メモリを食っているかを確認することができました。

Androidの場合

Androidの方ではtask_infoは使えませんが、代わりにAndroid自体に取得できる機能があったのでそちらを使いました。

import android.os.Debug;
import android.os.Process;
import android.app.ActivityManager;
import android.content.Context;

/**
 * @return 現在使用しているメモリ量(KB)
 */
private static long getUsedMemorySize() {
    final Context context = UnityPlayer.currentActivity.getApplication().getApplicationContext();
    final ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
    final int[] pids = new int[]{ Process.myPid() };
    final Debug.MemoryInfo[] memoryInfos = activityManager.getProcessMemoryInfo(pids);
    long sumMemories = 0;

    for (Debug.MemoryInfo mi : memoryInfos) {
        sumMemories += mi.getTotalPss();
    }

    return sumMemories;
}

Android公式リファレンスを見るとDebug.MemoryInfoがいろいろな情報を持っているのでこちらを利用します。
プロセス毎にメモリ使用量が取れるのでそれを合算して表示します。

改修後

両プラットフォームともネイティブコードを書いたところで実際にプロファイラと見比べてみましょう

iOS

device-screenshot
instruments-screenshot

Instrumentsから得られた物理メモリ使用量と画面に表示しているメモリ使用量が一致していますね!

Android

device-screenshot

プロファイラからは以下のようになりました

kamata% adb shell dumpsys meminfo | grep gopher
    50409 kB: com.example.gopher (pid XXXXX / activities)
               50409 kB: com.example.gopher (pid XXXXX / activities)

50409 / 1024 = 49.2MB

この通りメモリ使用量がプロファイラから見たものと一致していますね!
こうしてUnityApp上で各OSからみたメモリ使用量を可視化することができました。
これで、実際の端末上で気軽にメモリ使用量を計測することができますね!

最後に

いかがでしたか。

このように簡単なコードを一手間加えるだけでプロファイリングが格段に楽になりましたね。
時には標準Profiler以外の方法も試してみると、よりUnityと上手く付き合っていけるのでは無いでしょうか。
この記事を見てくださった方のメモリ監視が楽になることをサンタさんにお願いして終わりたいと思います。

なお、今回作成したサンプルで使われているGopherのモデルはGitHubにあるこちらのモデルを利用しました。
Gopherの原著作者はRenée Frenchさんです。

次回は@hnwさんの「Autotools三兄弟の末っ子libtoolとは何者なのか」です。
僕の記事より面白そうなので今から楽しみです!

このブログについて

KLabのゲーム開発・運用で培われた技術や挑戦とそのノウハウを発信します。

おすすめ

合わせて読みたい

このブログについて

KLabのゲーム開発・運用で培われた技術や挑戦とそのノウハウを発信します。