おれんじりりぃぶろぐ

きっと何者にもなれないエンジニアのブログ

JenkinsでHockeyAppへのアップロードを下流Jobにする

まえおき

JenkinsでHockeyAppへのバイナリアップロードを別ジョブとして切り離す方法です。
Jobを切り離さない場合、ジョブが失敗した時にビルド自体が失敗したのかアップロードが失敗したのか分かりづらい、さらにビルド時間とアップロードにかかる時間が一即多になってしまい純粋なビルド時間が分かりづらいといったことが起こってしまいます。

ビルドJob(上流Job) ---> HockeyAppへのアップロードJob(下流Job)

Jobを切り離すことで以下のメリットが期待できます

  • Job失敗原因が特定しやすい
  • Jobの実行時間が分かりやすくなる

やり方

まずは上流のビルドJobを作成します。 上流Jobでは成果物の保存を行います。

f:id:orange_lily27:20180505211103p:plain

[ビルド後の処理追加] -> [成果物の保存]を追加する

次にHockeyAppアップロード用の下流Jobを作成します。 f:id:orange_lily27:20180505211739p:plain

「ビルド・トリガ」で「他プロジェクトの後にビルド」を設定します。

次に上流Jobでの成果物をコピーします。 「CopyArtifactPlugin」を導入します。

[ビルド手順の追加]で[他プロジェクトから成果物をコピー]を選択して以下のように設定します。 f:id:orange_lily27:20180505212153p:plain

あとはビルド後の処理でHockeyAppへのアップロードをします。 HockeyAppへのアップロードはHockeyAppPluginを導入することで簡単にできます。

f:id:orange_lily27:20180506003851p:plain

以上でビルドJobとHockeyAppへのアップロードのJobを切り離して実行することができます。

UnityでSwitchのJoyConを使う

まえおき

switchのJoyConをUnityで使う方法です。 基本的には以下の記事を参考にしましたが、一部ハマった箇所があるのでメモしておきます。

http://baba-s.hatenablog.com/entry/2017

環境

  • Mac
  • Unity2018.1.b13

やり方

JoyConとMacBluetoothで接続します。

f:id:orange_lily27:20180428205243p:plain

右と左で別々のデバイスとして認識されます。

Edit -> ProjectSettings -> Inputで以下の設定します。 f:id:orange_lily27:20180428205031p:plain

f:id:orange_lily27:20180428205042p:plain

手元の環境では参考記事とAxisの番号が異なっていて、嵌ってしまいました。 こちらのアセットで簡単に確認することが出来ました。

Controller Tester - Asset Store

検出は以下できます。

float h1 = Input.GetAxis("Horizontal1");

Unityのマルチスレッド界隈のはなし

Unityでのマルチスレッド界隈について断片化した理解にとどまっていたので、自分なりのまとめを行ってみました。

まず非同期実装と並列プログラミングは分けて考えよう

  • 非同期実装は順番通りに処理を実行しない、逐次処理とは対比的な処理のことである
    • 非同期処理は通信などの重い処理を別スレッドに逃しメインスレッドを専有させないことを目的としている
  • 一方で、並列実装は比較的短い(1フレーム内で収まる)処理を複数のスレッド使って同時に実行することでマルチコアな物理ハードウェアのパフォーマンスを最大限に引き出そうというアプローチである
  • 非同期実装に適しているのがawait/async、Taskを使った方法である
  • 並列プログラミングの実装に適しているのがC#のParallelクラス、Unityが提供するC#JobSystemになる

