見出し画像

Now in REALITY Tech #103 ProfilerRecorderを用いてアプリ上でメモリ使用量を計測する


こんにちは、アバターシステムチーム所属、Unity エンジニアの虹ゴリラです。

今回は Unity の ProfilerRecorder と言う API を利用した「アプリ上でのメモリ使用量の計測方法」について紹介します。

こちらを用いればプロファイラの Memory Profiler module 上に表示されている計測値をアプリ上からでも取得することが出来るためProfiler API では取得できない詳細な情報を取得して独自の開発機能などに組み込むことが出来るようになるかと思います。

Profiler API では取得できない値も取得可能

更に一部の値に関しては Profiler API 同様にリリースビルドでも取得することが可能なため、機能の使い方によっては製品版相当の環境下での計測にも使えるかと思います。


記事は Unity バージョン2022.2.5f1 を前提に書いています。

ProfilerRecorder を用いた値の取得方法

まず予備知識として ProfilerRecorder を用いた「メモリ使用量」の取得方法について簡単に解説していきます。

こちらは公式ドキュメントの Memory Profiler module にある Availability in Players にてサンプルコードが載っているので、今回はこちらを引用する形で解説していきます。

using System.Text;
using Unity.Profiling;
using UnityEngine;

public class MemoryStatsScript : MonoBehaviour
{
    string statsText;
    ProfilerRecorder totalReservedMemoryRecorder;
    ProfilerRecorder gcReservedMemoryRecorder;
    ProfilerRecorder systemUsedMemoryRecorder;

    void OnEnable()
    {
        totalReservedMemoryRecorder = ProfilerRecorder.StartNew(ProfilerCategory.Memory, "Total Reserved Memory");
        gcReservedMemoryRecorder = ProfilerRecorder.StartNew(ProfilerCategory.Memory, "GC Reserved Memory");
        systemUsedMemoryRecorder = ProfilerRecorder.StartNew(ProfilerCategory.Memory, "System Used Memory");
    }

    void OnDisable()
    {
        totalReservedMemoryRecorder.Dispose();
        gcReservedMemoryRecorder.Dispose();
        systemUsedMemoryRecorder.Dispose();
    }

    void Update()
    {
        var sb = new StringBuilder(500);
        if (totalReservedMemoryRecorder.Valid)
            sb.AppendLine($"Total Reserved Memory: {totalReservedMemoryRecorder.LastValue}");
        if (gcReservedMemoryRecorder.Valid)
            sb.AppendLine($"GC Reserved Memory: {gcReservedMemoryRecorder.LastValue}");
        if (systemUsedMemoryRecorder.Valid)
            sb.AppendLine($"System Used Memory: {systemUsedMemoryRecorder.LastValue}");
        statsText = sb.ToString();
    }

    void OnGUI()
    {
        GUI.TextArea(new Rect(10, 30, 250, 50), statsText);
    }
}

インスタンス化時に取得したい情報を指定

こちらのサンプルでは ProfilerRecorder.StartNew と言う API が用いられており、インスタンス化と同時に計測の開始が行われています。

この中で肝となるのは StartNew メソッドの第二引数となる statsName であり、こちらに特定の文字列 (カウンター / マーカー名) を渡すことで計測対象を指定することが出来ます。

プロファイラの値を取得する際には事前に定義されているカウンター名を渡す必要があり、具体的にどういったものが指定できるのか?については次の章で解説します。

void OnEnable()
{
    // "Total Reserved Memory" -> Unity が確保しているメモリの総量
    totalReservedMemoryRecorder = ProfilerRecorder.StartNew(
        ProfilerCategory.Memory,
        "Total Reserved Memory");

    // "GC Reserved Memory" -> マネージドヒープサイズ
    gcReservedMemoryRecorder = ProfilerRecorder.StartNew(
        ProfilerCategory.Memory,
        "GC Reserved Memory");

    // "System Used Memory" -> アプリ全体のメモリ使用量
    systemUsedMemoryRecorder = ProfilerRecorder.StartNew(
        ProfilerCategory.Memory,
        "System Used Memory");
}

計測値の取得について

計測値の取得方法の一つとして、LastValue と言うプロパティから ProfilerRecorder によって収集された最後の計測値を得ることが出来ます。
サンプルではこのプロパティから計測値を取得 / 保持して、それを GUI.TextArea で表示させる実装となっています。

void Update()
{
    var sb = new StringBuilder(500);
    if (totalReservedMemoryRecorder.Valid)
        sb.AppendLine($"Total Reserved Memory: {totalReservedMemoryRecorder.LastValue}");
    if (gcReservedMemoryRecorder.Valid)
        sb.AppendLine($"GC Reserved Memory: {gcReservedMemoryRecorder.LastValue}");
    if (systemUsedMemoryRecorder.Valid)
        sb.AppendLine($"System Used Memory: {systemUsedMemoryRecorder.LastValue}");
    statsText = sb.ToString();
}

void OnGUI()
{
    GUI.TextArea(new Rect(10, 30, 250, 50), statsText);
}

他にもサンプルでは解説されてませんが、UnitType と言うプロパティからは計測値の単位を取得することが可能です。
(ちなみにサンプルで取得しているメモリ計測値の場合には全て Bytes が返ります)

この章では全てについては触れないので、詳しくはドキュメントを御覧ください。

取得できる情報について

ProfilerRecorder に指定可能なカウンターについてですが、こちらは公式ドキュメントの Memory Profiler module ページに一覧が載っています。

こちらのリストから参照可能

