背景
- DevelopmentBuild できない ので、実機でUnityプロファイラで計測できない
- 2022/8/10 追記:NRSDK 1.9.3 では Development Build で実機起動できるようになった
- NRSDK/Prefabs/FPSCanvas を流用すればFPS計測はすぐにできる
- スクリプト処理時間、レンダリング時間、NRKernel処理時間なども知りたい
- Android Studio の CPU Profile は低レイヤーすぎる(スレッド単位の計測)
結論
- Unityスクリプトライフサイクル に沿って Time.realtimeSinceStartup で打刻して、差分からCPU処理時間を求める
- Nrealアプリに限らず、一般的なUnityアプリでも同様
- 以下の「自前実装」とは、Unity にて自分で実装していく部分のこと
| Update のCPU処理時間 |
「最後の MonoBehaviour の yield null 時刻」から 「最初の MonoBehaviour の Update 時刻」を引いた値 |
| Animation のCPU処理時間 |
「最初の MonoBehaviour の LateUpdate 時刻」から 「最後の MonoBehaviour の yield null 時刻」を引いた値 |
| LateUpdate のCPU処理時間 |
「最後の MonoBehaviour の LateUpdate 時刻」から 「最初の MonoBehaviour の LateUpdate 時刻」を引いた値 |
| Rendering のCPU処理時間 |
「最初の MonoBehaviour の yield WaitForEndOfFrame 時刻」から 「最後の MonoBehaviour の LateUpdate 時刻」を引いた値 |
| Update_NRKernel のCPU処理時間 |
「最初の MonoBehaviour の Update 時刻」から 「自前実装の最初の MonoBehaviour の Update 時刻」を引いた値 |
| LateUpdate_NRKernel のCPU処理時間 |
「最初の MonoBehaviour の LateUpdate 時刻」から 「自前実装の最初の MonoBehaviour の LateUpdate 時刻」を引いた値 |
方法
- Unity にて3つの C# Script を作成
- アプリ全体の最初と最後に実行される MonoBehaviour と、自前実装の最初の MonoBehaviour の3つ
- クラス名はそれぞれ ProfilingFirstMonoBehaviour, ProfilingFirstMonoBehaviour, Framework とする
- 3つとも Main Camera などにアタッチする
- インスペクタから Execution Order を開き、下図のように設定する
- Unity の MonoBehaviour は実行順序不定だが、これで「最初」→「NRKernel」→「自前実装の最初」→「最後」の順番で実行される MonoBehaviour が得られる
- NRKernel から NRKernel.NREmulatorManager が外れているが、実機では実行されないので問題ナシ