C#JobSystem

  • 元々、UnityエンジンではWorkerThreadをコア数-1(例外あり)で持っていた、それをユーザーにも開放した形になる
  • 1フレームで収まる処理を並行的に行うのに適している
    • 通信やI/O待ちが発生する非同期的な処理には向かない
      • その場合には、後述するawait、Taskを使っての実装を行うのがよい
  • イテレーションを回すコレクションはC#のコレクションではなくUnityが独自に用意したNativeArrayというunsafeなコレクションになる
    • NativeArray以外にもNativeListなど順次実装されていくとのこと
    • C++の配列のように必要な領域をあらかじめ確保して必要なくなったら開放をしてあげる必要がある
      • そのため下手な使い方をするとメモリーリークを起こす
        • 一応デバック方法はあって、Editor上だとメモリリークを起こしているという警告が出る
  • C#のコレクションはメモリ上に離散的に存在するのでキャッシュヒット率がとても悪い、JobSystemではNativetiveArrayを使用することでメモリ上に連続的に展開を行いキャッシュヒット率を上げる
  • そのためintやstructなどのプリミティブ型に限られる
  • メインスレッド制約のあるUnityの通常のAPIは使えない
    • MainThreadで実行してくださいというエラーが出る
  • JobSystem上で動く専用のAPIが順次増えていく予定とのこと
    • 今のところ(2018.1.b13)専用のAPIが用意されできることは以下の3つである
      • NavMesh
      • RayCast
      • Transformの書換え
        • 逆に上記に上げる3つ以外のことは制約や煩雑さがあるため使わないほうが良いかもしれない、簡潔な記述ができるC#のParallelクラス(後述)を使ったほうがいいのではないだろうか
  • JobSystem内で戻り値を返す方法がやや煩雑な印象である
  • 詳しい使い方はこちらのブログ記事を参考にすると良いと思います

C#での非同期実装

  • C#5でasync/awaitというキーワードが導入された
  • awaitは文字通り「待つ」の仕組みを提供するものである
var task = SampleAsync(); // 実行
Debug.Log("Immediate Execution"); <- 即時実行される
await task;
Debug.Log("SampleAsync() compleated!"); SampleAsync()の実行完了を待って実行される
  • awaitにTaskクラスを組み合わせて使うことで 非同期実装が容易に実現できる
  • Taskは一連の処理をまとめるためのものである。1つのTaskをメインではない別スレッドで丸っと実行させるということも簡単に実現できる
  • awaitは特定の条件を満たしたクラスに対して使うことができ、Taskがその条件を満たした実装になっているので使用が可能となっている
  • 条件を満たすように実装を行えばawaitをTask以外にも利用でき、例えばUnityコルーチンをawaitさせるということも可能である
  • WebHttpRequestなどは非同期実装が行いやすいようにTaskクラスを返してくれる非同期の通信メソッドがあらかじめ用意されている
var webReq = (HttpWebRequest)WebRequest.Create(URL);
await webReq.GetResponseAsync();

C#Parallelクラス

  • 並行プログラミングを簡単に実装できる仕組みである
  • 立ち位置的にはC#Jobsystemとオーバーラップする
  • UnityではC#JobSystemのほうがパフォーマンスは出るのでしょうが、簡単に記述できるという利点がこちらにはある
Parallel.For(0, N, i =>
{
    Debug.Log(i); // 複数のスレッドで同時並行的に実行する
});

参考

ここで書いたC#JobSystemの情報については、こちらの勉強会に参加して得た情報になります。

Unityコルーチンをawaitするサンプル

概略

C#5からawait句が導入され非同期実装がとてもお手軽にできるようになりました。 awaitはTaskへ使うものと思ってしまいがちですが、一定の条件を満たせばawaitを使った非同期実装を行うことが出来ます。

実装

AwaitHelloWorld.cs

using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using UnityEngine;

public class AwaitHelloWorld : MonoBehaviour
{

    [SerializeField] private GameObject go;
    private ControlCoroutine controlCoroutine;
    
    private async void Start()
    {
        int starttime = DateTime.Now.Hour * 60 *60 * 1000 + DateTime.Now.Minute * 60 * 1000 + 
                    DateTime.Now.Second * 1000 + DateTime.Now.Millisecond;
        
        controlCoroutine = go.GetComponent<ControlCoroutine>();
        var result = new HelloWorldAwaitable(controlCoroutine);
        
        /*for (var i = 0; i < 1000; i++)
        {
            Debug.Log("HelloWorld");
        }*/
        
        int now = DateTime.Now.Hour * 60 *60 * 1000 + DateTime.Now.Minute * 60 * 1000 + 
                  DateTime.Now.Second * 1000 + DateTime.Now.Millisecond;
        Debug.Log($"time:{now-starttime}");

        await result;
        Debug.Log(result);
    }

