From 641158d541e40d18594098377f568e9fc668a898 Mon Sep 17 00:00:00 2001 From: ruccho Date: Wed, 28 Oct 2026 15:07:54 +0900 Subject: [PATCH 1/2] Add a performance guide that explains the memory, CPU and GPU cost of recording and the effect of each setting The guide describes where memory is used during recording, the CPU and GPU load of the encoding paths on iOS and Android, the cost of creating a session and exporting, and how the encoding options affect these. It includes the measured values on which the description is based. README links to it from the section on memory usage. --- README.ja.md | 2 + README.md | 2 + docs/performance-guide.ja.md | 226 +++++++++++++++++++++++++++++++++++ docs/performance-guide.md | 226 +++++++++++++++++++++++++++++++++++ 4 files changed, 456 insertions(+) create mode 100644 docs/performance-guide.ja.md create mode 100644 docs/performance-guide.md diff --git a/README.ja.md b/README.ja.md index e8b19d0c..3853325c 100644 --- a/README.ja.md +++ b/README.ja.md @@ -475,6 +475,8 @@ File.Move(outputPath, Path.Combine(Application.persistentDataPath, Path.GetFileN 実行時に使用されるメモリとしては、上記のエンコード済みのデータを保持するバッファに加え、エンコード前の生のフレームや音声サンプルがいくつか保持されます。これはエンコーダーが非同期的に動作する関係で、あるフレームをエンコードしている間に次のフレームを受け取るためです。`VideoInputQueueSize` と `AudioInputQueueSizeSeconds` でそれぞれのキューのサイズを指定できるほか、`MaxNumberOfRawFrameBuffers` (オプション) で圧縮前のフレームを保持するバッファの最大数を指定できます。この値を小さくすることでメモリ使用量を削減できる場合がありますが、フレームドロップの可能性が高まります。 +メモリ使用量や CPU / GPU 負荷の内訳と、各設定の影響については [パフォーマンスガイド](docs/performance-guide.ja.md) を参照してください。 + `IdrIntervalSeconds` は IDR (キー) フレームの最大間隔を秒で指定します。書き出しはキーフレームからしか開始できないため、この値は `StopAndExportAsync` が指定した長さにどれだけ近づけられるかの粒度を決めます。`null` を指定した場合は、プラットフォームのエンコーダー自身の既定値ではなく `VideoEncoderOptions.DefaultIdrIntervalSeconds` (1 秒) が適用されます。エンコーダーの既定値はプラットフォームごとに異なり、著しく長い場合があるためです。間隔を長くするとキーフレームに費やされるビットレートが減りますが、保持している時間より長い間隔を指定するとバッファ内にキーフレームが存在しなくなり、書き出しが何も生成しない場合があります。 ```csharp diff --git a/README.md b/README.md index 9b0c0d95..a90d597a 100644 --- a/README.md +++ b/README.md @@ -479,6 +479,8 @@ Recording uses memory in two places: buffers for compressed output, and buffers `VideoInputQueueSize`, `AudioInputQueueSizeSeconds`, and `MaxNumberOfRawFrameBuffers` (optional) control how many raw frames and audio samples are queued for encoding. These queues are needed because the encoder runs asynchronously, receiving the next frame while encoding the current one. Reducing these values decreases memory usage but may increase the likelihood of dropped frames. +See the [Performance Guide](docs/performance-guide.md) for the breakdown of memory usage and CPU / GPU load, and for the effect of each setting. + `IdrIntervalSeconds` sets the maximum interval between IDR (key) frames. An export can only begin at a key frame, so this bounds how closely `StopAndExportAsync` can match the requested duration. `null` applies `VideoEncoderOptions.DefaultIdrIntervalSeconds`, which is one second, rather than deferring to the platform encoder, whose own default differs per platform and may be considerably longer. A longer interval spends less of the bitrate on key frames, but an interval longer than the retained duration can leave the buffer with no key frame to start from, in which case the export produces nothing. ```csharp diff --git a/docs/performance-guide.ja.md b/docs/performance-guide.ja.md new file mode 100644 index 00000000..361146b8 --- /dev/null +++ b/docs/performance-guide.ja.md @@ -0,0 +1,226 @@ +# パフォーマンスガイド + +本文書は、パフォーマンス要件の厳しいプロジェクトで Instant Replay を利用する開発者を対象に、`RealtimeInstantReplaySession` が使用するメモリと CPU / GPU 処理の内訳、および各設定がそれらに与える影響を説明する。後半には実機での計測結果を掲載する。 + +計測値は特定の端末、シーン、設定における一例であり、他の環境での値を保証するものではない。自身のプロジェクトでの影響は、本文書で説明する方法で実機上で確認すること。本文書の内容は Instant Replay v1.7.0 に基づく。 + +英語版は [performance-guide.md](performance-guide.md) にある。 + +## 処理の概要 + +録画中は、毎フレーム次の処理が行われる。 + +1. フレームのキャプチャ: 既定の `ScreenshotFrameProvider` は、フレームの最後に `ScreenCapture.CaptureScreenshotIntoRenderTexture()` で画面全体を画面サイズの RenderTexture にコピーする。 +2. エンコーダーへの入力: キャプチャしたテクスチャを、エンコードサイズ (`VideoOptions.Width` × `VideoOptions.Height`) に縮小してエンコーダーに渡す。経路は次の 2 種類がある。 + - blit 経路: ネイティブプラグインが render thread 上で GPU を使い、エンコーダーの入力サーフェスに直接描画する。iOS / macOS (Metal) と Android (Vulkan、API 29 以上) で使用される。 + - readback 経路: Unity 側で縮小したテクスチャを `AsyncGPUReadback` で CPU メモリに読み戻し、エンコーダーに渡す。blit 経路に対応しないプラットフォーム (Android の OpenGL ES、Windows、Linux、Web など) と、`ForceReadback` が有効な場合に使用される。 +3. エンコード: プラットフォームのエンコーダー (VideoToolbox、MediaCodec、Media Foundation など) が H.264 と AAC にエンコードする。音声は `OnAudioFilterRead` で取得したサンプルをエンコードする。 +4. バッファリング: エンコード済みのデータを C# 側にコピーし、メモリ上のリングバッファに保持する。ディスクバッファが有効な場合は、ファイルに書き出す。 + +`StopAndExportAsync()` を呼び出すと、バッファ内のデータを MP4 ファイルに多重化 (mux) する。エンコードはやり直さない。 + +## メモリ + +### 内訳 + +| 項目 | サイズの目安 | 条件 | Memory Profiler での表示 | +| --- | --- | --- | --- | +| キャプチャ用 RenderTexture | 画面の幅 × 高さ × 4 バイト | `ScreenshotFrameProvider` を使用する場合 | RenderTexture `InstantReplay ScreenshotFrameProvider` | +| 縮小用 RenderTexture | エンコードの幅 × 高さ × 4 バイト | readback 経路 | RenderTexture `InstantReplay Preprocessing` | +| エンコード前のフレーム (CPU) | エンコードの幅 × 高さ × 4 バイト × 枚数 | readback 経路 | UniEnc のネイティブヒープ (後述) | +| エンコーダーの入力サーフェス | エンコードの幅 × 高さ × 4 バイト × 数枚 | blit 経路 | 表示されない | +| エンコーダー内部のメモリ | 不定 | 常に | 表示されない | +| エンコード済みデータのバッファ | `MaxMemoryUsageBytesForCompressedFrames` の 1.5~2 倍程度 | メモリバッファ | マネージドヒープの `System.Byte[]` | +| ディスクバッファの書き込みキュー | `DiskBufferOptions.MaxPendingWriteBytes` 以下 | ディスクバッファ | マネージドヒープの `System.Byte[]` | +| UniEnc のネイティブヒープ | 32 MiB 単位で確保される | 常に | `UniEnc` / `mimalloc` | + +以下、各項目を補足する。 + +#### キャプチャ用 RenderTexture + +`ScreenshotFrameProvider` は、画面サイズの ARGB32 RenderTexture を 1 枚確保する。エンコードサイズではなく画面サイズに比例するため、高解像度の端末ほど大きくなる。例えば 1080 × 2340 の画面では約 9.8 MB となる。独自の `IFrameProvider` を使用する場合、このテクスチャは確保されない。 + +#### エンコード前のフレーム + +readback 経路では、`AsyncGPUReadback` の読み戻し先として、エンコードサイズ × 4 バイトのバッファをフレームごとに確保する。同時に存在するバッファの数は `MaxNumberOfRawFrameBuffers` で制限できる。既定値 (null) では上限が設けられず、実際の枚数はエンコーダーの処理が追いつくまでの間に読み戻されたフレームの数で決まる。上限に達したフレームは破棄されるため、値を小さくしすぎるとフレーム落ちが起こる。 + +blit 経路では、GPU からエンコーダーの入力サーフェス (iOS では IOSurface を持つ `CVPixelBuffer`、Android では `AHardwareBuffer`) に直接描画するため、CPU 側のバッファは使用しない。入力サーフェスはエンコードサイズ × 4 バイトで、iOS では 2~4 枚、Android では最大 3 枚が確保される。これらは OS が管理するメモリであり、Unity のメモリ管理の対象外である。 + +#### エンコード済みデータのバッファ + +メモリバッファでは、エンコード済みのフレームを `ArrayPool.Shared` から借りた `byte[]` に格納する。`ArrayPool.Shared` は要求サイズを 2 のべき乗に切り上げた配列を返すため、実際に確保される配列の合計は、格納しているデータの合計より大きくなる。`MaxMemoryUsageBytesForCompressedFrames` は格納しているデータの合計に対する上限であり、切り上げ分を含まない。計測では、既定の 20 MiB に対してマネージドヒープの使用量 (GC Used Memory) が 35~50 MB 増加し、60 MiB に対しては約 103 MB 増加した。 + +マネージドヒープは一度拡張されると縮小されにくいため、録画を終了した後も GC Reserved Memory は増加したままとなる。 + +ディスクバッファでは、エンコード済みのフレームは書き込みキューを経由してファイルに書き出され、メモリには保持されない。キューのサイズは `DiskBufferOptions.MaxPendingWriteBytes` (既定値 4 MiB) を上限とする。 + +#### UniEnc のネイティブヒープ + +ネイティブライブラリ (unienc) 内部のメモリ確保は、Unity のメモリマネージャーを経由して行われ、Memory Profiler では `UniEnc` という領域の `mimalloc` というオブジェクトとして表示される。この領域は 32 MiB 単位で確保されるため、Memory Profiler 上の値は実際に使用しているメモリよりも大きく、段階的に増加する。計測では、録画中のスナップショットで iOS、Android ともに約 68 MB と表示された。また、readback 経路のフレームバッファなどの大きな確保を契機として、さらに約 64 MiB 増加する場合があった。確保された領域は、セッションを破棄した後も解放されない。 + +Memory Profiler 上の値はアドレス空間の予約量であり、物理メモリの使用量とは一致しない。物理メモリへの影響は、OS が報告する値 (iOS の `phys_footprint`、Android の PSS) で確認すること。 + +#### Memory Profiler に表示されないメモリ + +エンコーダーの入力サーフェス、ハードウェアエンコーダーの内部バッファ、Android の Java ヒープ上のオブジェクトは Unity のメモリ管理の対象外であり、Memory Profiler では Untracked に含まれるか、まったく表示されない。これらを確認するには、プラットフォームのツールを使用する。 + +- iOS: Xcode の Memory Report、Instruments の VM Tracker (IOSurface の領域) +- Android: `adb shell dumpsys meminfo <パッケージ名>` の Graphics と Private Other + +### Memory Profiler での確認手順 + +1. 録画を開始し、バッファが満たされるまで待つ。メモリバッファでは、`GetBufferStatistics()` で得られる使用量が上限に近づいたことで判断できる。 +2. Memory Profiler (`com.unity.memoryprofiler`) でスナップショットを取得する。 +3. 次の項目を確認する。 + - Unity Objects: `InstantReplay ScreenshotFrameProvider` と `InstantReplay Preprocessing` の RenderTexture + - All Of Memory の Native 以下に表示される `UniEnc` の領域 + - Managed Objects: `System.Byte[]` の合計サイズ。エンコード済みデータの配列は、`UniEnc.EncodedFrame[]` を経由して参照されている。 +4. 同じ操作を録画していない状態でも行い、差分を比較する。 + +## CPU / GPU 処理 + +### Unity のスレッドで行われる処理 + +| スレッド | 処理 | Profiler での表示 | +| --- | --- | --- | +| Main Thread | 画面のキャプチャ | `Awaitable.OnEndOfFrame()` | +| Main Thread | パイプラインの非同期処理の継続 | `UnitySynchronizationContext.ExecuteTasks()` | +| Render Thread | blit 経路の縮小と色変換 | `UniEnc.Unity.GraphicsEventIssuer`、`unienc_android_mc::vulkan::preprocess::blit` (Android)、`unienc_apple_vt::metal::custom_blit` (iOS / macOS) | +| Render Thread | readback 経路の読み戻し | `Gfx.RequestAsyncReadbackData`、`Gfx.UpdateAsyncReadbackData` | +| Audio Mixer Thread | 音声サンプルの取得 | `UnityAudioSampleProviderReceiver.OnAudioFilterRead()` | + +### Unity 外のスレッドで行われる処理 + +エンコーダーへの入力、エンコード済みデータの受け取り、C# 側へのコピーは、ネイティブライブラリのワーカースレッドとプラットフォームのエンコーダーのスレッドで行われる。これらのスレッドは Unity Profiler には表示されないか、名前のないスレッドとして表示される。CPU 使用量を確認するには、プラットフォームのツール (Xcode Instruments の Time Profiler、Android Studio の CPU Profiler や Perfetto) を使用する。Android では、ネイティブライブラリのスレッドは `Thread-<番号>` という名前で表示される。 + +これらの処理はフレームの描画とは並行して行われるため、フレーム時間には直接加算されない。ただし、端末全体の CPU 負荷と消費電力を増やし、発熱によるクロックの低下を通じてフレーム時間に影響することがある。消費電力と発熱については [encoder-power-consumption.md](encoder-power-consumption.md) を参照すること。 + +### 一時的な負荷 + +次の処理は、毎フレームではなく特定の時点で発生する。 + +- セッションの作成: `RealtimeInstantReplaySession` のコンストラクタはエンコーダーを初期化するため、計測では iOS で 30~110 ms、Android で 55~220 ms を要した。いずれもディスクバッファの場合に最も長かった。 +- エクスポート: メモリバッファでは、`StopAndExportAsync()` の処理の一部が main thread 上で同期的に実行され、計測では 95~135 ms のフレームが 1 回発生した。Profiler では `UnitySynchronizationContext.ExecuteTasks()` として表示される。ディスクバッファではこの停止は観測されなかった。 + +## 設定がメモリとフレーム時間に与える影響 + +### バッファの種類 (メモリ / ディスク) + +メモリバッファは、エンコード済みのデータをマネージドヒープに保持する。保持できる長さは `MaxMemoryUsageBytesForCompressedFrames` とビットレートで決まり、マネージドヒープの使用量はその 1.5~2 倍程度増加する。 + +ディスクバッファは、エンコード済みのデータをファイルに書き出すため、マネージドヒープはほとんど増加しない。計測では、満杯時の物理メモリの増加量がメモリバッファの約 40 % (iOS) と約 60 % (Android) となり、エクスポート時の main thread の停止も観測されなかった。一方で、ストレージへの継続的な書き込みによって Android では CPU 使用量が増加したほか、フラッシュメモリの寿命を縮める。ディスクバッファは主に開発ビルドと QA ビルドでの利用を想定している。詳細は [disk-buffered-recording.md](disk-buffered-recording.md) を参照すること。 + +### 画面サイズとエンコードサイズ + +画面サイズは、キャプチャ用 RenderTexture のサイズと、キャプチャおよび縮小の GPU 負荷に影響する。エンコードサイズは、エンコーダーの入力サーフェス、readback 経路のフレームバッファ、エンコーダーの負荷に影響する。readback 経路ではフレームバッファの容量がエンコードサイズに比例するため、エンコードサイズの影響が特に大きい。計測では、Android の readback 経路でエンコードサイズを 1280 × 720 から 1920 × 1080 に変更すると、物理メモリの増加量が約 130 MB から約 210 MB に増加した。 + +### フレームレート + +フレームレート (`FixedFrameRate` と `VideoOptions.FpsHint`) は、毎フレームの処理の回数に比例して CPU と GPU の負荷を増やす。計測では、30 fps から 60 fps に変更すると、プロセス全体の CPU 使用量の増加分が iOS で約 1.9 倍、Android で約 1.5 倍になった。同じバッファサイズで保持できる時間はビットレートで決まるため、フレームレートを上げても短くはならない。 + +### readback 経路 + +blit 経路が利用できるプラットフォームでも、`ForceReadback` を有効にすると readback 経路が使用される。readback 経路は、CPU メモリへの読み戻しとエンコード前のフレームバッファを必要とするため、blit 経路に比べて物理メモリの使用量と CPU 使用量が増加する。計測では、物理メモリの増加量が iOS で約 20 MB、Android で約 60 MB 多く、プロセスの CPU 使用量の増加分も iOS で約 2 倍となった。GPU 時間の増加量は、iOS では readback 経路の方が大きく、Android では blit 経路の方が大きかった。 + +エンコード待ちのフレームの数は `VideoInputQueueSize` (既定値 5) で制限されるが、読み戻し中のフレームはこれに含まれない。エンコーダーの処理が遅れた場合にフレームバッファが増え続けることを防ぐため、readback 経路を使用する場合は `MaxNumberOfRawFrameBuffers` を設定することを推奨する。なお、計測では上限を 3 に設定しても物理メモリの増加量はほとんど変わらなかったため、この設定はメモリ使用量の削減ではなく、増加の防止を目的とするものと考えること。 + +### ビットレート + +ビットレートは、エンコーダーの処理量にはほとんど影響しないが、同じバッファサイズで保持できる時間を決める。保持できる時間は、おおむねバッファサイズ × 8 ÷ (映像のビットレート + 音声のビットレート) 秒である。例えば既定の 20 MiB、2.5 Mbps、128 kbps では約 64 秒となる。保持したい時間が決まっている場合、ビットレートを下げることでバッファサイズを小さくでき、メモリ使用量を削減できる。 + +ただし、Android の一部の端末では、エンコーダーが指定したビットレートに従わないことがある。計測に使用した端末では、1280 × 720 で 1 Mbps と 2.5 Mbps のどちらを指定しても実際のビットレートは約 4.4 Mbps となり、20 MiB で保持できる時間は約 38 秒であった。対象の端末で実際に保持できる時間を `GetBufferStatistics()` で確認すること。 + +### 音声 + +音声の録音を無効にする (`NullAudioSampleProvider` を使用する) と、音声のエンコードが行われなくなる。計測では、プロセスの CPU 使用量の増加分が iOS で約 40 %、Android で約 30 % 減少した。 + +## 推奨事項 + +- メモリバッファのサイズは、ビットレートと必要な録画時間から決定し、マネージドヒープの使用量がその 2 倍程度まで増加することを見込んでおく。 +- readback 経路を使用する場合は、`MaxNumberOfRawFrameBuffers` を設定する。 +- フレームレートとエンコードサイズは、用途に必要な最小限の値にする。特にフレームレートは CPU と GPU の負荷に直接影響する。 +- セッションの作成とエクスポートは、フレーム落ちが許容される時点 (ロード中、リザルト画面など) に行う。 +- Android では、対象の端末でビットレートと保持できる時間を確認する。 +- 物理メモリへの影響は Memory Profiler だけでなく、OS が報告する値でも確認する。 + +## 計測結果 + +### 計測条件 + +| 項目 | iOS | Android | +| --- | --- | --- | +| 端末 | iPhone 12 | Galaxy S22 (SCG13, Snapdragon 8 Gen 1) | +| OS | iOS 26.7 | Android 16 (API 36) | +| グラフィックス API | Metal | Vulkan | +| 画面サイズ | 1170 × 2532 | 1080 × 2340 | + +- Unity 6000.3.26f1、Universal Render Pipeline、IL2CPP、リリースビルドを使用した。 +- 400 個の回転する立方体を描画するシーンを、60 fps を目標に実行した。 +- 各条件でアプリケーションを新たに起動し、録画なしの状態を 20 秒計測した後にセッションを作成して、録画中の状態を 20 秒計測した。表の値は録画なしの状態からの増加量である。 +- 既定の設定は、1280 × 720、30 fps、映像 2.5 Mbps、音声 128 kbps、メモリバッファ 20 MiB である。表の「条件」の列は既定の設定からの変更点を示す。 +- 画面は縦向きであり、横長のエンコードサイズに縮小する際に左右に黒い帯が入る。そのため、映像の情報量は画面全体を使う場合よりも少ない。 +- フレーム時間は `FrameTimingManager` で取得した。プロセスの CPU 使用量は、全スレッドの CPU 時間の合計を経過時間で割った値であり、1000 ms/s が 1 コアの全時間に相当する。 +- 物理メモリは、iOS では `phys_footprint`、Android では PSS の合計である。「開始直後」は録画開始から 10 秒後、「満杯時」はバッファが上限に達した後の値である。 + +### iOS (iPhone 12) + +| 条件 | Main Thread (ms) | GPU (ms) | プロセス CPU (ms/s) | 物理メモリ 開始直後 (MB) | 物理メモリ 満杯時 (MB) | GC Used Memory 満杯時 (MB) | 保持時間 (s) | +| --- | --- | --- | --- | --- | --- | --- | --- | +| 既定 | +0.19 | +0.76 | +70 | +28 | +66 | +44 | 66 | +| 640 × 360 | +0.37 | +0.69 | +80 | +19 | +59 | +43 | 67 | +| 1920 × 1080 | +0.30 | +0.82 | +75 | +40 | +76 | +41 | 66 | +| 60 fps | +0.52 | +0.80 | +132 | +29 | +72 | +49 | 65 | +| 1920 × 1080、60 fps | +0.63 | +0.85 | +139 | +41 | +83 | +47 | 65 | +| 1 Mbps | +0.26 | +0.76 | +73 | +28 | +79 | +48 | 162 | +| 5 Mbps | +0.19 | +0.75 | +57 | +33 | +62 | +40 | 33 | +| 10 Mbps | +0.28 | +0.76 | +78 | +37 | +69 | +42 | 16 | +| readback | +0.22 | +1.18 | +157 | +48 | +86 | +44 | 65 | +| readback、1920 × 1080 | -0.27 | +1.40 | +155 | +86 | +123 | +43 | 65 | +| ディスクバッファ | +0.06 | +0.81 | +70 | +26 | +27 | +2 | 75 | +| 音声なし | +0.24 | +0.77 | +41 | +27 | +64 | +43 | 66 | + +録画なしの状態の値は、Main Thread 約 3.1 ms、GPU 約 2.0 ms、プロセス CPU 約 490 ms/s であった。 + +### Android (Galaxy S22) + +| 条件 | Main Thread (ms) | GPU (ms) | プロセス CPU (ms/s) | 物理メモリ 開始直後 (MB) | 物理メモリ 満杯時 (MB) | GC Used Memory 満杯時 (MB) | 保持時間 (s) | +| --- | --- | --- | --- | --- | --- | --- | --- | +| 既定 | +0.42 | +2.61 | +465 | +45 | +73 | +37 | 38 | +| 640 × 360 | +0.5 | +2.85 | +478 | +40 | +76 | +42 | 63 | +| 1920 × 1080 | +0.62 | +2.67 | +506 | +46 | +82 | +37 | 63 | +| 60 fps | +0.75 | +4.53 | +680 | +43 | +84 | +39 | 38 | +| 1920 × 1080、60 fps | +0.68 | +4.42 | +686 | +43 | +97 | +51 | 63 | +| 1 Mbps | +0.54 | +2.64 | +484 | +43 | +71 | +37 | 38 | +| 5 Mbps | +0.59 | +2.61 | +488 | +46 | +76 | +37 | 32 | +| 10 Mbps | +0.52 | +2.65 | +490 | +52 | +77 | +38 | 16 | +| バッファ 60 MiB | +0.47 | +2.47 | +476 | +47 | +150 | +103 | 117 | +| readback | -0.42 | +0.40 | +570 | +97 | +132 | +33 | 38 | +| readback、`MaxNumberOfRawFrameBuffers` = 3 | -0.39 | +0.40 | +566 | +93 | +130 | +34 | 38 | +| readback、1920 × 1080 | -1.43 | +0.66 | +704 | +170 | +208 | +37 | 63 | +| ディスクバッファ | +0.59 | +2.59 | +551 | +41 | +46 | +2 | 76 | +| 音声なし | +0.27 | +2.53 | +335 | +45 | +80 | +35 | 39 | + +録画なしの状態の値は、Main Thread 約 6.5 ms、GPU 約 2.7 ms、プロセス CPU 約 1010 ms/s であった。640 × 360 の Main Thread の値は中央値の差である (平均値は外れ値の影響を受けていたため)。 + +### Profiler で計測したフレームごとの処理時間 + +開発ビルドで、60 fps で描画し既定の設定 (30 fps) で録画している状態を Unity Profiler で計測し、主なマーカーの描画フレームあたりの平均時間 (self time) を求めた。録画するフレームは描画フレームの半分であるため、録画するフレームあたりの時間はこの値の約 2 倍である。iOS の readback 経路は計測していない。 + +| スレッド | マーカー | iOS blit 経路 (ms) | Android blit 経路 (ms) | Android readback 経路 (ms) | +| --- | --- | --- | --- | --- | +| Main Thread | `Awaitable.OnEndOfFrame()` | 0.06 | 0.19 | 0.25 | +| Main Thread | `UnitySynchronizationContext.ExecuteTasks()` | 0.03 | - | 0.11 | +| Render Thread | `unienc_apple_vt::metal::custom_blit` 以下の合計 | 0.17 | - | - | +| Render Thread | `unienc_android_mc::vulkan::preprocess::blit` 以下の合計 | - | 0.73 | - | +| Render Thread | `UniEnc.Unity.GraphicsEventIssuer` | 0.01 | 0.16 | - | +| Render Thread | `Gfx.RequestAsyncReadbackData` | - | - | 0.46 | +| Render Thread | `Gfx.UpdateAsyncReadbackData` | - | - | 1.53 | +| Audio Mixer Thread | `UnityAudioSampleProviderReceiver.OnAudioFilterRead()` | 0.06 | 0.28 | 0.28 | + +### 考察 + +- iOS では、録画によるフレーム時間の増加は Main Thread、GPU ともに 1 ms 未満であった。Android では、blit 経路で GPU 時間が約 2.6 ms (60 fps では約 4.5 ms) 増加した。 +- Android ではプロセスの CPU 使用量の増加が iOS より大きく、そのほとんどは Unity 外のスレッド (ネイティブライブラリのワーカースレッドと MediaCodec のスレッド) で発生していた。 +- 物理メモリの増加量は、開始直後の時点ではキャプチャ用 RenderTexture、エンコーダー、ネイティブライブラリの初期化によるもので、その後バッファが満たされるにつれてマネージドヒープの増加分が加わる。 +- ディスクバッファでは、満杯時の物理メモリの増加量がメモリバッファより 25~40 MB 少なかった。 +- iOS ではビットレートの指定に従って保持時間が変化したが、Android では 1 Mbps と 2.5 Mbps (既定) の指定で保持時間が変わらなかった。Android でエンコードサイズを変更した場合に保持時間が変化したのは、エンコーダーが実際に生成するビットレートが変化したためである。 diff --git a/docs/performance-guide.md b/docs/performance-guide.md new file mode 100644 index 00000000..ec646d2a --- /dev/null +++ b/docs/performance-guide.md @@ -0,0 +1,226 @@ +# Performance Guide + +This document is intended for developers who use Instant Replay in projects with strict performance requirements. It describes the memory and the CPU / GPU work used by `RealtimeInstantReplaySession`, and how each setting affects them. Measurements on physical devices are given in the second half. + +The measured values are examples for specific devices, scenes and settings, and are not guaranteed for other environments. Verify the impact on your own project on physical devices, using the methods described in this document. The content of this document is based on Instant Replay v1.7.0. + +A Japanese version of this document is available in [performance-guide.ja.md](performance-guide.ja.md). + +## Overview of the Processing + +During recording, the following steps are performed on every recorded frame. + +1. Frame capture: the default `ScreenshotFrameProvider` copies the whole screen into a screen-sized RenderTexture with `ScreenCapture.CaptureScreenshotIntoRenderTexture()` at the end of the frame. +2. Input to the encoder: the captured texture is scaled down to the encoding size (`VideoOptions.Width` × `VideoOptions.Height`) and passed to the encoder. There are two paths. + - Blit path: the native plugin draws directly into the input surface of the encoder, using the GPU on the render thread. It is used on iOS / macOS (Metal) and on Android (Vulkan, API 29 or later). + - Readback path: the texture scaled down on the Unity side is read back into CPU memory with `AsyncGPUReadback` and passed to the encoder. It is used on platforms that do not support the blit path (OpenGL ES on Android, Windows, Linux, Web and others), and when `ForceReadback` is enabled. +3. Encoding: the encoder of the platform (VideoToolbox, MediaCodec, Media Foundation and others) encodes the video into H.264 and the audio into AAC. The audio samples are obtained with `OnAudioFilterRead`. +4. Buffering: the encoded data is copied to the C# side and held in a ring buffer in memory. When the disk buffer is enabled, it is written to files instead. + +Calling `StopAndExportAsync()` multiplexes (muxes) the data in the buffer into an MP4 file. The data is not encoded again. + +## Memory + +### Breakdown + +| Item | Approximate size | Condition | Shown in Memory Profiler as | +| --- | --- | --- | --- | +| RenderTexture for capture | Screen width × height × 4 bytes | When `ScreenshotFrameProvider` is used | RenderTexture `InstantReplay ScreenshotFrameProvider` | +| RenderTexture for scaling | Encoding width × height × 4 bytes | Readback path | RenderTexture `InstantReplay Preprocessing` | +| Frames before encoding (CPU) | Encoding width × height × 4 bytes × number of frames | Readback path | Native heap of UniEnc (described below) | +| Input surfaces of the encoder | Encoding width × height × 4 bytes × a few | Blit path | Not shown | +| Internal memory of the encoder | Unknown | Always | Not shown | +| Buffer of encoded data | About 1.5 to 2 times `MaxMemoryUsageBytesForCompressedFrames` | Memory buffer | `System.Byte[]` in the managed heap | +| Write queue of the disk buffer | Up to `DiskBufferOptions.MaxPendingWriteBytes` | Disk buffer | `System.Byte[]` in the managed heap | +| Native heap of UniEnc | Allocated in units of 32 MiB | Always | `UniEnc` / `mimalloc` | + +Each item is described below. + +#### RenderTexture for capture + +`ScreenshotFrameProvider` allocates one ARGB32 RenderTexture of the screen size. Its size is proportional to the screen size rather than to the encoding size, so it is larger on devices with a higher resolution. For example, it is approximately 9.8 MB for a 1080 × 2340 screen. This texture is not allocated when a custom `IFrameProvider` is used. + +#### Frames before encoding + +On the readback path, a buffer of the encoding size × 4 bytes is allocated for each frame as the destination of `AsyncGPUReadback`. The number of buffers that exist at the same time can be limited with `MaxNumberOfRawFrameBuffers`. With the default value (null), no limit is applied, and the actual number is determined by how many frames are read back before the encoder catches up. Frames that exceed the limit are dropped, so a value that is too small causes frame drops. + +On the blit path, the GPU draws directly into the input surfaces of the encoder (`CVPixelBuffer` backed by IOSurface on iOS, `AHardwareBuffer` on Android), so no buffer on the CPU side is used. Each input surface is the encoding size × 4 bytes; two to four are allocated on iOS and up to three on Android. They are memory managed by the OS and are outside the memory management of Unity. + +#### Buffer of encoded data + +With the memory buffer, each encoded frame is stored in a `byte[]` rented from `ArrayPool.Shared`. Because `ArrayPool.Shared` returns arrays whose sizes are rounded up to a power of two, the total size of the allocated arrays is larger than the total size of the stored data. `MaxMemoryUsageBytesForCompressedFrames` limits the total size of the stored data and does not include the rounding. In the measurements, the managed heap usage (GC Used Memory) increased by 35 to 50 MB with the default of 20 MiB, and by approximately 103 MB with 60 MiB. + +Once the managed heap has grown, it rarely shrinks, so GC Reserved Memory remains increased after recording ends. + +With the disk buffer, encoded frames are written to files through a write queue and are not held in memory. The size of the queue is limited by `DiskBufferOptions.MaxPendingWriteBytes` (4 MiB by default). + +#### Native heap of UniEnc + +Memory allocated inside the native library (unienc) is obtained through the memory manager of Unity, and is shown in Memory Profiler as the object `mimalloc` in the area `UniEnc`. Because this area is allocated in units of 32 MiB, the value in Memory Profiler is larger than the memory actually in use and grows in steps. In the measurements, snapshots taken during recording showed approximately 68 MB on both iOS and Android. In addition, a large allocation, such as the frame buffers of the readback path, sometimes triggered a further increase of approximately 64 MiB. The allocated area is not released after the session is disposed. + +The value in Memory Profiler is the amount of reserved address space and does not match the physical memory usage. Check the effect on physical memory with the values reported by the OS (`phys_footprint` on iOS, PSS on Android). + +#### Memory not shown in Memory Profiler + +The input surfaces of the encoder, the internal buffers of the hardware encoder, and objects on the Java heap on Android are outside the memory management of Unity. In Memory Profiler, they are included in Untracked or are not shown at all. Use the tools of the platform to examine them. + +- iOS: the Memory Report of Xcode, and the VM Tracker instrument of Instruments (IOSurface regions) +- Android: Graphics and Private Other in `adb shell dumpsys meminfo ` + +### Examining with Memory Profiler + +1. Start recording and wait until the buffer is full. With the memory buffer, this can be determined by the used size obtained from `GetBufferStatistics()` approaching the limit. +2. Take a snapshot with Memory Profiler (`com.unity.memoryprofiler`). +3. Examine the following items. + - Unity Objects: the RenderTextures `InstantReplay ScreenshotFrameProvider` and `InstantReplay Preprocessing` + - The `UniEnc` area under Native in All Of Memory + - Managed Objects: the total size of `System.Byte[]`. The arrays of encoded data are referenced through `UniEnc.EncodedFrame[]`. +4. Perform the same steps without recording, and compare the two snapshots. + +## CPU / GPU Work + +### Work on Unity threads + +| Thread | Work | Shown in the Profiler as | +| --- | --- | --- | +| Main Thread | Screen capture | `Awaitable.OnEndOfFrame()` | +| Main Thread | Continuations of the asynchronous pipeline | `UnitySynchronizationContext.ExecuteTasks()` | +| Render Thread | Scaling and color conversion on the blit path | `UniEnc.Unity.GraphicsEventIssuer`, `unienc_android_mc::vulkan::preprocess::blit` (Android), `unienc_apple_vt::metal::custom_blit` (iOS / macOS) | +| Render Thread | Readback on the readback path | `Gfx.RequestAsyncReadbackData`, `Gfx.UpdateAsyncReadbackData` | +| Audio Mixer Thread | Acquisition of audio samples | `UnityAudioSampleProviderReceiver.OnAudioFilterRead()` | + +### Work outside Unity threads + +The input to the encoder, the reception of encoded data and its copy to the C# side are performed on the worker threads of the native library and on the threads of the encoder of the platform. These threads are either not shown in the Unity Profiler or are shown as unnamed threads. Use the tools of the platform (Time Profiler of Xcode Instruments, the CPU Profiler of Android Studio, or Perfetto) to examine their CPU usage. On Android, the threads of the native library appear with names of the form `Thread-`. + +This work runs in parallel with frame rendering and is not added directly to the frame time. However, it increases the CPU load and the power consumption of the whole device, and can affect the frame time through clock reductions caused by heat. See [encoder-power-consumption.md](encoder-power-consumption.md) for power consumption and heat. + +### Temporary load + +The following work occurs at specific points rather than on every frame. + +- Session creation: the constructor of `RealtimeInstantReplaySession` initializes the encoders. In the measurements, it took 30 to 110 ms on iOS and 55 to 220 ms on Android, the longest being with the disk buffer on both platforms. +- Export: with the memory buffer, part of the processing of `StopAndExportAsync()` runs synchronously on the main thread, and in the measurements one frame of 95 to 135 ms occurred. It appears as `UnitySynchronizationContext.ExecuteTasks()` in the Profiler. This stall was not observed with the disk buffer. + +## Effect of Settings on Memory and Frame Time + +### Type of buffer (memory or disk) + +The memory buffer holds the encoded data in the managed heap. The duration that can be held is determined by `MaxMemoryUsageBytesForCompressedFrames` and the bitrate, and the managed heap usage increases by about 1.5 to 2 times that size. + +The disk buffer writes the encoded data to files, so the managed heap hardly increases. In the measurements, the increase in physical memory with a full buffer was approximately 40 % (iOS) and 60 % (Android) of that with the memory buffer, and no stall of the main thread was observed during export. On the other hand, the continuous writes to storage increased the CPU usage on Android and shorten the lifespan of flash memory. The disk buffer is intended primarily for development and quality-assurance builds. See [disk-buffered-recording.md](disk-buffered-recording.md) for details. + +### Screen size and encoding size + +The screen size affects the size of the RenderTexture for capture and the GPU load of capturing and scaling. The encoding size affects the input surfaces of the encoder, the frame buffers of the readback path, and the load of the encoder. On the readback path, the capacity of the frame buffers is proportional to the encoding size, so the encoding size has a particularly large effect. In the measurements, changing the encoding size from 1280 × 720 to 1920 × 1080 on the readback path on Android raised the increase in physical memory from approximately 130 MB to approximately 210 MB. + +### Frame rate + +The frame rate (`FixedFrameRate` and `VideoOptions.FpsHint`) increases the CPU and GPU load in proportion to the number of frames processed. In the measurements, changing from 30 fps to 60 fps multiplied the increase in CPU usage of the whole process by approximately 1.9 on iOS and 1.5 on Android. The duration that a buffer of a given size can hold is determined by the bitrate, so raising the frame rate does not shorten it. + +### Readback path + +Even on platforms where the blit path is available, enabling `ForceReadback` selects the readback path. Because the readback path requires a readback into CPU memory and frame buffers before encoding, it uses more physical memory and more CPU than the blit path. In the measurements, the increase in physical memory was larger by approximately 20 MB on iOS and 60 MB on Android, and the increase in CPU usage of the process was approximately twice as large on iOS. The increase in GPU time was larger with the readback path on iOS and with the blit path on Android. + +The number of frames waiting for the encoder is limited by `VideoInputQueueSize` (5 by default), but frames being read back are not included in it. Setting `MaxNumberOfRawFrameBuffers` is recommended when the readback path is used, to prevent the frame buffers from growing when the encoder falls behind. In the measurements, a limit of 3 hardly changed the increase in physical memory, so regard this setting as a means of preventing growth rather than of reducing memory usage. + +### Bitrate + +The bitrate hardly affects the amount of work of the encoder, but it determines the duration that a buffer of a given size can hold. The duration is approximately the buffer size × 8 ÷ (video bitrate + audio bitrate) seconds. For example, it is approximately 64 seconds with the default 20 MiB, 2.5 Mbps and 128 kbps. When the required duration is fixed, lowering the bitrate allows a smaller buffer and reduces memory usage. + +However, the encoder does not follow the specified bitrate on some Android devices. On the device used for the measurements, the actual bitrate at 1280 × 720 was approximately 4.4 Mbps whether 1 Mbps or 2.5 Mbps was specified, and 20 MiB held approximately 38 seconds. Check the duration actually held on the target devices with `GetBufferStatistics()`. + +### Audio + +Disabling audio recording (using `NullAudioSampleProvider`) removes the audio encoding. In the measurements, the increase in CPU usage of the process decreased by approximately 40 % on iOS and 30 % on Android. + +## Recommendations + +- Determine the size of the memory buffer from the bitrate and the required duration, and allow for the managed heap usage to increase by up to about twice that size. +- Set `MaxNumberOfRawFrameBuffers` when the readback path is used. +- Use the smallest frame rate and encoding size that meet the purpose. The frame rate in particular directly affects the CPU and GPU load. +- Create sessions and export at points where frame drops are acceptable, such as during loading or on a result screen. +- On Android, check the bitrate and the duration held on the target devices. +- Check the effect on physical memory with the values reported by the OS, not only with Memory Profiler. + +## Measurement Results + +### Conditions + +| Item | iOS | Android | +| --- | --- | --- | +| Device | iPhone 12 | Galaxy S22 (SCG13, Snapdragon 8 Gen 1) | +| OS | iOS 26.7 | Android 16 (API 36) | +| Graphics API | Metal | Vulkan | +| Screen size | 1170 × 2532 | 1080 × 2340 | + +- Unity 6000.3.26f1, the Universal Render Pipeline, IL2CPP and release builds were used. +- A scene rendering 400 rotating cubes was run with a target of 60 fps. +- For each condition, the application was launched anew, the state without recording was measured for 20 seconds, a session was created, and the state during recording was measured for 20 seconds. The values in the tables are increases from the state without recording. +- The default settings are 1280 × 720, 30 fps, 2.5 Mbps video, 128 kbps audio and a 20 MiB memory buffer. The "Condition" column shows the changes from the default settings. +- The screen was in portrait orientation, so black bars were added to the left and right when it was scaled to the landscape encoding size. The video therefore contained less information than a video using the whole frame. +- Frame times were obtained with `FrameTimingManager`. The CPU usage of the process is the sum of the CPU time of all threads divided by the elapsed time; 1000 ms/s corresponds to the full time of one core. +- Physical memory is `phys_footprint` on iOS and the total PSS on Android. "Start" is the value 10 seconds after recording started, and "Full" is the value after the buffer reached its limit. + +### iOS (iPhone 12) + +| Condition | Main Thread (ms) | GPU (ms) | Process CPU (ms/s) | Physical memory, start (MB) | Physical memory, full (MB) | GC Used Memory, full (MB) | Duration held (s) | +| --- | --- | --- | --- | --- | --- | --- | --- | +| Default | +0.19 | +0.76 | +70 | +28 | +66 | +44 | 66 | +| 640 × 360 | +0.37 | +0.69 | +80 | +19 | +59 | +43 | 67 | +| 1920 × 1080 | +0.30 | +0.82 | +75 | +40 | +76 | +41 | 66 | +| 60 fps | +0.52 | +0.80 | +132 | +29 | +72 | +49 | 65 | +| 1920 × 1080, 60 fps | +0.63 | +0.85 | +139 | +41 | +83 | +47 | 65 | +| 1 Mbps | +0.26 | +0.76 | +73 | +28 | +79 | +48 | 162 | +| 5 Mbps | +0.19 | +0.75 | +57 | +33 | +62 | +40 | 33 | +| 10 Mbps | +0.28 | +0.76 | +78 | +37 | +69 | +42 | 16 | +| Readback | +0.22 | +1.18 | +157 | +48 | +86 | +44 | 65 | +| Readback, 1920 × 1080 | -0.27 | +1.40 | +155 | +86 | +123 | +43 | 65 | +| Disk buffer | +0.06 | +0.81 | +70 | +26 | +27 | +2 | 75 | +| No audio | +0.24 | +0.77 | +41 | +27 | +64 | +43 | 66 | + +Without recording, the main thread took approximately 3.1 ms, the GPU approximately 2.0 ms, and the process CPU usage was approximately 490 ms/s. + +### Android (Galaxy S22) + +| Condition | Main Thread (ms) | GPU (ms) | Process CPU (ms/s) | Physical memory, start (MB) | Physical memory, full (MB) | GC Used Memory, full (MB) | Duration held (s) | +| --- | --- | --- | --- | --- | --- | --- | --- | +| Default | +0.42 | +2.61 | +465 | +45 | +73 | +37 | 38 | +| 640 × 360 | +0.5 | +2.85 | +478 | +40 | +76 | +42 | 63 | +| 1920 × 1080 | +0.62 | +2.67 | +506 | +46 | +82 | +37 | 63 | +| 60 fps | +0.75 | +4.53 | +680 | +43 | +84 | +39 | 38 | +| 1920 × 1080, 60 fps | +0.68 | +4.42 | +686 | +43 | +97 | +51 | 63 | +| 1 Mbps | +0.54 | +2.64 | +484 | +43 | +71 | +37 | 38 | +| 5 Mbps | +0.59 | +2.61 | +488 | +46 | +76 | +37 | 32 | +| 10 Mbps | +0.52 | +2.65 | +490 | +52 | +77 | +38 | 16 | +| 60 MiB buffer | +0.47 | +2.47 | +476 | +47 | +150 | +103 | 117 | +| Readback | -0.42 | +0.40 | +570 | +97 | +132 | +33 | 38 | +| Readback, `MaxNumberOfRawFrameBuffers` = 3 | -0.39 | +0.40 | +566 | +93 | +130 | +34 | 38 | +| Readback, 1920 × 1080 | -1.43 | +0.66 | +704 | +170 | +208 | +37 | 63 | +| Disk buffer | +0.59 | +2.59 | +551 | +41 | +46 | +2 | 76 | +| No audio | +0.27 | +2.53 | +335 | +45 | +80 | +35 | 39 | + +Without recording, the main thread took approximately 6.5 ms, the GPU approximately 2.7 ms, and the process CPU usage was approximately 1010 ms/s. The main thread value for 640 × 360 is the difference of the medians, because the mean was affected by outliers. + +### Per-frame time measured with the Profiler + +The state of rendering at 60 fps while recording with the default settings (30 fps) was measured with the Unity Profiler in development builds, and the mean time (self time) per rendered frame was obtained for the main markers. Because half of the rendered frames are recorded, the time per recorded frame is approximately twice these values. The readback path on iOS was not measured. + +| Thread | Marker | iOS, blit path (ms) | Android, blit path (ms) | Android, readback path (ms) | +| --- | --- | --- | --- | --- | +| Main Thread | `Awaitable.OnEndOfFrame()` | 0.06 | 0.19 | 0.25 | +| Main Thread | `UnitySynchronizationContext.ExecuteTasks()` | 0.03 | - | 0.11 | +| Render Thread | Total under `unienc_apple_vt::metal::custom_blit` | 0.17 | - | - | +| Render Thread | Total under `unienc_android_mc::vulkan::preprocess::blit` | - | 0.73 | - | +| Render Thread | `UniEnc.Unity.GraphicsEventIssuer` | 0.01 | 0.16 | - | +| Render Thread | `Gfx.RequestAsyncReadbackData` | - | - | 0.46 | +| Render Thread | `Gfx.UpdateAsyncReadbackData` | - | - | 1.53 | +| Audio Mixer Thread | `UnityAudioSampleProviderReceiver.OnAudioFilterRead()` | 0.06 | 0.28 | 0.28 | + +### Observations + +- On iOS, recording increased both the main thread time and the GPU time by less than 1 ms. On Android, the blit path increased the GPU time by approximately 2.6 ms (approximately 4.5 ms at 60 fps). +- The increase in CPU usage of the process was larger on Android than on iOS, and most of it occurred outside Unity threads (on the worker threads of the native library and the threads of MediaCodec). +- The increase in physical memory at the start is due to the RenderTexture for capture, the encoders and the initialization of the native library; the growth of the managed heap is added as the buffer fills. +- With the disk buffer, the increase in physical memory with a full buffer was 25 to 40 MB smaller than with the memory buffer. +- On iOS, the duration held changed according to the specified bitrate, whereas on Android it did not change between 1 Mbps and 2.5 Mbps (the default). On Android, the duration held changed with the encoding size because the bitrate actually produced by the encoder changed. From f2e1df6e45adc8ea5a6a6d46b823973dabcd8a73 Mon Sep 17 00:00:00 2001 From: ruccho Date: Thu, 29 Oct 2026 16:24:55 +0900 Subject: [PATCH 2/2] Describe the memory buffer, the export and the Android CPU usage as changed after v1.7.0 The guide now describes the versions after v1.7.0. The measured tables remain those of v1.7.0, and a new section gives the values measured after the changes on Android. --- docs/performance-guide.ja.md | 44 ++++++++++++++++++++++++++++-------- docs/performance-guide.md | 44 ++++++++++++++++++++++++++++-------- 2 files changed, 70 insertions(+), 18 deletions(-) diff --git a/docs/performance-guide.ja.md b/docs/performance-guide.ja.md index 361146b8..1fc3a752 100644 --- a/docs/performance-guide.ja.md +++ b/docs/performance-guide.ja.md @@ -2,7 +2,7 @@ 本文書は、パフォーマンス要件の厳しいプロジェクトで Instant Replay を利用する開発者を対象に、`RealtimeInstantReplaySession` が使用するメモリと CPU / GPU 処理の内訳、および各設定がそれらに与える影響を説明する。後半には実機での計測結果を掲載する。 -計測値は特定の端末、シーン、設定における一例であり、他の環境での値を保証するものではない。自身のプロジェクトでの影響は、本文書で説明する方法で実機上で確認すること。本文書の内容は Instant Replay v1.7.0 に基づく。 +計測値は特定の端末、シーン、設定における一例であり、他の環境での値を保証するものではない。自身のプロジェクトでの影響は、本文書で説明する方法で実機上で確認すること。本文書の説明は、[v1.7.0 以降の変更](#v170-以降の変更) に記載した変更を含む、Instant Replay v1.7.0 より後のバージョンを対象とする。計測値は、特に記載がない限り v1.7.0 で計測したものである。 英語版は [performance-guide.md](performance-guide.md) にある。 @@ -30,7 +30,7 @@ | エンコード前のフレーム (CPU) | エンコードの幅 × 高さ × 4 バイト × 枚数 | readback 経路 | UniEnc のネイティブヒープ (後述) | | エンコーダーの入力サーフェス | エンコードの幅 × 高さ × 4 バイト × 数枚 | blit 経路 | 表示されない | | エンコーダー内部のメモリ | 不定 | 常に | 表示されない | -| エンコード済みデータのバッファ | `MaxMemoryUsageBytesForCompressedFrames` の 1.5~2 倍程度 | メモリバッファ | マネージドヒープの `System.Byte[]` | +| エンコード済みデータのバッファ | `MaxMemoryUsageBytesForCompressedFrames` 以下 | メモリバッファ | マネージドヒープの 64 KiB の `System.Byte[]` | | ディスクバッファの書き込みキュー | `DiskBufferOptions.MaxPendingWriteBytes` 以下 | ディスクバッファ | マネージドヒープの `System.Byte[]` | | UniEnc のネイティブヒープ | 32 MiB 単位で確保される | 常に | `UniEnc` / `mimalloc` | @@ -48,7 +48,11 @@ blit 経路では、GPU からエンコーダーの入力サーフェス (iOS #### エンコード済みデータのバッファ -メモリバッファでは、エンコード済みのフレームを `ArrayPool.Shared` から借りた `byte[]` に格納する。`ArrayPool.Shared` は要求サイズを 2 のべき乗に切り上げた配列を返すため、実際に確保される配列の合計は、格納しているデータの合計より大きくなる。`MaxMemoryUsageBytesForCompressedFrames` は格納しているデータの合計に対する上限であり、切り上げ分を含まない。計測では、既定の 20 MiB に対してマネージドヒープの使用量 (GC Used Memory) が 35~50 MB 増加し、60 MiB に対しては約 103 MB 増加した。 +メモリバッファでは、エンコード済みのフレームをマネージドヒープ上の 64 KiB のチャンクにコピーする。映像と音声のストリームは 1 つのプールからチャンクを取得し、チャンクの合計サイズは `MaxMemoryUsageBytesForCompressedFrames` を超えない。チャンクは初めて必要になった時点で確保され、含まれるフレームがすべて破棄された後に再利用されるため、マネージドヒープはバッファが満たされるまで増加し、バッファが上限に達した時点で増加が止まる。各チャンクは大きなオブジェクトのヒープ (LOH) の閾値より小さい。バッファが満杯のときは、映像と音声を通じてタイムスタンプが古い順にフレームが破棄される。チャンクはセッションを破棄した時点で解放される。 + +Android での計測では、満杯時のマネージドヒープの使用量 (GC Used Memory) が、既定の 20 MiB に対して約 23 MB、60 MiB に対して約 70 MB 増加した。これらの値には、バッファ以外の数 MB の確保が含まれる。 + +v1.7.0 では、エンコード済みのフレームを `ArrayPool.Shared` から借りた `byte[]` に格納していた。`ArrayPool.Shared` は要求サイズを 2 のべき乗に切り上げた配列を返すため、マネージドヒープの使用量は 20 MiB に対して 35~50 MB、60 MiB に対して約 103 MB 増加した。また、エクスポートを行った場合、セッションを破棄した後もバッファがマネージドヒープに残ることがあった。 マネージドヒープは一度拡張されると縮小されにくいため、録画を終了した後も GC Reserved Memory は増加したままとなる。 @@ -74,7 +78,7 @@ Memory Profiler 上の値はアドレス空間の予約量であり、物理メ 3. 次の項目を確認する。 - Unity Objects: `InstantReplay ScreenshotFrameProvider` と `InstantReplay Preprocessing` の RenderTexture - All Of Memory の Native 以下に表示される `UniEnc` の領域 - - Managed Objects: `System.Byte[]` の合計サイズ。エンコード済みデータの配列は、`UniEnc.EncodedFrame[]` を経由して参照されている。 + - Managed Objects: `System.Byte[]` の合計サイズ。エンコード済みデータのチャンクは 65,536 バイトの配列であり、`InstantReplay.EncodedFrameTrackStorage` と `InstantReplay.EncodedFrameChunkPool` を経由して参照されている。 4. 同じ操作を録画していない状態でも行い、差分を比較する。 ## CPU / GPU 処理 @@ -100,15 +104,15 @@ Memory Profiler 上の値はアドレス空間の予約量であり、物理メ 次の処理は、毎フレームではなく特定の時点で発生する。 - セッションの作成: `RealtimeInstantReplaySession` のコンストラクタはエンコーダーを初期化するため、計測では iOS で 30~110 ms、Android で 55~220 ms を要した。いずれもディスクバッファの場合に最も長かった。 -- エクスポート: メモリバッファでは、`StopAndExportAsync()` の処理の一部が main thread 上で同期的に実行され、計測では 95~135 ms のフレームが 1 回発生した。Profiler では `UnitySynchronizationContext.ExecuteTasks()` として表示される。ディスクバッファではこの停止は観測されなかった。 +- エクスポート: `StopAndExportAsync()` は、映像と音声の多重化をスレッドプール上で行う。Android でメモリバッファを使用した計測では、エクスポート中の最長のフレームは 33~50 ms であった。v1.7.0 では、多重化と、既定の出力ファイル名に使用するローカルタイムゾーンの初回の初期化が main thread 上で行われ、95~135 ms のフレームが 1 回発生していた。現在は、タイムゾーンはセッションの作成時にスレッドプール上で初期化される。スレッドを持たない Web では、これらの処理は引き続き main thread 上で行われる。 ## 設定がメモリとフレーム時間に与える影響 ### バッファの種類 (メモリ / ディスク) -メモリバッファは、エンコード済みのデータをマネージドヒープに保持する。保持できる長さは `MaxMemoryUsageBytesForCompressedFrames` とビットレートで決まり、マネージドヒープの使用量はその 1.5~2 倍程度増加する。 +メモリバッファは、エンコード済みのデータをマネージドヒープに保持する。保持できる長さは `MaxMemoryUsageBytesForCompressedFrames` とビットレートで決まり、マネージドヒープの使用量は最大でそのサイズ程度増加する。 -ディスクバッファは、エンコード済みのデータをファイルに書き出すため、マネージドヒープはほとんど増加しない。計測では、満杯時の物理メモリの増加量がメモリバッファの約 40 % (iOS) と約 60 % (Android) となり、エクスポート時の main thread の停止も観測されなかった。一方で、ストレージへの継続的な書き込みによって Android では CPU 使用量が増加したほか、フラッシュメモリの寿命を縮める。ディスクバッファは主に開発ビルドと QA ビルドでの利用を想定している。詳細は [disk-buffered-recording.md](disk-buffered-recording.md) を参照すること。 +ディスクバッファは、エンコード済みのデータをファイルに書き出すため、マネージドヒープはほとんど増加しない。v1.7.0 での計測では、満杯時の物理メモリの増加量がメモリバッファの約 40 % (iOS) と約 60 % (Android) となった。それ以降のバージョンでは、メモリバッファがフレームごとのサイズの切り上げを行わなくなったため、この差は小さくなる。一方で、ストレージへの継続的な書き込みによって Android では CPU 使用量が増加したほか、フラッシュメモリの寿命を縮める。ディスクバッファは主に開発ビルドと QA ビルドでの利用を想定している。詳細は [disk-buffered-recording.md](disk-buffered-recording.md) を参照すること。 ### 画面サイズとエンコードサイズ @@ -136,15 +140,17 @@ blit 経路が利用できるプラットフォームでも、`ForceReadback` ## 推奨事項 -- メモリバッファのサイズは、ビットレートと必要な録画時間から決定し、マネージドヒープの使用量がその 2 倍程度まで増加することを見込んでおく。 +- メモリバッファのサイズは、ビットレートと必要な録画時間から決定し、マネージドヒープの使用量がそのサイズ程度増加することを見込んでおく。 - readback 経路を使用する場合は、`MaxNumberOfRawFrameBuffers` を設定する。 - フレームレートとエンコードサイズは、用途に必要な最小限の値にする。特にフレームレートは CPU と GPU の負荷に直接影響する。 -- セッションの作成とエクスポートは、フレーム落ちが許容される時点 (ロード中、リザルト画面など) に行う。 +- セッションの作成は、フレーム落ちが許容される時点 (ロード中、リザルト画面など) に行う。エクスポートによって main thread が長時間停止することはなくなったが、エクスポート中のフレームには処理が加わる。 - Android では、対象の端末でビットレートと保持できる時間を確認する。 - 物理メモリへの影響は Memory Profiler だけでなく、OS が報告する値でも確認する。 ## 計測結果 +本節の値は v1.7.0 で計測したものである。それ以降の変更の影響を受ける値については、[v1.7.0 以降の変更](#v170-以降の変更) に記載する。 + ### 計測条件 | 項目 | iOS | Android | @@ -224,3 +230,23 @@ blit 経路が利用できるプラットフォームでも、`ForceReadback` - 物理メモリの増加量は、開始直後の時点ではキャプチャ用 RenderTexture、エンコーダー、ネイティブライブラリの初期化によるもので、その後バッファが満たされるにつれてマネージドヒープの増加分が加わる。 - ディスクバッファでは、満杯時の物理メモリの増加量がメモリバッファより 25~40 MB 少なかった。 - iOS ではビットレートの指定に従って保持時間が変化したが、Android では 1 Mbps と 2.5 Mbps (既定) の指定で保持時間が変わらなかった。Android でエンコードサイズを変更した場合に保持時間が変化したのは、エンコーダーが実際に生成するビットレートが変化したためである。 + +### v1.7.0 以降の変更 + +v1.7.0 の後に次の変更が行われた。その効果を、Galaxy S22 上で上記と同じ条件により、既定の設定とバッファ 60 MiB の設定で計測した。iOS では再計測していない。 + +- エンコード済みデータのバッファは、2 のべき乗に切り上げた配列ではなく、`MaxMemoryUsageBytesForCompressedFrames` 以内の 64 KiB のチャンクにフレームを格納する。 +- セッションが保持するメモリは、エクスポートを行った場合も、セッションを破棄した時点で解放される。 +- エクスポート時に、多重化とローカルタイムゾーンの初回の初期化が main thread 上で行われなくなった。 +- Android では、ネイティブライブラリのスレッドの CPU 使用量が減少した。MediaCodec の待機を 1 回ずつスレッドプールに渡す処理をなくし、エンコーダーの入力サーフェスごとに作成する Vulkan のオブジェクトを再利用し、Java のメソッドの検索結果をキャッシュするようにした。 + +| 項目 | 条件 | v1.7.0 | 変更後 | +| --- | --- | --- | --- | +| GC Used Memory 満杯時 (MB) | 既定 | +38~+39 | +23~+24 | +| GC Used Memory 満杯時 (MB) | バッファ 60 MiB | +108 | +70~+71 | +| 物理メモリ 満杯時 (MB) | 既定 | +66~+71 | +47~+60 | +| 物理メモリ 満杯時 (MB) | バッファ 60 MiB | +151~+153 | +117~+120 | +| セッション破棄後の GC Used Memory (MB) | 既定 | 38~39 | 2.5~2.6 | +| エクスポート中の最長のフレーム (ms) | 既定 | 100~117 | 33~50 | + +録画なしの状態の GC Used Memory は約 2.1 MB であった。CPU 使用量は、同じアプリケーションのネイティブライブラリのみを置き換えて simpleperf で計測した。録画中のネイティブライブラリのスレッドの CPU 時間は、約 195 ms/s から約 120 ms/s に減少した。プロセス全体の CPU 使用量は、計測ごとのばらつきがこの差より大きく、別の計測では約 50 ms/s 減少した。 diff --git a/docs/performance-guide.md b/docs/performance-guide.md index ec646d2a..a335a827 100644 --- a/docs/performance-guide.md +++ b/docs/performance-guide.md @@ -2,7 +2,7 @@ This document is intended for developers who use Instant Replay in projects with strict performance requirements. It describes the memory and the CPU / GPU work used by `RealtimeInstantReplaySession`, and how each setting affects them. Measurements on physical devices are given in the second half. -The measured values are examples for specific devices, scenes and settings, and are not guaranteed for other environments. Verify the impact on your own project on physical devices, using the methods described in this document. The content of this document is based on Instant Replay v1.7.0. +The measured values are examples for specific devices, scenes and settings, and are not guaranteed for other environments. Verify the impact on your own project on physical devices, using the methods described in this document. The descriptions apply to the versions after Instant Replay v1.7.0, which include the changes described in [Changes after v1.7.0](#changes-after-v170). Unless otherwise noted, the measured values were obtained with v1.7.0. A Japanese version of this document is available in [performance-guide.ja.md](performance-guide.ja.md). @@ -30,7 +30,7 @@ Calling `StopAndExportAsync()` multiplexes (muxes) the data in the buffer into a | Frames before encoding (CPU) | Encoding width × height × 4 bytes × number of frames | Readback path | Native heap of UniEnc (described below) | | Input surfaces of the encoder | Encoding width × height × 4 bytes × a few | Blit path | Not shown | | Internal memory of the encoder | Unknown | Always | Not shown | -| Buffer of encoded data | About 1.5 to 2 times `MaxMemoryUsageBytesForCompressedFrames` | Memory buffer | `System.Byte[]` in the managed heap | +| Buffer of encoded data | Up to `MaxMemoryUsageBytesForCompressedFrames` | Memory buffer | `System.Byte[]` of 64 KiB in the managed heap | | Write queue of the disk buffer | Up to `DiskBufferOptions.MaxPendingWriteBytes` | Disk buffer | `System.Byte[]` in the managed heap | | Native heap of UniEnc | Allocated in units of 32 MiB | Always | `UniEnc` / `mimalloc` | @@ -48,7 +48,11 @@ On the blit path, the GPU draws directly into the input surfaces of the encoder #### Buffer of encoded data -With the memory buffer, each encoded frame is stored in a `byte[]` rented from `ArrayPool.Shared`. Because `ArrayPool.Shared` returns arrays whose sizes are rounded up to a power of two, the total size of the allocated arrays is larger than the total size of the stored data. `MaxMemoryUsageBytesForCompressedFrames` limits the total size of the stored data and does not include the rounding. In the measurements, the managed heap usage (GC Used Memory) increased by 35 to 50 MB with the default of 20 MiB, and by approximately 103 MB with 60 MiB. +With the memory buffer, each encoded frame is copied into chunks of 64 KiB on the managed heap. The video and audio streams take their chunks from one pool, and the total size of the chunks does not exceed `MaxMemoryUsageBytesForCompressedFrames`. A chunk is allocated when it is first needed and is reused after all frames in it have been discarded, so the managed heap grows while the buffer fills and stops growing when the buffer reaches the limit. Each chunk is smaller than the threshold of the large object heap. When the buffer is full, the oldest frames are discarded in timestamp order across the video and audio streams. The chunks are released when the session is disposed. + +In the measurements on Android, the managed heap usage (GC Used Memory) with a full buffer increased by approximately 23 MB with the default of 20 MiB, and by approximately 70 MB with 60 MiB. These values include a few megabytes of allocations other than the buffer. + +In v1.7.0, each encoded frame was stored in a `byte[]` rented from `ArrayPool.Shared`, which returns arrays whose sizes are rounded up to a power of two. The managed heap usage therefore increased by 35 to 50 MB with 20 MiB, and by approximately 103 MB with 60 MiB. In addition, after an export, the buffer could remain on the managed heap even after the session was disposed. Once the managed heap has grown, it rarely shrinks, so GC Reserved Memory remains increased after recording ends. @@ -74,7 +78,7 @@ The input surfaces of the encoder, the internal buffers of the hardware encoder, 3. Examine the following items. - Unity Objects: the RenderTextures `InstantReplay ScreenshotFrameProvider` and `InstantReplay Preprocessing` - The `UniEnc` area under Native in All Of Memory - - Managed Objects: the total size of `System.Byte[]`. The arrays of encoded data are referenced through `UniEnc.EncodedFrame[]`. + - Managed Objects: the total size of `System.Byte[]`. The chunks of encoded data are arrays of 65,536 bytes referenced through `InstantReplay.EncodedFrameTrackStorage` and `InstantReplay.EncodedFrameChunkPool`. 4. Perform the same steps without recording, and compare the two snapshots. ## CPU / GPU Work @@ -100,15 +104,15 @@ This work runs in parallel with frame rendering and is not added directly to the The following work occurs at specific points rather than on every frame. - Session creation: the constructor of `RealtimeInstantReplaySession` initializes the encoders. In the measurements, it took 30 to 110 ms on iOS and 55 to 220 ms on Android, the longest being with the disk buffer on both platforms. -- Export: with the memory buffer, part of the processing of `StopAndExportAsync()` runs synchronously on the main thread, and in the measurements one frame of 95 to 135 ms occurred. It appears as `UnitySynchronizationContext.ExecuteTasks()` in the Profiler. This stall was not observed with the disk buffer. +- Export: `StopAndExportAsync()` multiplexes the video and audio on the thread pool. In the measurements on Android with the memory buffer, the longest frame during export was 33 to 50 ms. In v1.7.0, the multiplexing and the first initialization of the local time zone, which is used for the default name of the output file, ran on the main thread, and one frame of 95 to 135 ms occurred. The time zone is now initialized on the thread pool when the session is created. On Web, which has no threads, these steps still run on the main thread. ## Effect of Settings on Memory and Frame Time ### Type of buffer (memory or disk) -The memory buffer holds the encoded data in the managed heap. The duration that can be held is determined by `MaxMemoryUsageBytesForCompressedFrames` and the bitrate, and the managed heap usage increases by about 1.5 to 2 times that size. +The memory buffer holds the encoded data in the managed heap. The duration that can be held is determined by `MaxMemoryUsageBytesForCompressedFrames` and the bitrate, and the managed heap usage increases by up to about that size. -The disk buffer writes the encoded data to files, so the managed heap hardly increases. In the measurements, the increase in physical memory with a full buffer was approximately 40 % (iOS) and 60 % (Android) of that with the memory buffer, and no stall of the main thread was observed during export. On the other hand, the continuous writes to storage increased the CPU usage on Android and shorten the lifespan of flash memory. The disk buffer is intended primarily for development and quality-assurance builds. See [disk-buffered-recording.md](disk-buffered-recording.md) for details. +The disk buffer writes the encoded data to files, so the managed heap hardly increases. In the measurements with v1.7.0, the increase in physical memory with a full buffer was approximately 40 % (iOS) and 60 % (Android) of that with the memory buffer. The difference is smaller in later versions, because the memory buffer no longer rounds up the size of each frame. On the other hand, the continuous writes to storage increased the CPU usage on Android and shorten the lifespan of flash memory. The disk buffer is intended primarily for development and quality-assurance builds. See [disk-buffered-recording.md](disk-buffered-recording.md) for details. ### Screen size and encoding size @@ -136,15 +140,17 @@ Disabling audio recording (using `NullAudioSampleProvider`) removes the audio en ## Recommendations -- Determine the size of the memory buffer from the bitrate and the required duration, and allow for the managed heap usage to increase by up to about twice that size. +- Determine the size of the memory buffer from the bitrate and the required duration, and allow for the managed heap usage to increase by about that size. - Set `MaxNumberOfRawFrameBuffers` when the readback path is used. - Use the smallest frame rate and encoding size that meet the purpose. The frame rate in particular directly affects the CPU and GPU load. -- Create sessions and export at points where frame drops are acceptable, such as during loading or on a result screen. +- Create sessions at points where frame drops are acceptable, such as during loading or on a result screen. Exporting no longer stalls the main thread for a long time, but it still adds work to the frames during the export. - On Android, check the bitrate and the duration held on the target devices. - Check the effect on physical memory with the values reported by the OS, not only with Memory Profiler. ## Measurement Results +The values in this section were measured with v1.7.0. The values affected by later changes are described in [Changes after v1.7.0](#changes-after-v170). + ### Conditions | Item | iOS | Android | @@ -224,3 +230,23 @@ The state of rendering at 60 fps while recording with the default settings (30 f - The increase in physical memory at the start is due to the RenderTexture for capture, the encoders and the initialization of the native library; the growth of the managed heap is added as the buffer fills. - With the disk buffer, the increase in physical memory with a full buffer was 25 to 40 MB smaller than with the memory buffer. - On iOS, the duration held changed according to the specified bitrate, whereas on Android it did not change between 1 Mbps and 2.5 Mbps (the default). On Android, the duration held changed with the encoding size because the bitrate actually produced by the encoder changed. + +### Changes after v1.7.0 + +The following changes were made after v1.7.0. Their effect was measured on the Galaxy S22 under the same conditions as above, with the default settings and with a 60 MiB buffer. iOS was not measured again. + +- The buffer of encoded data stores the frames in chunks of 64 KiB within `MaxMemoryUsageBytesForCompressedFrames`, instead of in arrays rounded up to a power of two. +- The memory held by a session is released when the session is disposed, also after an export. +- Exporting no longer runs the multiplexing or the first initialization of the local time zone on the main thread. +- On Android, the CPU usage of the threads of the native library is lower. The waits for MediaCodec are no longer handed to a thread pool one by one, the Vulkan objects created for each input surface of the encoder are reused, and the lookups of Java methods are cached. + +| Item | Condition | v1.7.0 | After the changes | +| --- | --- | --- | --- | +| GC Used Memory, full (MB) | Default | +38 to +39 | +23 to +24 | +| GC Used Memory, full (MB) | 60 MiB buffer | +108 | +70 to +71 | +| Physical memory, full (MB) | Default | +66 to +71 | +47 to +60 | +| Physical memory, full (MB) | 60 MiB buffer | +151 to +153 | +117 to +120 | +| GC Used Memory after the session is disposed (MB) | Default | 38 to 39 | 2.5 to 2.6 | +| Longest frame during export (ms) | Default | 100 to 117 | 33 to 50 | + +The GC Used Memory without recording was approximately 2.1 MB. The CPU usage was measured with simpleperf on the same application, replacing only the native library. The CPU time of the threads of the native library during recording decreased from approximately 195 ms/s to approximately 120 ms/s. The CPU usage of the whole process varied between runs by more than this difference, and decreased by approximately 50 ms/s in a separate measurement.