Cooooding!!

Unity(C#)を使ったゲーム開発関連Tipsなど

Unityで使えるメモリ量を取得する方法まとめ

概要

UnityやC#にはメモリ量を取得する関数がいくつかありますが、関数の種類が多く特定の環境でしか使えない関数もあったりして複雑なのでまとめてみました。WindowsMaciOSAndroidで確認しています。

取得方法まとめ

取れる値関数
バイスメモリ量SystemInfo.systemMemorySize
(取れるのはおおよその値。MB単位)
ビデオメモリ量SystemInfo.graphicsMemorySize
(取れるのはおおよその値。MB単位)
アプリアプリ全体※長くなるので後述
Unity Memory
(リソースなど)
全体Profiler.GetTotalReservedMemoryLong
使用量Profiler.GetTotalAllocatedMemoryLong
Profiler.usedHeapSizeLong
(MacのEditor以外ではどちらも同じ値)
未使用量Profiler.GetTotalUnusedReservedMemoryLong
Mono Heap
(Scriptからnewしたものなど)
全体Profiler.GetMonoHeapSizeLong
使用量Profiler.GetMonoUsedSizeLong
GC.GetTotalMemory(false)
(どちらも同じ値が取れる)
Graphic DriverProfiler.GetAllocatedMemoryForGraphicsDriver
(Editor、Development Buildのみ有効)
その他UnityEngine.Objectのサイズ
(GameObjectやSpriteなど)
Profiler.GetRuntimeMemorySizeLong
(Editor、Development Buildのみ有効)

※ Profiler名前空間の関数でもGetAllocatedMemoryForGraphicsDriverとGetRuntimeMemorySizeLong以外はDevelopment Build無しで使えます
MacのEditorではusedHeapSizeLongがGetTotalReservedMemoryLongを超える値になっていました。どんな理由にせよEditorのメモリ使用量はあまり参考にならないので詳しく調査していません

アプリ全体のメモリ使用量の取得

アプリ全体のメモリ使用量に関しては何故かどの環境でも簡単に取得できる方法が用意されていません。多くの環境ではNative Pluginを実装することになります。

環境取得方法
WindowsEditorNative Pluginを実装。
詳しくは『Windowsアプリのメモリ使用量を取得する』参照。
StandaloneIL2CPP
Mono
MacEditorProcess.GetCurrentProcess().WorkingSet64
(アクティビティモニタの「実メモリ」と同じ値)
StandaloneMono
IL2CPPNative Pluginで取れそうだけど使う予定がないので未検証
iOSIL2CPPNative Pluginを実装。
詳しくは『iOSでアプリのメモリ使用量を取得する』参照。
AndroidIL2CPPNative PluginかAndroidJavaClassで実装。
詳しくは『Native Pluginを作らずにAndroidアプリのメモリ使用量を取得する』参照。
(MonoのみProcess.GetCurrentProcess() .WorkingSet64でも取れます)
Mono

※ ちなみに System.Environment.WorkingSetはどの環境でも0を返しました
※ 細かい話をすると共有メモリや仮想メモリなどの種類があり、どの値が欲しいかによっては上記の実装そのままではダメな可能性があります。

環境

  • Unity2019.2
  • Windows10
  • Mac Mojave
  • Nexus 5X (Android 6.0.1)
  • iPhoneXS (iOS13.3)
  • iPhone7 (iOS11.2.6)
  • iPhone6 (iOS9.3.5)

iOSでアプリのメモリ使用量を取得する【Unity】

概要

iOSでアプリがメモリ不足でキルされるまでにどれくらい余裕があるかを判定するためにメモリ使用量を取得する方法を調べました。取得できるメモリ量にはいろいろな種類があり、なかなか適切な取得方法がわからなかったので調べた結果わかったことをまとめます。

実装

結論に至るまでには紆余曲折あったのですが、まずは結論となる実装を書きます。

Native Plugin側

#import <mach/mach.h>

extern "C" 
{
	bool GetTaskVmInfo(task_vm_info_data_t *info)
	{
		mach_msg_type_number_t count = TASK_VM_INFO_COUNT;
		if(task_info(mach_task_self(), TASK_VM_INFO, (task_info_t)info, &count) != KERN_SUCCESS)
		{
			return false;
		}
		return true;
	}

	long GetMemorySize()
	{
		task_vm_info_data_t info;
		if(!GetTaskVmInfo(&info))
		{
			return -1;
		}
		return (long)(info.internal + info.compressed);
	}
}

この実装を書いた.mmファイルをAssets/Plugins/iOS以下に置きます。

C#

[DllImport("__Internal")]
public static extern long GetMemorySize();

上記の実装になった理由

resident_sizeで取得できる値

メモリ使用量を取得する方法を調べてみるとtask_basic_info_data_t.resident_sizeを取得しているサンプルがよく見つかります。これは物理メモリの使用量を取得するものではあるのですが、実際にiOS上での動作を確認してみるとアプリが確保したメモリ量とは異なった増減をすることがわかります。これに関してはっきり理由を書いた説明は見つからなかったのですが、いろいろ調べた情報と観察した挙動から察するにスワップのような機能やメモリを圧縮する機能があることなどが原因ではないかと思います。

iOSにあるスワップのような機能

iOSにもWindowsにあるスワップに似た機能があります。Windowsでは物理メモリが不足したときにHDDやSDDなどのストレージにデータを退避(ページアウト)して必要になったら再び物理メモリ上に戻されますが、iOSでは変更可能なメモリ(newやmallocで確保されるもの)がストレージにページアウトされることはありません。ストレージからロードしたデータで変更を加えていないもの(Cleanなメモリ)だけがページアウトされる可能性があります。

メモリを圧縮する機能

iOSではページアウトできるデータが限られていますが、その代わりメモリを圧縮する機能があります。これはCleanではないメモリ(Dirtyなメモリ)でも圧縮されます。圧縮されたメモリ量(おそらく圧縮前のサイズ)はtask_vm_info_data_t.compressedで取得できます。

resident_sizeではダメな理由

上記の事情により確保したメモリが増えるほどresident_sizeの値は増えにくくなります。実機で動作を観察してみるとresident_sizeの値がほとんど変わっていないのにも関わらずLow Memory Warningが発生したりキルされたりするので、メモリに余裕があるかどうかの判定にこの値は使えなさそうでした。

phys_footprintで取得できる値

task_vm_info_data_t.phys_footprintという値もあります。詳しい説明は見つかりませんでしたが挙動を観察してみると、アプリがnewした分この値も増えるわかりやすい挙動をしており、この値が特定の値を超えるとメモリ不足でアプリキルされるようでした。例えばiPhone XS(iOS13.3)なら2.04GiB、iPhone7(iOS11.2.6)なら1.37GiBを超えた辺りでアプリがキルされました。

また、この値はXcodeのDebug NavigatorのMemory Reportのメモリ量やInstrumentsのActivity MonitorのLive ProcessesのMemoryの値とも一致するようでした。

XcodeのDebug NavigatorのMemory Reportの値

InstrumentsのActivity MonitorのLive ProcessesのMemoryの値

ただ、iPhone6(iOS9.3.5)で確認してみると何故かXcodeの値と33MiBほどのズレがあるようでした。

internal + compressedにした理由

挙動を観察してみるとiPhone XS(iOS13.3)とiPhone7(iOS11.2.6)でphys_footprintの値はtask_vm_info_data_tのinternal + compressedとほぼ一致するようでした。iPhone6(iOS9.3.5)ではinternal + compressedの値がXodeの値とほぼ一致するようなのでphys_footprintよりもこちらの値を使った方が良さそうでした。

古いiPhoneでphys_footprintの値が異なる件についてはっきりとした情報はありませんでしたがおそらくiOSが古いことが原因ではないかと思います。iOS10がどうなるか気になりますが手元にiOS10の端末がないため確認できませんでした。何年か経って古いiOSのサポートをする必要がなくなったらphys_footprintの方を見た方が良くなるかもしれません。

環境

  • Unity2019.2.9f1
  • iPhoneXS (iOS13.3)
  • iPhone7 (iOS11.2.6)
  • iPhone6 (iOS9.3.5)

Native Pluginを作らずにAndroidアプリのメモリ使用量を取得する【Unity】

概要

Androidでアプリ全体のメモリ使用量を取得する場合Native Pluginを作るのが一般的ですが、AndroidJavaClassを使えばNative Plugin無しでも実装できそうだったので実装してみました。

実装

public static long GetMemorySize()
{
    var unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
    var activity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity");
    var application = activity.Call<AndroidJavaObject>("getApplication");
    var context = activity.Call<AndroidJavaObject>("getApplicationContext");
    var staticContext = new AndroidJavaClass("android.content.Context");
    var service = staticContext.GetStatic<AndroidJavaObject>("ACTIVITY_SERVICE");
    var activityManager = activity.Call<AndroidJavaObject>("getSystemService", service);
    var process = Process.GetCurrentProcess();
    var pidList = new int[] { process.Id };
    var memoryInfoList = activityManager.Call<AndroidJavaObject[]>("getProcessMemoryInfo", pidList);

    long total = 0;
    foreach(var memoryInfo in memoryInfoList)
    {
        total += memoryInfo.Call<int>("getTotalPss") * 1024;// kB単位なのでByte単位に直す
    }
    return total;
}

実行速度の面ではNative Pluginを作った方がいいと思いますが、ちょっと試すだけならこの方が楽で良さそうです。

環境

  • Unity2019.2.9f1
  • Nexus 5X (Android6.0.1)

Windowsアプリのメモリ使用量を取得する【Unity】

概要

UnityではないC#であればSystem.Environment.WorkingSetやProcess.GetCurrentProcess().WorkingSet64などを使ってアプリのメモリ使用量を取得できますが、Unityで作ったWindows Standaloneアプリでは何故か0を返します。調べても解決方法が見つかりませんでしたがC++でPluginを書いてみたら取得できたのでその方法を解説します。

メモリ関連の用語について

まず実装の説明をする前にメモリ関連の用語について簡単に説明しておきます。

用語 意味
スワップ 物理的なメモリ上のデータをストレージ(HDDやSSD)上に退避させること
ワーキングセット 物理的なメモリ上に確保されているメモリ量
プライベート ワーキングセット ワーキングセットのうちそのプロセスだけが使えるメモリ量
共有 ワーキングセット ワーキングセットのうち他のプロセスと共有しているメモリ量
コミットサイズ プライベートワーキングセット + スワップしたメモリ量

この記事ではワーキングセットとコミットサイズの取得方法を解説します。

実装

Pluginの実装

ダイナミックリンクライブラリ(DLL)の新規プロジェクトを作って以下のコードを書きます。

ヘッダー(.h)

#pragma once

extern "C"
{
	__declspec(dllexport) long GetWorkingSet();
	__declspec(dllexport) long GetCommitSize();
}

cpp

#include "pch.h"
#include "memory.h"
#include <psapi.h>

long GetWorkingSet()
{
	PROCESS_MEMORY_COUNTERS info;
	if (!GetProcessMemoryInfo(GetCurrentProcess(), &info, sizeof(info)))
	{
		return -1;
	}
	return (SIZE_T)info.WorkingSetSize;
}

long GetCommitSize()
{
	PROCESS_MEMORY_COUNTERS_EX info;
	if (!GetProcessMemoryInfo(
		GetCurrentProcess(),
		(PROCESS_MEMORY_COUNTERS*)&info,
		sizeof(info)
	))
	{
		return -1;
	}
	return (SIZE_T)info.PrivateUsage;
}

これをビルドしてできたdllをUnityで使います。環境にもよると思いますが 自分の環境ではソリューションプラットフォームを「x86」から「x64」に変更してビルドする必要がありました。

Unity側の実装

作ったdll(以下、MemorySize.dll)をPluginsフォルダ以下に置きます。あとは適当なクラスに以下のメソッドを定義すればメモリ量が取得できるようになります。

[DllImport("MemorySize")]
public extern static long GetWorkingSet();

[DllImport("MemorySize")]
public extern static long GetCommitSize();

GetWorkingSetはタスクマネージャーの「ワーキングセット(メモリ)」と同じ値になります。GetCommitSizeは「コミットサイズ」と同じ値になります。

ちなみに、WindowsのUnity Editorのプレビュー中にも取得できますがUnity Editor自体のメモリ使用量が含まれてしまいます。

環境

  • Visual Studio2019
  • Unity2019.2.9f1
  • Windows10 (64bit)

Unity2019.2に更新して発生したトラブルとその対処【Unity】

概要

プロジェクトをUnity2019.1.9f1からUnity2019.2.9f1に更新に更新したところ大量のコンパイルエラーが出てしまいました。

Unity2019.2.9f1に更新して発生したエラー

調べてみたところどうやらAssembly Definitionの仕様変更による影響のようだったのでその詳細と対処方法をまとめます。

エラー内容詳細

エラーは以下のようにOdinのクラスや名前空間にアクセスしているところで発生しているようでした。

Unity2019.2.9f1に更新して発生したエラーの発生個所1

The type or namespace name 'Sirenix' could not be found (are you missing a using directive or an assembly reference?)

Unity2019.2.9f1に更新して発生したエラーの発生個所2

The type or namespace name 'ReadOnlyAttribute' could not be found (are you missing a using directive or an assembly reference?)
The type or namespace name 'ShowInInspectorAttribute' could not be found (are you missing a using directive or an assembly reference?)

原因

このエラーはAssembly Definitionの仕様が変更されたことにより「Test Assemblies」が消えたことと、Unityのアップデート処理でこれらの移行がうまくできなかったのが原因のようです。

おそらく以下の条件を満たしていればOdin以外のdllでも発生するのではないかと思います。

  • Assembly Definitionを使っている
  • Test Assembliesにチェックを入れていた
  • そのAssembly Definition以下のスクリプトコンパイル済みのdllを参照している

対処方法

このエラーは参照関係を正しく設定してあげれば解決します。

Test Assembliesにチェックを入れていたAssembly Definitionは移行後に以下のようになっていると思います。
Unity2019.2.9f1に更新後のAssembly Definitionの設定

問題があるのは「Assembly References」の部分です。参照が足りていないので追加します。今回のケースではOdinのdllを2つ追加しました。
Unity2019.2.9f1に更新後のエラーを修正したAssembly Definitionの設定

(※ もし「Assembly References」の項目が表示されていなければ「Override References」のチェックを入れると表示されます。)

環境

  • Unity2019.1.9f1
  • Unity2019.2.9f1

TestRunnerでLogErrorや例外が発生することをテストする【Unity】

概要

TestRunnerで例外が発生することをテストするのはNUnit.Framework.Assert.Catchでできますが LogErrorのテストはできません。指定した文字列のLogErrorが出力されることをテストするのはTestTools.LogAssertでできますが、前回の記事で書いたように少し扱い辛い仕様になっています。そこでNUnit.Framework.Assert.Catchのようなインターフェースで使えてLogErrorもテストできる関数を実装してみました。

実装

/// <summary>
/// 何らかのエラー(LogError/例外)が発生されることを確認する
/// </summary>
public static void ErrorOccurs(TestDelegate code)
{
    LogAssert.ignoreFailingMessages = true; // LogErrorの出力でテストが失敗しないように
    bool error = false;                     // エラーが発生したかどうかのフラグ
    // ログ出力イベント処理用のローカル関数
    void OnLogMessage(string logString, string stackTrace, LogType logType)
    {
        switch(logType)
        {
            case LogType.Assert:
            case LogType.Exception:
            case LogType.Error:
                error = true;
                break;
        }
    };

    Application.logMessageReceived += OnLogMessage;
    try
    {
        code.Invoke();  // 引数で渡されたDelegateを実行
    }
    catch(System.Exception e)
    {
        UnityEngine.Debug.LogException(e);
    }
    Application.logMessageReceived -= OnLogMessage;
    LogAssert.ignoreFailingMessages = false;
    
    if (!error)
    {
        Assert.Fail("エラーが発生しませんでした");
    }
}

使用例

[Test]
public void ErrorOccurs_LogError()
{
    ErrorOccurs(() => { Debug.LogError("エラー!!"); });
}

[Test]
public void ErrorOccurs_Exception()
{
    ErrorOccurs(() => { throw new System.Exception("例外!!"); });
}

[Test]
public void ErrorOccurs_Nothing()
{
    ErrorOccurs(() => { });     // エラーが発生しないのでテストに失敗する
}

環境

  • Unity 2019.1.9f1
  • VisualStudio 2019

TestTools.LogAssertについて調べてみた【Unity】

概要

Test Runner(NUnit)でログが出力されることをテストできるTestTools.LogAssertを使おうと思ったのですが、ドキュメントを呼んでもよくわからなかったので詳しく調べてみました。

LogAssertの基礎

LogAssertにはExpectとNoUnexpectedReceivedの2つの関数があります。

1.Expectについて

LogAssert.Expectは指定したログが出力されなければエラーにする関数です。

例えば、↓これはエラーになりませんが

[Test]
public void Expect_Log()
{
    LogAssert.Expect(LogType.Log, "abc");
    Debug.Log("abc");
}

↓これはエラーになります。

[Test]
public void Expect_Log_Fail()
{
    LogAssert.Expect(LogType.Log, "abc");
    // 期待していたログが出力されない
}

エラー内容

Expected log did not appear: [Log] abc

上記の例ではLog関数を使っていますが、LogWarningやLogErrorでも同様です。

[Test]
public void Expect_Error()
{
    LogAssert.Expect(LogType.Error, "xxxx");
    Debug.LogError("xxxx");
}

エラーログが出力されていますがExpectされたものなのでテストを通ります。

ログの内容は正規表現で指定することもできます。

[Test]
public void Expect_Regex()
{
    LogAssert.Expect(LogType.Log, new Regex("[0-9]+"));
    Debug.Log("123456789");
}

2.NoUnexpectedReceivedについて

LogAssert.NoUnexpectedReceivedはExpectされてないログが出力されていたらエラーにする関数です。

例えば、↓これはエラーになりますが

[Test]
public void NoUnexpectedReceived_Fail()
{
    Debug.Log("xxxx");
    LogAssert.NoUnexpectedReceived();  // Expectされてないログが出力されたのでエラー
}

↓これはエラーになりません。

[Test]
public void NoUnexpectedReceived()
{
    LogAssert.Expect(LogType.Log, "xxxx");
    Debug.Log("xxxx");
    LogAssert.NoUnexpectedReceived();  // Expectされたログ以外は出力されなかったのでOK
}

詳細な挙動

ここからはもっと詳細な挙動を見てみます。

1.一度Expectされたログを何度出力していいわけではない

以下のようにExpectしたログを2回出力するとNoUnexpectedReceivedでエラーになります。

[Test]
public void LogRepeat_Fail()
{
    LogAssert.Expect(LogType.Log, "xxxx");
    Debug.Log("xxxx");
    Debug.Log("xxxx");
    LogAssert.NoUnexpectedReceived();
}

以下のように2回Expectしておけばエラーになりません。

[Test]
public void LogRepeat2()
{
    LogAssert.Expect(LogType.Log, "xxxx");
    LogAssert.Expect(LogType.Log, "xxxx");
    Debug.Log("xxxx");
    Debug.Log("xxxx");
    LogAssert.NoUnexpectedReceived();
}

2.複数回Expectされた場合は登録順にマッチする

挙動を観察した限りでは 複数回ExpectするとExpectされた順にマッチするようです。マッチする順序を考慮しないと意図せずエラーになってしまうことがあるかもしれません。

[Test]
public void MultiExpectOrder_Fail()
{
    LogAssert.Expect(LogType.Log, new Regex("[a-z]+"));  // xxxxとマッチさせるつもりが…
    LogAssert.Expect(LogType.Log, "yy");
    Debug.Log("yy");      // 1つ目のExpectとマッチしてしまう
    Debug.Log("xxxx");
    LogAssert.NoUnexpectedReceived();  // xxxxがExpectされていないログになりエラー
}

3.Expectした直後に出力される必要はない

Expectしたログはすぐに出力される必要はなく、NoUnexpectedReceivedの後だったり別のログが出力された後でも問題ありません。例えば、以下のテストも通ります。

[Test]
public void ExpectAfter()
{
    LogAssert.Expect(LogType.Log, "xxxx");
    Test0();    // この関数でxxxxが出力されることを期待していたが…
    LogAssert.NoUnexpectedReceived();

    Test1();    // この関数でxxxxが出力されてしまったのでテストが通ってしまった
}

public void Test0() { }
public void Test1() { Debug.Log("yyy"); Debug.Log("xxxx"); }

4.ログの出力をしてからExpectしてもテストが通る

実はログを出力してからExpectしてもテストが通ります。

[Test]
public void ExpectOrder()
{
    Debug.Log("xxxx");
    LogAssert.Expect(LogType.Log, "xxxx");
}

感想

順序を入れ替えても動く挙動が直感的でなかったり、Expectしたものをクリアできなかったりして使いにくい印象です。特に1つのテスト関数で複数の関数をテストする場合には注意が必要そうです。ログのテストをするなら1つのテスト関数でテスト対象の関数を1回だけ呼び出すようにするのが良さそうです。

環境

  • Unity 2019.1.9f1
  • VisualStudio 2019