    public void ClickButton()
    {
        Debug.Log("On Clicked Button!!!");
    }
}

public class HelloWorldAwaitable
{
    private ControlCoroutine cc;
    public HelloWorldAwaitable(ControlCoroutine controlCoroutine)
    {
        this.cc = controlCoroutine;
    }
    
    public HelloWorldAwaiter GetAwaiter()
    {
        return new HelloWorldAwaiter(cc);
    }
}

public struct HelloWorldAwaiter : INotifyCompletion
{
    private ControlCoroutine cc;

    public HelloWorldAwaiter(ControlCoroutine controlCoroutine)
    {
        this.cc = controlCoroutine;
    }
    
    public bool IsCompleted
    {
        get { return false; }
    }

    public string GetResult()
    {
        return "OnCompleated!";
    }

    public void OnCompleted(Action continuation)
    {
        cc.RegisterCoroutine(() =>
        {
            continuation();
        });
        
    }
}

ControlCoroutine.cs

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ControlCoroutine : MonoBehaviour 
{
    public void RegisterCoroutine(Action action)
    {
        StartCoroutine(WorkCoroutine(action));
    }

    private IEnumerator WorkCoroutine(Action action)
    {
        for (var i = 0; i < 1000; i++)
        {
            Debug.Log("HelloWorld");
            yield return null;
        }
        // callbackを実行する
        action();
    }
}

それぞれのソースコードをGameObjectにComponentとして追加します。

まとめ

けっこうコード量が増えパット見は面倒な感じになってしまいました。 しかしWorkCoroutineのところだけ、カスタマイズしてあとは共通のものと考えてしまえばそれほど冗長でもなさそうです。 コルーチンを連続して使うときなどはコールバック地獄からも開放されそうです。

参考

Unity2018での文字列の扱い方

概略

Unityで文字列の連結を行う方法として以下の3つのパフォーマンスを比較してみました。1000回ループし文字列を連結させた時の処理時間とGC回数を計測しました。

ソースコード

using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using UnityEngine;

public class LinkStringSample : MonoBehaviour
{
    private const int N = 10000;

    public void StingTest()
    {
        
        int starttime = DateTime.Now.Hour * 60 *60 * 1000 + DateTime.Now.Minute * 60 * 1000 + 
                        DateTime.Now.Second * 1000 + DateTime.Now.Millisecond;
        string str = string.Empty;

        for (int i = 0; i < N; i++)
        {
            str += "aaa";
        }
        
        int now = DateTime.Now.Hour * 60 * 60 * 1000 + DateTime.Now.Minute * 60 * 1000 + 
                  DateTime.Now.Second * 1000 + DateTime.Now.Millisecond;

        Debug.Log($"StingTest()実行時間: {now - starttime}ms");
        // GC.CollectionCountはプロスを起動してからのGC回数を返す
        // https://msdn.microsoft.com/ja-jp/library/system.gc.collectioncount(v=vs.110).aspx
        Debug.Log($"GC回数: {GC.CollectionCount(0)}回");

    }

    public void StringBuilderTest()
    {
        int starttime = DateTime.Now.Hour * 60 *60 * 1000 + DateTime.Now.Minute * 60 * 1000 + 
                        DateTime.Now.Second * 1000 + DateTime.Now.Millisecond;
        
        StringBuilder strBuilder = new StringBuilder();

        for (int i = 0; i < N; i++)
        {
            strBuilder.Append("aaa");
        }
        
        int now = DateTime.Now.Hour * 60 * 60 * 1000 + DateTime.Now.Minute * 60 * 1000 + 
                  DateTime.Now.Second * 1000 + DateTime.Now.Millisecond;
        
        Debug.Log($"StringBuilderTest()実行時間: {now - starttime}ms");
        Debug.Log($"GC回数: {GC.CollectionCount(0)}回");
    }