例えば幾つかピックアップすると次の項目などがあり、これらを指定することでプロファイラ上に表示されている各項目に対応した値を取得することが出来ます。
( "[ ]" 内の文字列は ProfilerRecorder.StartNew の statName に渡す名前 )

  • Total Committed Memory [ "System Used Memory" ]

    • アプリ全体のメモリ使用量

  • Tracked Memory (in use / Reserved) [ "Total Used Memory" / "Total Reserved Memory " ]

    • Unity レイヤーが利用しているメモリ使用量

  • Managed Heap (in use / Reserved) [ "GC Used Memory" / "GC Reserved Memory" ]

    • マネージドヒープの使用量

  • Graphics & Graphics Driver [ "Gfx Used Memory" / "Gfx Reserved Memory" ]

    • グラフィックス関連 (Texture, Shader, Mesh, etc) で利用されるドライバーのメモリ使用量の推定値

    • ※ Development Build 限定

Untracked Memory について

プロファイラから計測可能な項目の一つとして「Untracked Memory」と言う指標があります。

こちらはドキュメントから引用すると次のようにあり、主に Unity がトラッキングしていないネイティブ側で使用しているメモリなどが含まれているものとなります。

Untracked Memory
Indicates the total amount of memory that Unity used but isn’t aware of. Some examples of untracked memory are:

- Memory allocated through native plug-ins or some drivers
- Mono or IL2CPP type metadata
- Memory that executable code and DLLs use

The Memory Profiler package and native platform providers might have more information on some of these untracked memory amounts.

Memory Profiler module より

特に REALITY に於いてはネイティブプラグイン以外にも、UaaL (Unity as a Library) で運用されている性質からネイティブレイヤーで確保されているメモリにも気を配る必要があり、その観点から Untracked Memory は大事な指標となります。

Untracked Memory を取得する

そんな Untracked Memory ですが、ドキュメントを参照すると対応するカウンターが N/A となっており、ProfilerRecorder から取得する術がありません。

N/A となっており対応するカウンターが存在しない

UnityCsReference の実装を見るに恐らくは View 側で各計測値を元に算出して表示しているらしく、これを元に手元で検証してみた所、「System Used Memory - Total Reserved Memory」を求めることでプロファイラ上に表示されている計測値とおおよそ一致する値を取得することが出来ました。

private void Update()
{
    _stringBuilder.Clear();

    // System Used Memory
    var systemUsed = _systemUsedMemoryRecorder.LastValue;
    _stringBuilder.AppendLine($"System Used Memory: {systemUsed / (1024f * 1024f):0.0}");

    // Total Reserved Memory
    var totalReserved = _totalReservedMemoryRecorder.LastValue;
    _stringBuilder.AppendLine($"Total Reserved Memory: {totalReserved / (1024f * 1024f):0.0}");

    var untracked = systemUsed - totalReserved;
    _stringBuilder.AppendLine($"Untracked Memory: {untracked / (1024f * 1024f)}:0.0");
}

Profiler API との違いについて

最後に Profiler API との違いについて簡単に調査してみたので紹介してみたいと思います。

ちなみに Profiler API とは ProfilerRecorder が登場する以前から利用可能な API であり、こちらにも幾つかのメモリ使用量を取得するための機能が存在します。

Unity レイヤーが利用しているメモリ使用量の取得値に違いがある

Unity レイヤーが使用しているメモリ使用量の取得は ProfilerRecorder 及び Profiler API の両方で利用可能ですが、これらの結果を見比べてみた所、値が一致していないのを確認しました。

例えば以下のキャプチャは簡易的なアプリに両方の計測値を表示させたものですが、結果としては次のようになってます。

Profiler API の方が値が小さい?

この件について調査してみた感じだと、恐らくは ProfilerRecorder の方は Profiler API で言う GetTotalAllocatedMemoryLong で取得できる値以外にも、GetMonoUsedSizeLong や GetAllocatedMemoryForGraphicsDriver が乗った結果が表示されいるように思われました。( Reserved の方も同様です)

コードで表すと以下のようになり、仕様としてこのような性質の違いがあるのかと思われます。

private void Update()
{
    _stringBuilder.Clear();

    // ProfilerRecorder
    {
        var totalUsedMemory = _totalUsedMemoryRecorder.LastValue;
        _stringBuilder.AppendLine($"ProfilerRecorder: {totalUsedMemory / (1024f * 1024f):0.0}");
    }

    // Profiler API
    {
        // NOTE: これらの総和で ProfilerRecorder の `Total Used Memory` と一致する ( Reserved も同様)
        var totalAlloc = Profiler.GetTotalAllocatedMemoryLong();
        var monoUsed = Profiler.GetMonoUsedSizeLong();
        var graphics = Profiler.GetAllocatedMemoryForGraphicsDriver();
        var totalUsedMemory = totalAlloc + monoUsed + graphics;
        _stringBuilder.AppendLine($"Profiler API: {totalUsedMemory / (1024f * 1024f):0.0}");
    }
}

ちなみにそれ以外の値 (マネージドヒープやグラフィックス関連) は特に違いは見受けられずに一致している結果が得られている印象です。

おわりに

今回は ProfilerRecorder を用いたアプリ上でのメモリ使用量の計測方法について簡単にご紹介させていただきました。

特にこちらが登場する以前だと「アプリ全体のメモリ使用量」を取得するにはネイティブプラグインなどを実装する必要があったイメージですが、その必要も無く簡単に取得できるようになったかなと思います。