【Unity】Androidのファイル作成パフォーマンスはディレクトリにある既存ファイル数の影響を受けるのか？

## はじめに
この記事は[サムザップ #2 Advent Calendar 2020](https://qiita.com/advent-calendar/2020/sumzap2)の12/14の記事です。
前日は @leosuke さんの[エンジニアらしく本を整理したい... 「Yahoo Web API を用いたバーコード読み取りによる本整理術」 - Qiita](https://qiita.com/leosuke/items/6b816c70b9fa330932de)でした。

## モチベーション
個人的にUnityで作成した、Androidアプリの動作確認を行っていたところ
ファイルを作成する処理で、実行を繰り返すごとにパフォーマンスが悪化しているものがありました。

アプリを新規インストールし直すとまたもとのパフォーマンスに戻るため、
もしかするとディレクトリに存在するファイルの数に影響を受けているのかと思い、調べてみることにしました。

## パフォーマンス計測コードの作成
### 前準備
#### Performance testing API パッケージの導入
実機上でのパフォーマンス計測には、UnityからPerformance testing APIという便利なパッケージが提供されていますので、それを使うことにします。

Performance testing APIは2020/12/11時点でまだpreviewパッケージしか提供されておりませんので、
「Package Manager」ウィンドウ上部の「Advanced」から「Show preview packages」にチェックを入れます。
![_PackageManager_ShowPreviewPackages.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201332/ea32765a-e236-34bd-e9db-d5253431b0a5.png)
すると画像のようにパッケージが表示されると思いますので、こちらをインストールします。
![_PackageManager_PerformancetestingAPI.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201332/49f9e089-93aa-a19a-a6a4-c29d8570a0f0.png)
インストールが終わったら、パッケージのドキュメントに従っていくつかの設定を行います。

##### link.xmlの追加
IL2CPPのマネージドコードストリッピングが有効になっている場合、計測に必要なコードが取り除かれてしまいますので、
それを避けるためlink.xmlへ下記の項目を追加します。

```xml
<linker>
  <assembly fullname="Unity.PerformanceTesting" preserve="all"/>
</linker>
```

#### テスト用のアセンブリの作成
パッケージの導入が完了しましたら、
Projectウィンドウを右クリック→「Create」→「Testing」→「Tests Assembly Folder」を選択して、テスト用のアセンブリを作成します。
![_CreateTestsAssemblyFolder.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201332/c4ebb395-ed07-3ab8-4f70-1ba61c84908b.png)
次にPerformance testing APIを利用するため、作成された `Tests.asmdef` を選択し、
そのInspectorからAssembly Definition ReferencesへUnity.PerformanceTestingの参照を追加します。
![AssemblyDefinitionReferences.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201332/6ffbb9bc-249b-4497-6c53-a8ac7765331a.png)

### 計測コードの実装
前準備が終わったので、パフォーマンスを計測するコードを実装します。
まず単なるC#のクラスを作成し、

```csharp:FilePerformanceTest.cs
public class FilePerformanceTest
{
}
```

計測を行うメソッドへ`Test`属性、`Performance`属性を指定して、
計測対象の処理を`Measure.Method`へ渡すActionに記述します。

```csharp:FilePerformanceTest.cs
private static string BasePath => Path.Combine(Application.persistentDataPath, "test");
private static readonly byte[] _dummyData = new byte[4096];

[Test]
[Performance]
public void CreateTest()
{
    Measure.Method(() =>
        {
            string path = Path.Combine(BasePath, Guid.NewGuid().ToString("D"));
            using (var fs = File.Create(path))
            {
                fs.Write(_dummyData, 0, _dummyData.Length);
            }
        })
        .Run();
}
```

今回はファイル数の違いによるパフォーマンスの変化も確認したいため複数のパラメータで計測が行えるように設定します。
`CreateTest`メソッドに引数と`TestCase`属性を追加して、`Measure.Method`の実行前にダミーのファイルを作成するコードを追加します。

```csharp:FilePerformanceTest.cs
[TestCase(0)]
[TestCase(64)]
[TestCase(512)]
[TestCase(4096)]
[Test]
[Performance]
public void CreateTest(int numExistsFile)
{
    for (int i = 0; i < numExistsFile; ++i)
    {
        string path = Path.Combine(BasePath, i.ToString("04D"));
        File.WriteAllBytes(path, _dummyData);
    }

    Measure.Method(() =>
            ...
```

最後に計測の実施毎にファイルが残り続けないよう、
`SetUp`属性と`TearDown`属性を使ってディレクトリの作成と後始末の処理を追加します。

```csharp:FilePerformanceTest.cs
[SetUp]
public void Initialize()
{
    Directory.CreateDirectory(BasePath);
}

[TearDown]
public void Cleanup()
{
    Directory.Delete(BasePath, recursive: true);
}
```

このようにPerformance testing APIを使うと、通常のテストコードと同じような要領で計測コードを実装することができます。
実行も同じようにできるため、一通り実装が完了したら、UnityEditor上でテストを実行して動作確認するとよいでしょう。

<details><summary>コード全文</summary><div>

```csharp:FilePerformanceTest.cs
using System;
using System.IO;
using NUnit.Framework;
using Unity.PerformanceTesting;
using UnityEngine;

public class FilePerformanceTest
{
    private static string BasePath => Path.Combine(Application.persistentDataPath, "test");
    private static readonly byte[] _dummyData = new byte[4096];

    [SetUp]
    public void Initialize()
    {
        Directory.CreateDirectory(BasePath);
    }

    [TearDown]
    public void Cleanup()
    {
        Directory.Delete(BasePath, recursive: true);
    }

    [TestCase(0)]
    [TestCase(64)]
    [TestCase(512)]
    [TestCase(4096)]
    [Test]
    [Performance]
    public void CreateTest(int numExistsFile)
    {
        for (int i = 0; i < numExistsFile; ++i)
        {
            string path = Path.Combine(BasePath, i.ToString("04D"));
            File.WriteAllBytes(path, _dummyData);
        }

        Measure.Method(() =>
            {
                string path = Path.Combine(BasePath, Guid.NewGuid().ToString("D"));
                using (var fs = File.Create(path))
                {
                    fs.Write(_dummyData, 0, _dummyData.Length);
                }
            })
            .WarmupCount(1)
            .MeasurementCount(3)
            .Run();
    }
}
```

</div></details>

## パフォーマンス計測の実行
予めAndroid実機をPCへ接続し、USBデバッグを有効にしておきます。
「Test Runner」ウィンドウの右上から「Run all in player(Android)」を選択して、計測を実行します。
![_RunAllInPlayer.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201332/7c17ef64-8b96-ade3-aab5-c4b61fc776c1.png)
UnityEditorのConsoleに `Saving results to: ~` と出力されたら計測は完了です。
メニューの「Window」→「Analysis」→「Performance Test Report」から計測結果を確認します。

## 結果
手元にあったAndroid端末(Snapdragon 855 / Android 10)で計測したところ、結果は次の画像のようになりました。
![_Result.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201332/4952403d-7677-662b-daf0-be8e98c10b65.png)
計測結果の中央値（`Median`）に着目すると、0個と64個の場合はほぼ同じものの、
以降の512個、4096個のケースでは、ファイル作成にかかった時間が増えていることが分かりました、

以上になります。
明日は @s_ebata さんの記事です。

## 環境

- Unity 2019.4.4f1
- Performance testing API version 2.4.1