    public void BuildInStringTest()
    {
        int starttime = DateTime.Now.Hour * 60 *60 * 1000 + DateTime.Now.Minute * 60 * 1000 + 
                        DateTime.Now.Second * 1000 + DateTime.Now.Millisecond;
        
        string str = String.Empty;

        for (int i = 0; i < N; i++)
        {
            str = $"{str}aaa";
        }
        
        int now = DateTime.Now.Hour * 60 * 60 * 1000 + DateTime.Now.Minute * 60 * 1000 + 
                  DateTime.Now.Second * 1000 + DateTime.Now.Millisecond;
        
        Debug.Log($"BuildInStringTest()実行時間: {now - starttime}ms");
        Debug.Log($"GC回数: {GC.CollectionCount(0)}回");
    }
}

検証環境

  • Unity2018.3b13
    • .NET4.x
    • il2cpp
  • ios
    • iPhone7Plus
  • android
    • s8

結果

StingTest StringBuilderTest BuildInStringTest
Editor 511/168 0.6/56 1248/288
ios 234/1577 0.6/6 542/3885
android 708/2015 1.0/6 1613/5656
  • 処理時間(ミリsec)/GC回数(回)
  • 5回の平均値
  • EditorのGC回数は参考値

まとめ

  • 文字列の連結には基本的にはStringBuilderを使うのがよい
  • 特にインライン形式での埋め込みは高コストなのでDebugログなどの限定的な使用に留めるべきである

参考

Unityでの並列プログラミング(Parallel、PLinq)のパフォーマンス比較

概略

.NET4.0からは、簡易に並列処理を記述することができるParallelクラス、Parallel LINQ(PLinq)の提供がはじまりました。 今回は、前半でParallクラス、PLinqの使い方を見て、後半ではUnityでの実際のパフォーマンス調査を行った結果を示します。

Parallelクラス、PLinqクラスの使い方

For文

Parallel.For(0, N, i =>
{
    lock(syncObj){sum += i ;}
});

Foreach文

foreach (var x in data)
{
    lock(syncObj){sum += x;}
}

同じリソース(今の場合はsum)へのアクセスがある場合は排他制御が必要になります。

PLinq

data.AsParallel().Sum(x => x );

通常のLinq構文にAsParallel()をつけるだけになります。 PLinqの場合には、排他制御はシステム側が行ってくれます。

パフォーマンス検証

実行環境

  • Unity2018.1b13
    • .NET4.x
    • il2cpp
  • iPhone7Plus
  • android s8

実施項目

0 〜 29999の単純な加算を以下の4つの方法で処理時間測定した * 通常のFor文(SequentialSum) * Parallel.For文(ParallelSum) * 通常のLinq(PlaneLinqSum) * PLinq(PlinqSum)

ソースコード

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using UnityEngine;

public class ParallelSample : MonoBehaviour
{

    private const int N = 30000;
    private System.Object syncObj = new System.Object();
    
    
    private void Start()
    {
        
    }

    public void SequentialSum()
    {
        int starttime = DateTime.Now.Hour * 60 *60 * 1000 + DateTime.Now.Minute * 60 * 1000 + 
                    DateTime.Now.Second * 1000 + DateTime.Now.Millisecond;
        
        int sum = 0;
        for (int i = 0; i < N; i++)
        {
            sum += i;
        }
        Debug.Log($"sum: {sum}");
        
        int now = DateTime.Now.Hour * 60 * 60 * 1000 + DateTime.Now.Minute * 60 * 1000 + 
              DateTime.Now.Second * 1000 + DateTime.Now.Millisecond;
        
        Debug.Log($"SequentialSum()実行時間: {now - starttime}ms");
    }