コード
Profilingクラスはデータ集計するだけなので、MonoBehaviour を継承しない。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public sealed class ProfilingFirstMonoBehaviour : MonoBehaviour
{
// Start is called before the first frame update
void Awake()
{
mWaitForEndOfFrame = new WaitForEndOfFrame();
StartCoroutine(coroutine_WaitForEndOfFrame());
}
// Update is called once per frame
void Update()
{
Profiling.Instance.OnUpdate_FirstMonoBehaviour();
}
void LateUpdate()
{
Profiling.Instance.OnLateUpdate_FirstMonoBehaviour();
}
private IEnumerator coroutine_WaitForEndOfFrame()
{
for (; ; )
{
yield return mWaitForEndOfFrame;
Profiling.Instance.OnYieldWaitForEndOfFrame_FirstMonoBehaviour();
}
}
private WaitForEndOfFrame mWaitForEndOfFrame;
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public sealed class ProfilingLastMonoBehaviour : MonoBehaviour
{
// Start is called before the first frame update
void Awake()
{
StartCoroutine(coroutine_null());
}
// Update is called once per frame
void Update()
{
}
void LateUpdate()
{
Profiling.Instance.OnLateUpdate_LastMonoBehaviour();
}
private IEnumerator coroutine_null()
{
for (; ; )
{
yield return null;
Profiling.Instance.OnYieldNull_LastMonoBehaviour();
}
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public sealed class Framework : MonoBehaviour
{
// Start is called before the first frame update
void Awake()
{
}
// Update is called once per frame
void Update()
{
Profiling.Instance.OnUpdate_GameFirstMonoBehaviour();
// 30秒経過時にCPU処理時間をログ出力
if (!mIsPrintProcessingTime && Time.realtimeSinceStartup > 30f)
{
mIsPrintProcessingTime = true;
Debug.Log($"[Profiling]Update: {Profiling.Instance.ProcessingTimeAverage_Update:f1} ms (NRKernel: {Profiling.Instance.ProcessingTimeAverage_Update_NRKernel:f1} ms)");
Debug.Log($"[Profiling]Animation: {Profiling.Instance.ProcessingTimeAverage_Animation:f1} ms");
Debug.Log($"[Profiling]LateUpdate: {Profiling.Instance.ProcessingTimeAverage_LateUpdate:f1} ms (NRKernel: {Profiling.Instance.ProcessingTimeAverage_LateUpdate_NRKernel:f1} ms)");
Debug.Log($"[Profiling]Rendering: {Profiling.Instance.ProcessingTimeAverage_Rendering:f1} ms");
Debug.Log($"[Profiling]All: {Profiling.Instance.ProcessingTimeAverage_All:f1} ms");
}
}
void LateUpdate()
{
Profiling.Instance.OnLateUpdate_GameFirstMonoBehaviour();
}
private bool mIsPrintProcessingTime = false;
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// 処理計測クラス
// 複数個所から呼ばれるのでシングルトンにしている
public sealed class Profiling
{
public void OnUpdate_FirstMonoBehaviour()
{
// 計測開始:Update
mStartTime_Update = Time.realtimeSinceStartup;
if (++mIndex == cIndexMax)
{
mIndex = 0;
}
}
public void OnUpdate_GameFirstMonoBehaviour()
{
// 計測終了:Update_NRKernel
mProcessingTime_Update_NRKernel = (Time.realtimeSinceStartup - mStartTime_Update) * 1000;
if (mIndex != cIndexMax - 1)
{
mProcessingTimeSum_Update_NRKernel += mProcessingTime_Update_NRKernel;
}
else
{
mProcessingTimeAverage_Update_NRKernel = mProcessingTimeSum_Update_NRKernel / cIndexMax;
mProcessingTimeSum_Update_NRKernel = 0f;
}
}
public void OnYieldNull_LastMonoBehaviour()
{
// 計測終了:Update
mProcessingTime_Update = (Time.realtimeSinceStartup - mStartTime_Update) * 1000;
if (mIndex != cIndexMax - 1)
{
mProcessingTimeSum_Update += mProcessingTime_Update;
}
else
{
mProcessingTimeAverage_Update = mProcessingTimeSum_Update / cIndexMax;
mProcessingTimeSum_Update = 0f;
}
// 計測開始:Animation
mStartTime_Animation = Time.realtimeSinceStartup;
}
public void OnLateUpdate_FirstMonoBehaviour()
{
// 計測終了:Animation
mProcessingTime_Animation = (Time.realtimeSinceStartup - mStartTime_Animation) * 1000;
if (mIndex != cIndexMax - 1)
{
mProcessingTimeSum_Animation += mProcessingTime_Animation;
}
else
{
mProcessingTimeAverage_Animation = mProcessingTimeSum_Animation / cIndexMax;
mProcessingTimeSum_Animation = 0f;
}
// 計測開始:LateUpdate
mStartTime_LateUpdate = Time.realtimeSinceStartup;
}
public void OnLateUpdate_GameFirstMonoBehaviour()
{
// 計測終了:LateUpdate_NRKernel
mProcessingTime_LateUpdate_NRKernel = (Time.realtimeSinceStartup - mStartTime_LateUpdate) * 1000;
if (mIndex != cIndexMax - 1)
{
mProcessingTimeSum_LateUpdate_NRKernel += mProcessingTime_LateUpdate_NRKernel;
}
else
{
mProcessingTimeAverage_LateUpdate_NRKernel = mProcessingTimeSum_LateUpdate_NRKernel / cIndexMax;
mProcessingTimeSum_LateUpdate_NRKernel = 0f;
}
}
public void OnLateUpdate_LastMonoBehaviour()
{
// 計測終了:LateUpdate
mProcessingTime_LateUpdate = (Time.realtimeSinceStartup - mStartTime_LateUpdate) * 1000;
if (mIndex != cIndexMax - 1)
{
mProcessingTimeSum_LateUpdate += mProcessingTime_LateUpdate;
}
else
{
mProcessingTimeAverage_LateUpdate = mProcessingTimeSum_LateUpdate / cIndexMax;
mProcessingTimeSum_LateUpdate = 0f;
}
// 計測開始:Rendering
mStartTime_Rendering = Time.realtimeSinceStartup;
}
public void OnYieldWaitForEndOfFrame_FirstMonoBehaviour()
{
// 計測終了:Rendering
mProcessingTime_Rendering = (Time.realtimeSinceStartup - mStartTime_Rendering) * 1000;
if (mIndex != cIndexMax - 1)
{
mProcessingTimeSum_Rendering += mProcessingTime_Rendering;
}
else
{
mProcessingTimeAverage_Rendering = mProcessingTimeSum_Rendering / cIndexMax;
mProcessingTimeSum_Rendering = 0f;
}
}
public float ProcessingTime_Update { get { return mProcessingTime_Update; } }
public float ProcessingTime_Animation { get { return mProcessingTime_Animation; } }
public float ProcessingTime_LateUpdate { get { return mProcessingTime_LateUpdate; } }
public float ProcessingTime_Rendering { get { return mProcessingTime_Rendering; } }
public float ProcessingTime_Update_NRKernel { get { return mProcessingTime_Update_NRKernel; } }
public float ProcessingTime_LateUpdate_NRKernel { get { return mProcessingTime_LateUpdate_NRKernel; } }
public float ProcessingTimeAverage_Update { get { return mProcessingTimeAverage_Update; } }
public float ProcessingTimeAverage_Animation { get { return mProcessingTimeAverage_Animation; } }
public float ProcessingTimeAverage_LateUpdate { get { return mProcessingTimeAverage_LateUpdate; } }
public float ProcessingTimeAverage_Rendering { get { return mProcessingTimeAverage_Rendering; } }
public float ProcessingTimeAverage_All { get { return ProcessingTimeAverage_Update + ProcessingTimeAverage_Animation + ProcessingTimeAverage_LateUpdate + ProcessingTimeAverage_Rendering; } }
public float ProcessingTimeAverage_Update_NRKernel { get { return mProcessingTimeAverage_Update_NRKernel; } }
public float ProcessingTimeAverage_LateUpdate_NRKernel { get { return mProcessingTimeAverage_LateUpdate_NRKernel; } }
public static Profiling Instance
{
get
{
if (sInstance == null)
{
sInstance = new Profiling();
}
return sInstance;
}
}
private Profiling() { }
private static Profiling sInstance = null;
private float mProcessingTime_Update = 0f;
private float mProcessingTime_Animation = 0f;
private float mProcessingTime_LateUpdate = 0f;
private float mProcessingTime_Rendering = 0f;
private float mProcessingTime_Update_NRKernel = 0f;
private float mProcessingTime_LateUpdate_NRKernel = 0f;
private float mStartTime_Update = 0f;
private float mStartTime_Animation = 0f;
private float mStartTime_LateUpdate = 0f;
private float mStartTime_Rendering = 0f;
private static readonly int cIndexMax = 60;
private int mIndex = 0;
private float mProcessingTimeAverage_Update = 0f;
private float mProcessingTimeAverage_Animation = 0f;
private float mProcessingTimeAverage_LateUpdate = 0f;
private float mProcessingTimeAverage_Rendering = 0f;
private float mProcessingTimeAverage_Update_NRKernel = 0f;
private float mProcessingTimeAverage_LateUpdate_NRKernel = 0f;
private float mProcessingTimeSum_Update = 0f;
private float mProcessingTimeSum_Animation = 0f;
private float mProcessingTimeSum_LateUpdate = 0f;
private float mProcessingTimeSum_Rendering = 0f;
private float mProcessingTimeSum_Update_NRKernel = 0f;
private float mProcessingTimeSum_LateUpdate_NRKernel = 0f;
}
処理計測結果

Editor の Stats で上記の Batch数、頂点数 のシーンにて、
Computing Unit実機(Snapdragon 845)での処理計測結果。
上記コードだと、30秒経過時にCPU処理時間がログ出力される。
(ログは Android Studio の Logcat からコピペ。参考:Nrealアプリの開発環境構築)
| Monoビルド | IL2CPPビルド | |
| Update | 2.2 ms (NRKernel: 0.6 ms) | 1.8 ms (NRKernel: 0.6 ms) |
| Animation | 0.3 ms | 0.2 ms |
| LateUpdate | 0.0 ms (NRKernel: 0.0 ms) | 0.0 ms (NRKernel: 0.0 ms) |
| Rendering | 10.8 ms | 11.1 ms |
| All | 13.4 ms (80 %) | 13.2 ms(79 %) |
ms は ミリ秒。
% は 60fps のときの 16.6ms を基準としたときの割合(100%を超えると処理落ち)
備考1(2022年5月)
- Computing Unit実機、NRSDK 1.8.0 での NRKernel 処理時間は 0.6ms (60fps換算で 3.6%)
- 平面検出、Image Tracking、Hand Tracking などは不使用
- 6DoF移動しても NRKernel 処理時間は変わらなかった
- スクリプト処理が多くなってくると、もっと IL2CPP 優勢になりそう
備考2(2022年7月)
Android実機(OnePlus 8)(Snapdragon 845)、NRSDK 1.8.0 での計測結果。

| Monoビルド | IL2CPPビルド(ARM64) | IL2CPPビルド(ARMv7) | |
| Update | 1.9 ms (NRKernel: 0.4 ms) | 1.4 ms (NRKernel: 0.6 ms) | 1.6 ms (NRKernel: 0.4 ms) |
| Animation | 0.2 ms | 0.2 ms | 0.2 ms |
| LateUpdate | 0.0 ms (NRKernel: 0.0 ms) | 0.0 ms (NRKernel: 0.0 ms) | 0.0 ms (NRKernel: 0.0 ms) |
| Rendering | 10.4 ms | 12.5 ms | 11.8 ms |
| All | 12.5 ms (75 %) | 14.2 ms(85 %) | 13.5 ms (81%) |
Computing Unit と同様に、OnePlus 8 でも IL2CPP のほうが処理負荷が高いという結果になってしまった。
IL2CPP だと Rendering時間が伸びるのは謎。(他スレッドの影響?)
備考3(2022年8月)
Android実機(OnePlus 8)(Snapdragon 845)、NRSDK 1.9.3 での計測結果。

| Mono | Mono_Dev | IL2CPP(ARM64) | IL2CPP(ARMv7) | |
| Update | 2.1 ms (NRKernel: 0.4 ms) | 2.7 ms (NRKernel: 0.5 ms) | 1.6 ms (NRKernel: 0.6 ms) | 1.8 ms (NRKernel: 0.4 ms) |
| Animation | 0.2 ms | 0.2 ms | 0.2 ms | 0.2 ms |
| LateUpdate | 0.0 ms (NRKernel: 0.0 ms) | 0.0 ms (NRKernel: 0.0 ms) | 0.0 ms (NRKernel: 0.0 ms) | 0.0 ms (NRKernel: 0.0 ms) |
| Rendering | 11.3 ms | 10.7 ms | 11.6 ms | 11.4 ms |
| All | 13.6 ms (82 %) | 13.6 ms (82 %) | 13.3 ms(80 %) | 13.4 ms (81%) |
Mono_Dev は、Monoビルドかつ Development Build オン のこと。
今回は、IL2CPP のほうが若干ではあるが処理負荷が低いという結果で、想定通り。
IL2CPP だと Rendering時間が伸びていたのも解消されていた。