こんにちは
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()
公式のリファレンスにもなにも書いてない...
実際に動かした結果とプロファイラを見比べてみると
全ての計測値にかなり開きがありますね。これでは計測の目安とするには不十分です。
ではどうすればいいのでしょうか?
僕らが見たいメモリ使用量は各OSから見てUnityAppがどれほどのメモリを使っているかというものです。
なのでUnityから利用できるネイティブプラグインを書いてそこから各OSに問い合わせればいいわけです。
こうして書いたネイティブプラグインコードがこちらです。
#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の方では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
がいろいろな情報を持っているのでこちらを利用します。
プロセス毎にメモリ使用量が取れるのでそれを合算して表示します。
両プラットフォームともネイティブコードを書いたところで実際にプロファイラと見比べてみましょう
Instrumentsから得られた物理メモリ使用量と画面に表示しているメモリ使用量が一致していますね!
プロファイラからは以下のようになりました
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のゲーム開発・運用で培われた技術や挑戦とそのノウハウを発信します。