    public void ParallelSum()
    {
        int starttime = DateTime.Now.Hour * 60 * 60 * 1000 + DateTime.Now.Minute * 60 * 1000 + 
                        DateTime.Now.Second * 1000 + DateTime.Now.Millisecond;
        
        int sum = 0;
        Parallel.For(0, N, i =>
        {
            lock(syncObj){sum += i ;}
        });
        Debug.Log($"sum: {sum}");
        
        int now = DateTime.Now.Hour * 60 * 60 * 1000 + DateTime.Now.Minute * 60 * 1000 + 
                  DateTime.Now.Second * 1000 + DateTime.Now.Millisecond;
        
        Debug.Log($"ParallelSum()実行時間: {now - starttime}ms");
    }

    public void PlaneLinqSum()
    {
        int starttime = DateTime.Now.Hour * 60 * 60 * 1000 + DateTime.Now.Minute * 60 * 1000 + 
                        DateTime.Now.Second * 1000 + DateTime.Now.Millisecond;
        
        var data = Enumerable.Range(0, N);
        
        int sum = data.Sum(x => x );
        Debug.Log($"sum: {sum}");
        
        int now = DateTime.Now.Hour * 60 * 60 * 1000 + DateTime.Now.Minute * 60 * 1000 + 
                  DateTime.Now.Second * 1000 + DateTime.Now.Millisecond;
        
        Debug.Log($"PlaneLinqSum()実行時間: {now - starttime}ms");
    }

    public void PlinqSum()
    {
        int starttime = DateTime.Now.Hour * 60 * 60 * 1000 + DateTime.Now.Minute * 60 * 1000 + 
                        DateTime.Now.Second * 1000 + DateTime.Now.Millisecond;
        
        var data = Enumerable.Range(0, N);
        
        int sum = data.AsParallel().Sum(x => x );
        Debug.Log($"sum: {sum}");
        
        int now = DateTime.Now.Hour * 60 * 60 * 1000 + DateTime.Now.Minute * 60 * 1000 + 
                  DateTime.Now.Second * 1000 + DateTime.Now.Millisecond;
        
        Debug.Log($"PlinqSum()実行時間: {now - starttime}ms");
    }
}

結果

SequentialSum ParallelSum PlaneLinqSum PlinqSum
Editor 4.4 22.2 7.8 31.2
ios 4.2 16.0 8.0 x
android 2.0 58.6 5.0 x

5回の平均値 単位はミリSec il2cppでのPlinqは以下のランタイムエラーが出て実行不可能

Unity   : ExecutionEngineException: Attempting to call method 'System.Linq.Parallel.SelectQueryOperator`2<System.Int32, System.Int32>::WrapPartitionedStream<System.Int32>' for which no ahead of time (AOT) code was generated. 

まとめ

  • PLinqはil2cpp環境では使用できない
  • 並列ループ内で同じリソースにアクセスする場合には、排他制御を行わなければならず結局は同期的な処理に近くなってしまう。さらにオーバーヘッドが大きく通常のFor文よりも遅くなる
  • Linqは気になるほどではないが通常のFor文に比べると遅い

参考

UnityにおけるAwaitAsyncを使った非同期実装について④

概略

前回までに最新Unityにおけるマルチスレッドプログラミングについて、さらにその時直面する問題点について紹介してきました。今回は、Task、awaitを使ったマルチスレッドでの排他制御について触れていきたいと思います。 今回も特にUnityに特化した情報はありません。

実装方法

複数のスレッドでから同時にアクセスされる可能性があるリソースにはlock文を使い排他制御をします。

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;

public class AsyncSampleScene : MonoBehaviour
{

    private int sum = 0; // 同時にアクセスされる可能性があるデータ
    private object syncObject = new object();

    private async void Start()
    {
        sum = 0;
        
        List<Task> t = new List<Task>();
        t.Add(TestAsyncMethod());
        t.Add(TestAsyncMethod());
        t.Add(TestAsyncMethod());
        await Task.WhenAll(t);
        
        Debug.Log($"sum: {sum}");
    }

    private async Task TestAsyncMethod()
    {
        await Task.Run(() =>
        {
            lockSum();
        });
    }

    // だめな例
    private void nolockSum()
    {
        for (int i = 1; i <= 1000; i++)
        {
            sum += i * i;
        }
    }
    

