【Unity】Unity上からiOS端末の発熱状態を取得する

iOS端末の発熱状態を取得できるAPIが無いか調べていたところ、[ProcessInfo.thermalState
](https://developer.apple.com/documentation/foundation/processinfo/1417480-thermalstate)から取得できることが分かりました。(※ドキュメントを見るに対応OSは`iOS 11.0+`である必要がありそう)

この値を知ることが出来れば、例えば重いシーンなどで状態を見てサーマルスロットリング対策を可変的に機能させる処理を書くことが出来るかもしれません。(e.g. 熱状態がヒドいときに一部の機能のクオリティや更新頻度を下げるなど)

今回はこちらをUnityからも参照できるようにネイティブプラグインも合わせて実装したのでメモ。
[※コードだけ先に見たい方はこちら参照](https://gist.github.com/mao-test-h/f7a6733fc3eea2b73f617717c82d0351)

- Unity 2019.4.15f
- Xcode 12.2


# `ProcessInfo.ThermalState` について

[thermalState](https://developer.apple.com/documentation/foundation/processinfo/1417480-thermalstate)を参照すると[ProcessInfo.ThermalState](https://developer.apple.com/documentation/foundation/processinfo/thermalstate)と言うenum型が返ってきます。
こちらは以下の様に定義されており、現在の端末の発熱状態を知ることが出来ます。

```swift
    // Describes the current thermal state of the system.
    @available(iOS 11.0, *)
    public enum ThermalState : Int {

        // No corrective action is needed.
        case nominal = 0
        
        // The system has reached a state where fans may become audible (on systems which have fans). Recommendation: Defer non-user-visible activity.
        case fair = 1
        
        // Fans are running at maximum speed (on systems which have fans), system performance may be impacted. Recommendation: reduce application's usage of CPU, GPU and I/O, if possible. Switch to lower quality visual effects, reduce frame rates.
        case serious = 2
        
        // System performance is significantly impacted and the system needs to cool down. Recommendation: reduce application's usage of CPU, GPU, and I/O to the minimum level needed to respond to user actions. Consider stopping use of camera and other peripherals if your application is using them.
        case critical = 3
    }
```

コメントには状態及び推奨される解決策などが記載されており、ざっと和訳すると以下のように分類できそうです。

- **nominal**
    - 正常動作の範囲内
- **fair**
    - 熱状態がやや上昇している
    - `推奨される対策:`
        - ユーザーに表示されないアクティビティの遅延
- **serious**
    - 熱状態が高い
        - (ファン搭載機の場合には)最大速度で動作しているために、システムのパフォーマンスに影響を及ぼす可能性がある
    - `推奨される対策:`
        - 可能であればアプリのCPU,GPU,I/Oの使用率を下げる
        - 視覚効果を低品質のものに切り替えるなどしてフレームレートを下げる
- **critical**
    - 熱状態がシステムパフォーマンスに大幅な影響を与えるレベルまで達したので、デバイスを冷やす必要がある
    - `推奨される対策:`
        - アプリのCPU,GPU,I/Oの使用率をユーザーアクションに応答するために必要な最小限レベルまで削減すること
        - アプリがカメラやその他周辺機器を使用している場合には使用を停止することを検討





# Unity上で参照するには

`thermalState`はProcessInfoが持つstatic変数から参照することが出来るので、そのまま流すNativePluginを書くことでUnity上からも参照することが出来ます。(要iOSビルド)

以下にコードを載せておきます。
(※P/Invokeに於ける便宜上、ObjectiveC++で実装してます)

- [Gist Link](https://gist.github.com/mao-test-h/f7a6733fc3eea2b73f617717c82d0351)

```ProcessInfoBridge.mm
#import <Foundation/Foundation.h>

#ifdef __cplusplus
extern "C" {
#endif

// システムの熱状態を取得
// NOTE: マーシャリングの都合でintで返しているが、結果自体はNSProcessInfoThermalStateに準拠
// ref: https://developer.apple.com/documentation/foundation/nsprocessinfothermalstate?language=objc
int thermalState() {
    return (int) [NSProcessInfo.processInfo thermalState];
}

#ifdef __cplusplus
}
#endif
```

```ProcessInfoBridge.cs
using System.Runtime.InteropServices;

namespace iOSNative
{
    /// <summary>
    /// `ProcessInfo`のBridge
    /// </summary>
    /// <remarks>
    /// - https://developer.apple.com/documentation/foundation/processinfo
    /// </remarks>
    public static class ProcessInfoBridge
    {
        /// <summary>
        /// システムの熱状態を取得
        /// </summary>
        /// <returns>
        /// NOTE:
        ///  - iOS実機以外は常に`.Nominal`を返す
        /// </returns>
        public static ThermalState ThermalState
        {
            get
            {
#if !UNITY_EDITOR && UNITY_IOS
                return (ThermalState) ThermalStateNative();
#endif
                return ThermalState.Nominal;
            }
        }

        #region P/Invoke

        [DllImport("__Internal", EntryPoint = "thermalState")]
        static extern int ThermalStateNative();

        #endregion P/Invoke
    }
}
```

```ThermalState.cs
namespace iOSNative
{
    /// <summary>
    /// システムの熱状態
    /// </summary>
    /// <remarks>
    /// - https://developer.apple.com/documentation/foundation/processinfo/thermalstate
    /// </remarks>
    public enum ThermalState
    {
        /// <summary>
        /// The thermal state is within normal limits.
        /// </summary>
        Nominal,

        /// <summary>
        /// The thermal state is slightly elevated.
        /// </summary>
        Fair,

        /// <summary>
        /// The thermal state is high.
        /// </summary>
        Serious,

        /// <summary>
        /// The thermal state is significantly impacting the performance of the system and the device needs to cool down.
        /// </summary>
        Critical,
    }
}
```

スクリプトからは`ProcessInfoWrapperBridge.GetThermalState()`を参照することで状態を取得できます。


## おまけ: 動作確認

簡単な動作確認としてGPUに負荷の掛かりそうな以下のシーンを用意して、手持ちのiPhoneXで動作確認を行ってみました。

シーン自体は大量の半透明なPlaneを重ねて配置しているだけです。(2枚目はOverdraw)

![Shared.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80207/30ca4dad-0648-acc9-9d89-74bb8a90f9b5.png)

![Overdraw.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80207/a4de63e8-1206-f762-0a6f-be3866d1ac99.png)

これをコルーチン上から1秒間隔で熱状態のポーリングを行い、結果を色とテキストで表示するようにします。

```csharp
IEnumerator Check()
{
    // 発熱状態を1秒間隔でポーリング
    while (true)
    {
        var thermalState = ProcessInfoBridge.ThermalState;

        // 結果の表示
        _text.text = thermalState.ToString();
        switch (thermalState)
        {
            case ThermalState.Nominal:
                _text.color = Color.green;
                break;
            case ThermalState.Fair:
                _text.color = Color.yellow;
                break;
            case ThermalState.Serious:
                _text.color = new Color32(255, 165, 0, 255);
                break;
            case ThermalState.Critical:
                _text.color = Color.red;
                break;
            default:
                throw new ArgumentOutOfRangeException();
        }

        yield return new WaitForSeconds(1);
    }
}
```

### 結果

起動したばかりの何も表示されていない状態では`Nominal`となってます。

![iphonex_resize.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80207/7816bb58-b7f4-0b88-9d11-3769f0e1ce55.jpeg)

こちらを先程の大量のPlaneを表示して適当に放置すると、以下のように`Serious`に変わっていることが確認できました。

![IMG_3454_resize.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80207/5d60972c-935e-4d14-609c-7d11897a2de2.png)




# リンク

- [thermalState](https://developer.apple.com/documentation/foundation/processinfo/1417480-thermalstate)
- [ProcessInfo.ThermalState](https://developer.apple.com/documentation/foundation/processinfo/thermalstate)

- [Unity上からiOS端末の発熱状態を取得する - Gist](https://gist.github.com/mao-test-h/f7a6733fc3eea2b73f617717c82d0351)