    private void lockSum()
    {
        for (int i = 1; i <= 1000; i++)
        {
            // 同時にアクセスされる可能性があるデータ
            lock(syncObject) { sum += i * i; }
        }
    }
}

await句の中ではlock文を使うことはできません。ロックをかけたスレッドとロックを解除するスレッドが異なる可能性があるからです。 nolockSum()の場合は、sumに対して競合が発生して毎回異なる結果になってしまいます。

まとめ

AsyncAwaitが導入されようとも漏れなく排他制御を行うのは難しい(´・_・`)

UnityにおけるAwaitAsyncを使った非同期実装について③

これまでTask、AsyncAwaitを使った非同期処理の実装、メインスレッド以外から特定の(メイン)スレッドにアクセスする方法についてみてきました。 今回は、複数の非同期に実行されるTaskをハンドリングする方法について説明します。今回もUnityに特化した情報はありません。

以下のコードがあったとします。

using System.Threading;
using System.Threading.Tasks;
using UnityEngine;

public class AsyncSampleScene : MonoBehaviour {

    private async void Start()
    {
        await TestAsyncMethod1();
        await TestAsyncMethod2();
        await TestAsyncMethod3();
        Debug.Log("All Compleated!");
    }

    private async Task TestAsyncMethod1()
    {
        await Task.Run(() => {
            Debug.Log($"ID {Thread.CurrentThread.ManagedThreadId}, [Async] 1");
            Thread.Sleep(1000); 
        });
    }
    
    private async Task TestAsyncMethod2()
    {
        await Task.Run(() => {
            Debug.Log($"ID {Thread.CurrentThread.ManagedThreadId}, [Async] 2");
            Thread.Sleep(1000); 
        });
    }
    
    private async Task TestAsyncMethod3()
    {
        await Task.Run(() => {
            Debug.Log($"ID {Thread.CurrentThread.ManagedThreadId}, [Async] 3");
            Thread.Sleep(1000); 
        });
    }
}

TestAsyncMethod1()が完了したらTestAsyncMethod2()が実行され、TestAsyncMethod2()が完了したらTestAsyncMethod3()が実行され、 TestAsyncMethod3()が完了したら最後にDebugログが出力されます。

図にすると以下のようになります。 スクリーンショット 2018-04-16 19.33.41.png (77.7 kB)

しかし、TestAsyncMethod1()TestAsyncMethod2()TestAsyncMethod3()に依存関係が特にない場合にはいちいちawait()するのは無駄です。 この場合、このようになってほしいわけです。 スクリーンショット 2018-04-16 19.46.45.png (76.1 kB)

それぞれのTaskをパラレルに実行し、全て完了したらDebugログを出力しています。

このような実装は以下のコードで実現できます。

using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;

public class AsyncSampleScene : MonoBehaviour {

    private async void Start()
    {
        List<Task> tasks = new List<Task>(); 
        tasks.Add(TestAsyncMethod1());
        tasks.Add(TestAsyncMethod2());
        tasks.Add(TestAsyncMethod3());

        await Task.WhenAll(tasks);
        
        Debug.Log("All Compleated!");
    }

    private async Task TestAsyncMethod1()
    {
        await Task.Run(() => {
            Debug.Log($"ID {Thread.CurrentThread.ManagedThreadId}, [Async] 1");
            Thread.Sleep(1000); 
        });
    }
    
    private async Task TestAsyncMethod2()
    {
        await Task.Run(() => {
            Debug.Log($"ID {Thread.CurrentThread.ManagedThreadId}, [Async] 2");
            Thread.Sleep(1000); 
        });
    }
    
    private async Task TestAsyncMethod3()
    {
        await Task.Run(() => {
            Debug.Log($"ID {Thread.CurrentThread.ManagedThreadId}, [Async] 3");
            Thread.Sleep(1000); 
        });
    }
}

tasksというTaskのコレクションを生成し、各々のtaskをaddしていきます。最後にTask.WhenAll()awaitよって全てのコレクション内のTaskが完了したタイミングを取得することができます。

参考