Archive - Rx

UniTask - Unity + async/awaitの完全でハイパフォーマンスな統合

Unityでasync/await使えてハッピー。が、しかしまだ大々的に使われだしてはいないようです。理由の一つとして、Unityが標準でサポートする気が全くなさそう。少なくとも、Unityがフレームワークとしてasync/awaitには何一つ対応していない。async/awaitという道具立てだけじゃあ何もできないのです、フレームワークとして何らかのサポートがなければ機能しないわけですが、なんと、何もない……。

何もないことの理由はわからないでもないです。パフォーマンス面で不満/不安もありそうですし、マルチスレッドはC# Job System使ってくれというのは理にかなっている(私もそちらが良いと思います、つまりTaskのマルチスレッドな機能は原則使わない)。とはいえ、async/awaitは便利なので、このまま、便利だけど性能は微妙だから控えようみたいな扱い(あ、それ知ってる、LINQだ)になるのは嫌なのよね。まぁLINQは局所的なので使わないのは簡単なのだけど(実際、最近は私もあまりLINQ書いてないぞ!遅いからね!)、async/awaitは割と上位に伝搬していって汚染気味になるので、そもそも一度どこかで使うと使わない、という選択肢が割と取りづらいので、ならいっそむしろ超究極パフォーマンスのasync/awaitを提供すればそれで全部解決なのである。

という長ったらしい前置きにより、つまり超究極パフォーマンスのUnityのasync/await統合を提供するライブラリを作りました。場所は(面倒くさいので)UniRxに同梱です。というわけでなんと久しぶりにUniRxも更新しました……!(主にReactivePropertyが高速になりました、よかったよかった。PRとかIssueのチェックはこれからやります、いや、まず重い腰を上げたというのが何より大事なのですよ!)

GitHub/UniRx と、アセットストアに既に上がっています。

UniTask

何ができるか、について。

// この名前空間はasync有効化と拡張メソッドの有効化に必須です
using UniRx.Async;
 
// UniTask<T>をasyncの戻り値にできます、これはより軽量なTask<T>の置き換えです
// ゼロ(or 少しの)アロケーションと高速な実行速度を実現する、Unityに最適化された代物です
async UniTask<string> DemoAsync()
{
    // Unityの非同期オブジェクトをそのまま待てる
    var asset = await Resources.LoadAsync<TextAsset>("foo");
 
    // .ConfigureAwaitでプログレスのコールバックを仕込んだりも可能
    await SceneManager.LoadSceneAsync("scene2").ConfigureAwait(new Progress<float>(x => Debug.Log(x)));
 
    // 100フレーム待つなどフレームベースの待機(フレームベースで計算しつつTimeSpanも渡せます)
    // (次の更新でフレーム数での待機はDelayFrameに名前変えます)
    await UniTask.Delay(100); // be careful, arg is not millisecond, is frame count
 
    // yield return WaitForEndOfFrameのような、あるいはObserveOnみたいな
    await UniTask.Yield(PlayerLoopTiming.PostLateUpdate);
 
    // もちろんマルチスレッドで動作する普通のTaskも待てる(ちゃんとメインスレッドに戻ってきます)
    await Task.Run(() => 100);
 
    // IEnumeratorなコルーチンも待てる
    await ToaruCoroutineEnumerator();
 
    // こんなようなUnityWebRequestの非同期Get
    async UniTask<string> GetTextAsync(UnityWebRequest req)
    {
        var op = await req.SendWebRequest();
        return op.downloadHandler.text;
    }
 
    var task1 = GetTextAsync(UnityWebRequest.Get("http://google.com"));
    var task2 = GetTextAsync(UnityWebRequest.Get("http://bing.com"));
    var task3 = GetTextAsync(UnityWebRequest.Get("http://yahoo.com"));
 
    // 並列実行して待機、みたいなのも簡単に書ける。そして戻り値も簡単に受け取れる(これ実際使うと嬉しい)
    var (google, bing, yahoo) = await UniTask.WhenAll(task1, task2, task3);
 
    // タイムアウトも簡単にハンドリング
    await GetTextAsync(UnityWebRequest.Get("http://unity.com")).Timeout(TimeSpan.FromMilliseconds(300));
 
    // 戻り値はUniTask<string>の場合はstringを、他にUniTask(戻り値なし)、UniTaskVoid(Fire and Forget)もあります
    return (asset as TextAsset)?.text ?? throw new InvalidOperationException("Asset not found");
}

提供している機能は多岐にわたるのですが、

  • Unityの非同期オブジェクトをawaitできるように拡張(最速で動くように細心の注意を払って対応させています)
  • コルーチンやUniRxで出来るフレームベースのawaitサポート(Delay, Yield)
  • 戻り値をTupleで受け取れるWhenAll, どれが返ってきたかをindexで受け取れるWhenAny, 便利なTimeout
  • 標準のTaskよりも高速でアロケーションの少ないUniTask[T], UniTask, UniTaskVoid

となっています。で、何が出来るのかと言うと、ようはコルーチンの完全な置き換えが可能です。async/awaitがあります、っていう道具立てだけだと、何もかもが足りないんですね。ちゃんと機能するようにフレームワーク側でサポートさせてあげるのは必須なのですが、前述の理由(?)どおり、Unityはサポートする気が1ミリもなさそうなので、代わりに必要だと思える全てを提供しました。

Taskを投げ捨てよ

目の付け所がいかれているので、Taskを投げ捨てることにしました。Taskってなんなの?というと、asyncにする場合戻り値がTaskで強要される、という型。そして究極パフォーマンスの実現として、このTaskがそもそも邪魔。なんでかっていうと、歴史的経緯によりそもそもTaskは図体がデカいのです。異様に高機能なのは(TaskSchedulerがどうだのLongRunningがどうだの)、ただたんなる名残(或いは負の遺産)でしかない。アドホックな対応を繰り返すことにより(言語/.NET Frameworkのバージョンアップの度に)コードパス的に小さくはなっていったのですが(async/awaitするためだけには不要な機能がてんこ盛りなのだ!)、もういっそ全部いらねーよ、という気にはなる。

そこでC# 7.0です。C# 7.0からasyncの戻り値を任意の型に変更することが可能になりました。詳しくは言語仕様のAsync Task Types in C#に書いてありますが、Builderを実装することにより、なんとかなります。

というわけで、UniRx.Asyncでは軽量のTaskであるUniTaskと、そのためのBuilderを完全自前実装して、Unityに最適化されたasync/awaitを実現しました。

代わりにC# 7.0が必須のため、現状ではIncremental Compilerを導入する必要があります(現状のUnity 2017/2018はC# 6.0のため)

Incremental Compilerではなくても、恐らくUnity 2018の近いバージョンではC#のバージョン上がりそうな気配なので、先取りするのは悪くないでしょう。

PlayerLoop

UniRx.AsyncはUniRxに依存していません。そのため、GitHubのreleasesページではUniRxを含まないパッケージも提供しています。併せて使ったほうがお得なのは事実ですが、なしでも十分に機能します。

さて、UniRxではMainThreadDispatcherというシングルトンのMonoBehaviourにMicroCoroutine(というイテレータを中央管理するもの)を駆動してもらっていましたが、今回スタンドアロンで動作させるため、別の手段を取りました。それがPlayerLoopです(詳しくはテラシュールブログの解説が分かりやすい)。

これをベースにUpdateループをフックして、await側に戻す処理を仕掛けています。

Multithreading

掲げたのはNo Task, No SynchronizationContext。何故かというと、そもそもUnityの非同期って、C++のエンジン側で駆動されていて、C#のスクリプティングレイヤーに戻ってくる際には既にメインスレッドで動くんですよね。例えば AsyncOperation.completed += action とか。コルーチンのyield retunもそうですね、PlayerLoop側で処理されている。ようするに、本来SynchronizationContextすら不要なのです、全てメインスレッドで動作するので。

通常のC#はスレッドベースで、Windows FormsやWPF, ASP.NETなど諸々の事情を吸収するために存在していたわけですが、Unityだけで考えるなら完全に不要です。他のものにはないフレーム毎に駆動することと、本体がC#ではなくC++側にあるということが大きな大きな違いです。async/awaitやTask自体は汎用的にする必要があるため、それらの吸収層が必要(SynchronizationContext)なわけですが、当然ながらオーバーヘッドなので、取り除けるなら取り除いたほうが良いでしょう。そのために、UniTaskの独自実装も含めて、全てのコードパスを慎重に検討し、不要なものを消し去りました。

UniTaskはどちらかというとJavaScript的(シングルスレッドのための非同期の入れ物)に近いです。Taskは、そうした非同期の入れ物に加えてマルチスレッドのためなどなど、とにかく色々なものが詰まりすぎていて、あまりよろしくはない。非同期とマルチスレッドは違います。明確に分けたほうが良いでしょうし、UnityではC# JobSystemを使ったほうが良いので、カジュアルな用途以外(まぁラクですからね)ではマルチスレッドとしてのTaskの出番は少なくなるでしょう。

嬉しいこととして、スレッドを使わないのでWebGLでもasync/awaitが完全に動作します。

Rx vs Coroutine vs async/await

もう結論が出ていて、async/await一本でOK、です。まずRxには複数の側面があって、代表的にはイベントと非同期。そのうち非同期はasync/awaitのほうがハンドリングが用意です。そしてコルーチンによるフレームベースの処理に関してはUniTask.DelayやYieldが解決しました。ので、コルーチン→出番減る, async/await → 非同期, Rx → イベント処理 というように分離されていくと思われます。

C# Standard vs Unity

正直なところ私は別にUnityがC#スタンダードに添わなくてもいいと思ってるんですよね。繰り返しましが、Unityの本体はC++の実行エンジンのほうで、C#はスクリプティングレイヤーなので。C#側が主張するよりも、C++に寄り添うことを第一に考えたほうが、よい結果がもたらされると思っています。よりC#に、というならPure C#ゲームエンジンでないとならないですが、商業的にはほぼ全滅であることを考えると、Unityぐらいの按配が実際ちょうどいいのだろうな、と。理想もいいんですが、ビジネスとしての成功がないと全く意味がないので。

と、いうわけで、C# JobSystemは大歓迎だしBurst Compilerは最高 of 最高なわけですが(そしてECSなんてそもそもオブジェクト指向ですらなくなる)、さて、Task。UniTaskの有用性や存在意義については、よくわかってもらえたと思います!そのうえで、それを分かったうえでもノンスタンダードな選択を取るべきなのか論は、それ自体は発生して然りです。

まぁ、まずUnityだとそもそもC# 7.0が来たら片っ端からValueTask(という、TとTaskのユニオンがC# 7.0から追加された)に置き換え祭りは発生するでしょう。実際async祭りで組むと、「同期で動くTask」がどうしても多く発生してしまい、無駄なアロケーション感半端ないので、ValueTask主体のほうがよい。

更にその上で.NET Core 2.1ではValueTaskにIValueTaskSourceという仕掛けが用意されて、これは何かと言うと、やっぱりasync/awaitの駆動においてTaskを無視するための仕組みです(現状はSystem.IO.Pipelinesというこれまたつい先週ぐらいに出た機能のみ対応)。そう、別にUnityだけじゃなくて通常の.NETでもTaskはオーバーヘッドと認識されているのだ……。

つまりなんというか、そう、そもそもC#本流ですら割と迷走しているのだ……。存在すると思っているStandardなんてもはやないのだ……。てわけで、別にUniTask、いいんじゃない?とか思ってしまいますがどうでしょう。どうでしょうね、それはさすがにポジショントークすぎにしても。

ようはポリシーとして、asyncで宣言した際に、TaskにするかValueTaskにするかUniTaskにするかを迫られます。逆に言えばそれだけです。あれ、意外と人畜無害。そう、意外と人畜無害なのです。よし、なら、とりまやってみるか。いいんじゃないかな?別に最悪、一括置換で戻したり進めたり割と容易なので。あと、ちなみに、UniTaskがUnityでデファクトスタンダードになれば、尚更迷う必要性はなくなるので、むしろ是非みんなでデファクトスタンダードまで持っていきましょう:)

まとめ

非同期革命の幕開け!そもそもこれぐらいやらないと世論は動かない、というのもあるので、フルセットでどーんと凄い(っぽい)(実際凄い)のを掲示することにはめちゃくちゃ意味があります。UniTaskが流行っても流行らなくても、この掲示にはめちゃくちゃ意味があるでしょう。UniRx.Asyncが何を実現したかを理解することは非常に重要です、教科書に出ますよ!

それと、UniRx全然更新していなくてごめんなさい、があります。ごめんなさい。今回、ReactivePropertyのパフォーマンス向上を(ようやく)入れたり、今後はちゃんと面倒みていくのでまたよろしくおねがいします。

Open Collective/UniRxというところで寄付/スポンサー募集もはじめたので、よければ個人/企業で入れてくれると嬉しいですね……!今ならUniRxのGitHubページのファーストビューにロゴが出るので、特に企業などはアピールポイントです……!

Rx.NETの近況、或いはUniRxの近況(?)

あまり長々とした記事ばかりではなく、サラッと流したのも書きましょう!というかリハビリです、リハビリ。ハイパー無職タイムが発動しつつあることもあり、ダラけようと思えば無限にダラけてしまえるのです。なんだかやっぱ一瞬、緊張の糸が途切れた感はどうしてもあります、どうしても。GitHubのグラフもかなり白くなってしまっていますからねー、いやはや。その辺はそろそろモードを切り替えないと、というタイミングです。色々と考えてはいるんですけどね。やることが多すぎると逆にフリーズするというあるああるあるです。

dotnet/reactive

GitHubのリポジトリが Reactive-Extensions/Rx.NET から dotnet/reactive に引越ししました(リダイレクトされます)。これ、ただ単に引っ越したというだけではなく、今回からついにRx.NETがコミュニティ主導による開発に引き渡された、という意味合いを持ちます。これ凄く大きなことでめちゃくちゃ大事なのですよ。

何故かというと、経緯は issues/466 Become an official dotnet repo?のディスカッションにありますが、Rx.NET自体が、ずっとRx v2以降長らくRx.NETの開発をほぼ一人でリードしてきた Bart J.F. De Smet氏(MicrosoftのPrincipal Software Development Engineer, 直近ではBingやコルタナの裏側の分散サービス基盤をRxで実装、とか)によって開発の主導権は握られていました。これは別に悪いことではないですよ!(ほぼ)オリジナルのAuthorですし、実装力も高いですし、学生時代(MS MVP時代)のブログなんかはもう10年前とかの記事ばかりですが今でも珠玉のものばかりですし、私のリスペクトするエンジニアの一人です。んで、まぁそれはいいんですが、かなり昔(4年も前)にRx v3の計画を発表していて(Cloud-scale Event Processing using Rx)、その中心が「Reactor」という上記コルタナの裏側で作られたシステムを元にしてRxを見つめ直すという、多分に野心的なシステムで、なんと結局パブリックにするといって未だにパブリックにされていない!

その辺ののらりくらりっぷりは、以前に私の書いた各言語に広まったRx(Reactive Extensions、ReactiveX)の現状・これからという記事で悲観的に説明しましたが、やはり想像通りに、そんな野心的なビジョンがあるせいで、Rx.NETの開発はほとんど止まっちゃいました。その後の進捗としてはRx.NETをGitHub上でコミュニティを加えて開発していく、という話があり、実際にコミュニティの手によりv3もリリースされましたが、UWPや.NET Standardへ対応するといった、リリースマネージャーを引き受けただけであり、Rx.NETを発展させていく、という点では依然としてコミュニティの手には渡っていない状況が続いていました。

とはいえ、RxJavaRxSwiftは次々と進化を遂げていて、Rx.NETは元祖ではあるが、ただ単に元祖であるというだけで、すっかり先端ではなくなった現状が間違いなくあり、また、もはや決して看過されるべきではない時期が来ている。壮大なビジョンやオリジナル著者へのリスペクトも大事ですが、何よりも重要なのは止まらない成長なのだ。と、突きつけられたのが件のissueで、まぁ概ね開発の主導も移されました。

こういうの、パイオニアってだけじゃ追いつかれ追い抜かれちゃうから、現代では元祖であること、そのこと自体には価値はないのですよね。良くも悪くも目の前にあるモノの出来が全てで。元祖だとか、10年先を行っていただとか、それは立派なことで尊敬されるべきことですが、ようするにただの勲章なのです。勲章を誇りだしたら老害ですから、一番よくない価値観でもある。(C#は昔からー、とかWPF/XAMLはー、とかMVVMはずっと前にー、とかRxはー、とか言うのはダサいところは結構あります、C#が格好良かった(まだ過去形ではない)のは、実践的な形に落とし込んだ先端を走り続けていたことでしょう)。

さて、開発の主導が渡されたからといって、じゃあ誰が開発していけるの?という話ですが、まずはRxJavaに追いつこうぜ、というところで、現在RxJavaのプロジェクトリードであるakarnokd氏が(RxJavaのコミット数2位、2015年以降だと1位)入って、オペレーターの追加やRxJavaによって成熟された最適化が始まっています。外部の風強い……。

議論の場もreactivex/slackのrxnetチャンネルに移り、活発にやり取りされています。そんなわけで、そう遠くなくRx.NETの時は動き出しそうです。めでたしめでたし。

UniRxの近況?

まぁ、ぶっちけるとRx.NETにおける Bart先生と同じような状況ですね!やりたいこともあるし将来のビジョンもあり、やる気もあるはずなのだけど、手が止まっている、みたいな。ダメじゃん!ダメですね……。

ではなくて、ええと、まぁ私もNew World, Inc.という会社をこないだ立ち上げまして、とりあえず何かお金に変えていかなければならないのです!別に有料にしたりはしませんが、寄付ぐらいは入れたいかなー、とか、思っているのですけれど、だったらプロジェクトはアクティブじゃないとダメですよねアタリマエですよね、ということで、いよいよついにちゃんとガチでやってきます。ほんと。これはほんと。人は霞じゃ生きていけないのでチャリンチャリンも大事。私はよくチャリンチャリンビジネスと言ってます。

それとは別にRx.NET本体をUnity(.NET 4.6/IL2CPP)対応させるというのもあります。これは、Rx.NET側からの要請もあって、手伝っていきたいことですね。とはいえ、UniRxはUnityにフィットさせるために手を加えているものも多いので、コンパイル通してランタイムエラー潰しただけだと、ぶっちけイマイチなところがかなり出てくるので、単純移植はそこまで価値あるかというと、そうでもないかな。というわけで、どちらにせよRxJavaに追いつけプロジェクトのほうが優先なわけで、そちらが落ち着いたら、UniRx側で得た知見などを少しずつフィードバックしていこうかなとは思ってますが、だいぶ遠い未来にはなりそうなので、そういう面でもUniRxは安心して使ってほしいですね。更新もします、しますから……!

実際まあ、Unityもasync/await入ってRxとどう使い分けるとかそもそも使い分けないとか、色々あるわけで、で、私はその答えを持っているので(async/await自体もC#7までの機能をフルに使って色々ごにょったりしたことあるので、私、めちゃくちゃ詳しいんです!)、ライブラリ実装という技術面でも、こうした文章での解説でも、出せていけたらなーという感じですねー。

とにかくダラダラ生きないためにも今年は色々やっていきますよです。

UnityのMonoアップグレードによるasync/awaitを更にUniRxで対応させる

ついに!.NET 4.6アップグレードが始まりました。Unityの。Unity 5.5でC#コンパイラをアップグレードしていましたが、今回はついにフレームワークも、です。また、Unity 5.5のものはC#のバージョンは4に制限されていましたが、今回はC# 6が使えます。現在はForumでアーリアクセスバージョンが公開されていて、ついでにそこでリンクされているVisual Studio Tools for Unityも入れると、かなりふつーに.NET 4.6, C# 6対応で書ける感じです。

さて、.NET 4.6, C# 6といったら非同期。async/await。もちろん、書けました。が、しかし。

async Task ThraedingError()
{
    Debug.Log($"Start ThreadId:{Thread.CurrentThread.ManagedThreadId}");
 
    await Task.Delay(TimeSpan.FromMilliseconds(300));
 
    Debug.Log($"From another thread, can't touch transform position. ThreadId:{Thread.CurrentThread.ManagedThreadId}");
    Debug.Log(this.transform.position); // exception
}

これはtransformのとこで例外でます。なんでかっていうと、awaitによって別スレッドに行ってしまっているから。へー。この辺、async/awaitではSynchronizationContextという仕組みで制御するのですが、現在のUnity標準では特に何もされてないようです。

それだけだとアレなので、そこで出てくるのがUniRx。今日、アセットストアで最新バージョンのVer 5.5.0を公開したのですが、この5.5.0ではasync/await対応を試験的に入れています。それによって自動的にSynchronizationContextも生成/登録してくれます。

async Task UseUniRxInBackground()
{
    Debug.Log($"Start ThreadId:{ Thread.CurrentThread.ManagedThreadId}");
    await Task.Delay(TimeSpan.FromMilliseconds(300));
    Debug.Log($"From same thread, because UniRx installs UniRxSynchronizationContext.ThreadId:{ Thread.CurrentThread.ManagedThreadId}");
    Debug.Log(this.transform.position); // show transform
}

というように、UniRxをインポート後では、前の例外を吐いたコードと全く同じでも、ちゃんとメインスレッドに戻してくれるようになります。

Coroutine is awaitable

UniRxを入れることで追加される機能はそれだけではなく、更に普通のコルーチンもawait可能な仕組みを裏側で仕込んでいます。これにより

async Task CoroutineBridge()
{
    Debug.Log("start www await");
    var www = await new WWW("https://unity3d.com");
    Debug.Log(www.text);
    await CustomCoroutine();
    Debug.Log("await after 3 seconds");
}
 
IEnumerator CustomCoroutine()
{
    Debug.Log("start wait 3 seconds");
    yield return new WaitForSeconds(3);
    Debug.Log("end 3 seconds");
}

といったように、WWWとかIEnumeratorを直接awaitすることが可能になります。これはUniRx側で用意した仕組みによるものなので、普通では(現状は)できません。

勿論(?)IObservableもawait可能になっています。

async Task AwaitObservable()
{
    Debug.Log("start await observable");
    await Observable.NextFrame();  // like yield return null
    await Observable.TimerFrame(5); // await 5 frame
    try
    {
        // ObservableWWW promote exception when await(difference in await WWW)
        var result = await ObservableWWW.Get("https://404.com");
        Debug.Log(result);
    }
    catch (WWWErrorException ex)
    {
        Debug.LogError(ex.ToString());
    }
    Debug.Log("end await observable");
}

ObservableWWWを使うと例外はちゃんとtry-catchのほうに投げてくれるようになって、より自然に、簡単に扱えるようになります。

まとめ

思ったよりも、普通に使えて、普通に統合できるな、という印象があります < async/await。コルーチンで扱うよりも自然で、より強力なので、非同期を扱うのに適したシチュエーションではこっちのほうが良いのは間違いないはずです。Rxとの住み分けですが、基本的に非同期が対象ならばasync/awaitのほうが良いです。が、今回見ていただいたようにIObservableはawaitableなので、コードがRxになっているならば、現在のコードから自然にasync/awaitベースにソフトに移行することが可能でしょう。

Unityが今後、標準でSynchronizationContextを入れてくるのか、コルーチン対応クラスをawait対応にさせてくるのか、などはちょっと分かりません。分かりませんが、UniRxならば、その対応がずっと後のことになるとしても、今すぐ問題なく使うことが出来ますし、その可能性を今すぐ感じ取ることが可能なので、ぜひとも試してみてください!

余談

UniRxがAssetStore STAFFPICKに選ばれましたー。

うーん、嬉しい。

UniRx 5.4.0 - Unity 5.4対応とまだまだ最適化

UniRx 5.4.0をリリースしました!ちょうどUnity 5.4もリリースされたので、5.4向けの修正(Warning取り除いただけですが)を出せて良かった。というわけで5.4正式対応です。リリースは前回が5月だったので3ヶ月ぶりです。5.2 -> 5.3も3ヶ月だったので、今のとこ3ヶ月スパンになってますが偶然です。

何が変わったのかというと

Add: Observable.FrameInterval
Add: Observable.FrameTimeInterval
Add: Observable.BatchFrame
Add: Observable.Debug(under UniRx.Diagnostics namespace)
Add: ObservableParticleTrigger and OnParticleCollisionAsObservable, OnParticleTriggerAsObservabl(after Unity 5.4) extension methods
Add: UniRx.AsyncReactiveCommand
Add: ReactiveCommand.BindToOnClick, `IObservable<bool>.BindToButtonOnClick`
Add: UniRx.Toolkit.ObjectPool, AsyncObjectPool
Add: UniRx.AsyncMessageBroker, asynchronous variation of MessageBroker
Add: ObserveEveryValueChanged(IEqualityComparer) overload
Add: `Observable.FromCoroutine(Func<CancellationToken, IEnumerator>)` overload
Add: ObservableYieldInstruction.IsDone property
Add: IPresenter.ForceInitialize(object argument)
Improvement: Where().Select(), Select().Where() peformance was optimized that combine funcs at internal
Improvement: MicroCoroutine performance was optimized that prevent refresh spike
Improvement: Observable.Return performance was optimized that reduced memory cost
Improvement: Observable.Return(bool) was optimzied perofmrance that allocate zero memory
Improvement: Observable.ReturnUnit was optimzied perofmrance that allocate zero memory
Improvement: Observable.Empty was optimzied perofmrance that allocate zero memory
Improvement: Observable.Never was optimzied perofmrance that allocate zero memory
Improvement: Observable.DelayFrame performance was optimized
Improvement: UnityEqualityComparer.GetDefault peformance was optimized
Improvement: AddTo(gameObject) dispose when ObservableTrigger is not activated
Improvement: AddTo(gameObject/component) performance was optimized by use inner CompositeDisposable of ObservableDestroyTrigger
Improvement: `FromCoroutine<T>(Func<IObserver<T>, IEnumerator>)` stops coroutine when subscription was disposed
Improvement: ReactiveCollection, ReactiveDictionary implements dispose pattern
Fix: ToYieldInstruction throws exception on MoveNext when reThrowOnError and has error 
Fix: ObserveEveryValueChanged publish value immediately(this is degraded from UniRx 5.3)
Fix: Prevent warning on Unity 5.4 at ObservableMonoBehaviour/TypedMonoBehaviour.OnLevelWasLoaded
Fix: Remove indexer.set of IReadOnlyReactiveDictionary
Breaking Changes: Does not guaranty MicroCoroutine action on same frame
Breaking Changes: UniRx.Diagnostics.LogEntry was changed from class to struct for performance improvement

相変わらずへっぽこな英語はおいといてもらえるとして、基本的にはパフォーマンス改善、です。

前回紹介したMicroCoroutineを改良して、配列をお掃除しながら走査する(かつ配列走査速度は極力最高速を維持する)ようになったので、より安定感もましたかな、と。その他メモリ確保しないで済みそうなものは徹底的に確保しないようになど、しつっこく性能改善に努めました。あと新規実装オペレータに関しては性能に対する執拗度がかなり上がっていて、今回でいうとBatchFrameはギチギチに最適化した実装です。既存オペレータも実装甘いものも残ってはいるので、見直せるものは見なおしてみたいですねえ。

また、9/13日にPhoton勉強会【Photon Server Deep Dive - PhotonWireの実装から見つめるPhoton Serverの基礎と応用、ほか】で登壇するので、PhotonWireではUniRxもクライアント側でかなり使っているので、その辺もちょっと話したいなと思っていますので、Photonに興味ある方もない方も是非是非。Photon固有の話も勿論しますが、普通にUnityとリアルタイム通信エンジンについての考えや、UniRx固有の話なども含めていきますので。

Debug

Debugという直球な名前のオペレータが追加されました。標準では有効化されていなくて、UniRx.Diagnosticsというマイナーな名前空間をusingするようで使えるようになります。実際どんな効果が得られるのかというと

using UniRx.Diagnostics;
 
---
 
// [DebugDump, Normal]OnSubscribe
// [DebugDump, Normal]OnNext(1)
// [DebugDump, Normal]OnNext(10)
// [DebugDump, Normal]OnCompleted()
{
    var subject = new Subject<int>();
 
    subject.Debug("DebugDump, Normal").Subscribe();
 
    subject.OnNext(1);
    subject.OnNext(10);
    subject.OnCompleted();
}
 
// [DebugDump, Cancel]OnSubscribe
// [DebugDump, Cancel]OnNext(1)
// [DebugDump, Cancel]OnCancel
{
    var subject = new Subject<int>();
 
    var d = subject.Debug("DebugDump, Cancel").Subscribe();
 
    subject.OnNext(1);
    d.Dispose();
}
 
// [DebugDump, Error]OnSubscribe
// [DebugDump, Error]OnNext(1)
// [DebugDump, Error]OnError(System.Exception)
{
    var subject = new Subject<int>();
 
    subject.Debug("DebugDump, Error").Subscribe();
 
    subject.OnNext(1);
    subject.OnError(new Exception());
}

シーケンス内で検出可能なアクション(OnNext, OnError, OnCompleted, OnSubscribe, OnCancel)が全てコンソールに出力されます。よくあるのが、何か値が流れてこなくなったんだけど→どこかで誰かがDispose済み(OnCompleted)とか、OnCompletedが実は呼ばれてたとかが見えるようになります。

超絶ベンリな可視化!ってほどではないんですが、こんなものがあるだけでも、Rxで困ったときのデバッグの足しにはなるかなー、と。

BatchFrame

BatchFrameは特定タイミング後(例えばEndOfFrameまでコマンドまとめるとか)にまとめて発火するという、Buffer(Frame)のバリエーションみたいなものです。都度処理ではなくてまとめてから発火というのは、パフォーマンス的に有利になるケースが多いので、そのための仕組みです。Bufferでも代用できなくもなかったのですが、Bufferとは、タイマーの回るタイミングがBufferが空の時にスタートして、出力したら止まるというのが大きな違いですね。その挙動に合わせて最適化されています。

// BatchFrame特定タイミング後にまとめられて発火
// デフォルトは0フレーム, EndOfFrameのタイミング
var s1 = new Subject<Unit>();
var s2 = new Subject<Unit>();
 
Observable.Merge(s1, s2)
    .BatchFrame()
    .Subscribe(_ => Debug.Log(Time.frameCount));
 
Debug.Log("Before BatchFrame:" + Time.frameCount);
 
s1.OnNext(Unit.Default);
s2.OnNext(Unit.Default);

実装的には、まとめる&発火のTimerはコルーチンで待つようにしているのですが、今回はそのIEnumeratorを手実装して、適宜Resetかけて再利用することで、パイプライン構築後は一切の追加メモリ消費がない状態にしてます。

Optimize Combination

オペレータの組み合わせには、幾つかメジャーなものがあります。特に代表的なのはWhere().Select()でしょう。これはリスト内包表記などでも固有記法として存在するように、フィルタして射影。よくありすぎるパターンです。また、Where().Where()などのフィルタの連打やSelect().Select()などの射影の連打、そして射影してフィルタSelect().Where()などもよくみかけます(特にWhere(x => x != null)みたいなのは頻出すぎる!)。これらは、内部的に一つのオペレータとして最適化した合成が可能です。

// Select().Select()
onNext(selector1(selector2(x)));
 
// Where().Where()
if(predicate1(x) && predicate2(x))
{
    onNext(x);
}
 
// Where().Select()
if(predicate(x))
{
    onNext(selector(x));
}
 
// Select().Where()
var v = selector(x);
if(predicate(v))
{
    onNext(v);
}

と、いうわけで、今回からそれらの結合を検出した場合に、内部的には自動的にデリゲートをまとめた一つのオペレータに変換して返すようになっています。

MessageBroker, AsyncMessageBroker

MessageBrokerはRxベースのインメモリPubSubです。AndroidでOttoからRxJavaへの移行ガイドのような記事があるように、PubSubをRxベースで作るのは珍しいことではなく、それのUniRx版となってます。

UniRxのMessageBrokerは「型」でグルーピングされて分配される仕組みにしています。

// こんな型があるとして
public class TestArgs
{
    public int Value { get; set; }
}
 
---
 
// Subscribe message on global-scope.
MessageBroker.Default.Receive<TestArgs>().Subscribe(x => UnityEngine.Debug.Log(x));
 
// Publish message
MessageBroker.Default.Publish(new TestArgs { Value = 1000 });
 
// AsyncMessageBroker is variation of MessageBroker, can await Publish call.
 
AsyncMessageBroker.Default.Subscribe<TestArgs>(x =>
{
    // show after 3 seconds.
    return Observable.Timer(TimeSpan.FromSeconds(3))
        .ForEachAsync(_ =>
        {
            UnityEngine.Debug.Log(x);
        });
});
 
AsyncMessageBroker.Default.PublishAsync(new TestArgs { Value = 3000 })
    .Subscribe(_ =>
    {
        UnityEngine.Debug.Log("called all subscriber completed");
    });

AsyncMessageBrokerはMessageBrokerの非同期のバリエーションで、Publish時に全てのSubscriberに届いて完了したことを待つことができます。例えばアニメーション発行をPublishで投げて、Subscribe側ではそれの完了を単一のObservableで返す、Publish側はObservableになっているので、全ての完了を待ってSubscribe可能。みたいな。文字だけだとちょっと分かりにくいですが、使ってみれば結構簡単です。

UniRx.Toolkit.ObjectPool/AsyncObjectPool

UniRx.Toolkit名前空間は、本体とはあんま関係ないけれど、Rx的にベンリな小物置き場という感じのイメージでたまに増やすかもしれません。こういうのはあまり本体に置くべき「ではない」とも思っているのですが、Rxの内部を考慮した最適化を施したコードを書くのはそこそこ難易度が高いので、実用的なサンプル、のような意味合いも込めて、名前空間を隔離したうえで用意していってもいいのかな、と思いました。

というわけで、最初の追加はObjectPoolです。ObjectPoolはどこまで機能を持たせ、どこまで汎用的で、どこまで特化させるべきかという範囲がかなり広くて、実装難易度が高いわけではないですが、好みのものに仕上げるのは難しいところです。なのでまぁプロジェクト毎に作りゃあいいじゃん、と思いつつもそれはそれで面倒だしねー、の微妙なラインなのでちょっと考えつくも入れてみました。

// こんなクラスがあるとして
public class Foobar : MonoBehaviour
{
    public IObservable<Unit> ActionAsync()
    {
        // heavy, heavy, action...
        return Observable.Timer(TimeSpan.FromSeconds(3)).AsUnitObservable();
    }
}
 
// それ専用のPoolを<T>で作る
public class FoobarPool : ObjectPool<Foobar>
{
    readonly Foobar prefab;
    readonly Transform hierarchyParent;
 
    public FoobarPool(Foobar prefab, Transform hierarchyParent)
    {
        this.prefab = prefab;
        this.hierarchyParent = hierarchyParent;
    }
 
    // 基本的にはこれだけオーバーロード。
    // 初回のインスタンス化の際の処理を書く(特定のtransformに下げたりとかその他色々あるでしょふ)
    protected override Foobar CreateInstance()
    {
        var foobar = GameObject.Instantiate<Foobar>(prefab);
        foobar.transform.SetParent(hierarchyParent);
 
        return foobar;
    }
 
    // 他カスタマイズする際はOnBeforeRent, OnBeforeReturn, OnClearをオーバーロードすればおk
    // デフォルトでは OnBeforeRent = SetActive(true), OnBeforeReturn = SetActive(false) が実行されます
 
    // protected override void OnBeforeRent(Foobar instance)
    // protected override void OnBeforeReturn(Foobar instance)
    // protected override void OnClear(Foobar instance)
}
 
public class Presenter : MonoBehaviour
{
    FoobarPool pool = null;
 
    public Foobar prefab;
    public Button rentButton;
 
    void Start()
    {
        pool = new FoobarPool(prefab, this.transform);
 
        rentButton.OnClickAsObservable().Subscribe(_ =>
        {
            // プールから借りて
            var foobar = pool.Rent();
            foobar.ActionAsync().Subscribe(__ =>
            {
                // 終わったらマニュアルで返す
                pool.Return(foobar);
            });
        });
    }
}

基本的に手動で返しますし、貸し借りの型には何の手も入ってません!Rent後のトラッキングは一切されてなくて、手でReturnしろ、と。まあ、9割のシチュエーションでそんなんでいいと思うんですよね。賢くやろうとすると基底クラスがばら撒かれることになって、あまり良い兆候とは言えません。パフォーマンス的にも複雑性が増す分、どんどん下がっていきますし。

どこがRxなのかというと、PreloadAsyncというメソッドが用意されていて、事前にプールを広げておくことができます。フリーズを避けるために毎フレームx個ずつ、みたいな指定が可能になっているので、その完了がRxで待機可能ってとこがRxなとこです。

それと同期版の他に非同期版も用意されていて、それは CreateInstance/Rent が非同期になってます。

MessageBrokerと同じくAsyncとそうでないのが分かれているのは、Asyncに統一すべき「ではない」から。統一自体は可能で、というのも同期はObservable.Returnでラップすることで非同期と同じインターフェイスで扱えるから。そのこと自体はいいんですが、パフォーマンス上のペナルティと、そもそもの扱いづらさ(さすがにTのほうがIObservable[T]より遙かに扱いやすい!)を抱えます。

sync over asyncは、UniRx的にはバッドプラクティスになるかなあ。なので、同期版と非同期版とは、あえて分けて用意する。使い分ける。使う場合は極力同期に寄せる。ほうがいいんじゃないかな、というのが最近の見解です。

なお、Rent, Returnというメソッド名はdotnet/corefxのSystem.Buffersから取っています。

AsyncReactiveCommand

というわけでこちらもsync/asyncの別分けパターンで非同期版のReactiveCommandです。ReactiveCommandは何がベンリなのか分からないって話なのですが、実はこっちのAsyncReactiveCommandはかなりベンリです!

public class Presenter : MonoBehaviour
{
    public UnityEngine.UI.Button button;
 
    void Start()
    {
        var command = new AsyncReactiveCommand();
 
        command.Subscribe(_ =>
        {
            // heavy, heavy, heavy method....
            return Observable.Timer(TimeSpan.FromSeconds(3)).AsUnitObservable();
        });
 
        // after clicked, button shows disable for 3 seconds
        command.BindTo(button);
 
        // Note:shortcut extension, bind aync onclick directly
        button.BindToOnClick(_ =>
        {
            return Observable.Timer(TimeSpan.FromSeconds(3)).AsUnitObservable();
        });
    }
}

interactableの状態をコード実行中、というかつまりIO<T>が返されるまでfalseにします。連打防止でThrottleFirstがよく使われますが、それをより正確にコントロールしたり、また、引数にIReactiveProperty[bool]を渡せて、それを複数のAsyncReactiveCommandで共有することで、特定のボタンを実行中は他のボタンも実行できない、のような実行可否のグルーピングが可能になります(例えばグローバルでUI用に一個持っておけば、ゲーム中でUIは単一の実行しか許可されない、的なことが可能になる)

PresenterBase再考

PresenterBase、Obsoleteはつけてないのですけれど、GitHub上のReadMeで非推奨の明言を入れました。賢い基底クラスは悪。なのです。POCO。それはUnityにおいても何事においても例外ではない。その原則からするとPresenterBaseは賢すぎたのでナシ of the Year。動きはする、動きはするんですが……。

Model-View-Presenterパターン自体の否定ではなくて(それ自体は機能するとは思っています、ただし関心がModelにばかり向きがちですが、Viewは何か、Presenterは何か、についてもきちんと向き合わないとPresenterが奇形化するかなー、というのは実感としてある。ViewであるものをPresenterとして表現してアレゲになる、とか)、PresenterBaseというフレームワークのミスかな、とは。です。

とりあえずいったん初期化順序が気になるシーンは手でInitializeメソッド立てて、それをAwake, Startの代わりにして、呼ばせる。いじょ。みたいな素朴な奴で十二分かなー、とオモッテマス。結局。メリットよりもデメリットのほうが大きすぎたかな。反省。

この辺りに関してはアイディアはあるので、形にするまで、むー、ちょっと味噌汁で顔洗って出直してきます。

まとめ

あんまり大きな機能追加はなく細々とした変化なんですが、着々と良くはなっているかな、と!

Rxに関してもバッドプラクティスを色々考えられるというか反省できる(おうふ……)ようになっては来たので、どっかでまとめておきたいですね。油断するとすぐリアクティブスパゲティ化するのはいくないところではある。強力なツールではあるんですが、やりすぎて自爆するというのは、どんなツールを使っても避けられないことではあるけれど、Rxがその傾向はかなり強くはある。

まぁ、sync over asyncはいくないです。ほんと(思うところいっぱいある)。

というわけかで繰り返しますが、9/13日にPhoton勉強会【Photon Server Deep Dive - PhotonWireの実装から見つめるPhoton Serverの基礎と応用、ほか】で登壇するので、よければそちらも是非是非です。

Unityにおけるコルーチンの省メモリと高速化について、或いはUniRx 5.3.0でのその反映

UniRx 5.3.0をリリースしました!今回のアップデートは、内部的な最適化、です。最適化は、もうそろそろあんまやるところ残ってないよね、なんて思ってたんですが、じっくり考えるとそんなことなく割とあったので埋めました。それが表題のコルーチンの省メモリと高速化です。使い方次第ではありますが、場合によっては今回のアップデートでものすごく恩恵に授かる人もいればそこそこの人もいるかもです。ともあれ基本的に内部的に変更してるだけなので、入れるだけでそれなりに高速化したりする可能性がそれなりにあります。

前回が2月だったので3ヶ月ぶりですね。あまりオペレータ追加がないので、次はオペレータ追加に集中したい気もする。なんか優先的に欲しいのあればリクエストもどうぞ(Observable.Windowとかいい加減そろそろ入れろよって話なんですが)

MicroCoroutine

今回の大きい変化はMicroCoroutine(と、自称してる)の導入です。特に大量にEveryUpdateやEveryValueChangedを呼んでるシチュエーションにおいて10倍、というのは場合によりで大雑把なのですが、相当速くなります。

void Start()
{
    // Start 10000 Coroutines
    for (int i = 0; i < 10000; i++)
    {
        // Standard Unity Coroutine
        // StartCoroutine(Counter());
 
        // Use UniRx 5.3 - MicroCoroutine
        MainThreadDispatcher
          .StartUpdateMicroCoroutine(Counter());
    }
}
 
IEnumerator Counter()
{
    while (true)
    {
        count++;
        yield return null;
    }
}

こんな10000個、単純なコルーチンを起動するコードがあったとして

image

大きく違いがでます。ちょっと恣意的すぎではあるんですが、UniRxはコルーチンを簡単にかけるが故に、これに近いシチュエーションってのが意図せず起こりがちではありました。また、Resources.LoadAsyncなど非同期系APIからの取得に関しても、一時的に多くのコルーチンを起動するシチュエーションはあり得るのではないでしょうか。

性能改善した理由は、基本的にはUnityの公式ブログUPDATE()を10000回呼ぶで紹介されていることの話で、10000個のUpdateは遅くて、配列に詰めて直接ループで呼ぼうぜ、と。どうせUpdate内のメソッドは呼ばれてC#の領域で実行されるんだから、マネージド(C#)-アンマネージド(C++)の繋ぎのレイヤーは純粋にオーバーヘッドになってくるよ、と。なるほどそうだねそりゃそうだねぇ。それはStartCoroutineにも言えて、というかコルーチンのほうがもっと性能劣化度が大きいんですよね。

この記事は非常に素晴らしくて、大量にモノ出して速度遅くなってるのがスクリプト起因なら、マネージャー立ててまとめて、あとUpdateに限らずマネージド-アンマネージドの繋ぎをやってる部分が遅いだろうからそこを適切に取り除ける限り除けば、全然まだまだそれなりに捌ける余裕は残ってるぜ。ということで、むしろ希望に満ちていていい感じです。実際、ハイパフォーマンスを謳うDOTweeenとかのライブラリもそんな感じですね、動かすものそれぞれにUpdateするコンポーネントを挿したりはしない、中央管理で動かすのだ、と。

さて、UniRxでは幾つかのメソッドはコルーチン依存でループを回しています。Observable.EveryUpdateとかEveryValueChangedとか。少しに使う分にはいいんですが、気楽に使えるが故に、大量に使うと、10000個とまではいかなくてもやっぱり、それぞれがコルーチンを起動することによるマネージド-アンマネージドオーバーヘッドがそのまま乗っかってきてしまいます。というわけで、やはりコルーチン自前管理の道を進むしかない……。幸い、自前管理で問題になる機能面での低下に関しては、UniRx自体がコルーチンを凌ぐだけの機能を提供しているので、気にしないでよし。というわけで純粋にいかにコルーチン(IEnumerator)を高速に回転させ、高速にメンテナンスするかにだけ集中すればよし。

回転させるのはforループ回すだけの話なんですが、マネージャー作ろうぜ、となった時に、Listに詰めるのはいいんですが、面倒くさいのは削除。削除は要注意で、単純にListのRemoveやって済ませたりするのは結構アレです(Removeは相当高コストな操作です)。かといってDictionaryやSet、LinkedListでやるなんていうのは論外で(列挙の性能が死ぬので本末転倒)、基本的に配列で頑張るべきなんですが、さてはて。結局、その辺のめんどーを見るのがめんどーだからUpdateやStartCoroutineでぶん回すのだ。割と本気で。

ではどうしたか、というと、UniRxのMicroCoroutineのアプローチはRemoveしない。です。しない。空いた部分はnullで埋めて純粋にスキップするだけにする。多少の空きなら、いちいち削るよりもスキップさせたほうが速い。しかし、それだけだとブヨブヨと膨らみ続けてしまうので、xフレーム毎に空きスペースに詰めなおして小さくします。縮める際も前の方に整列させるんじゃなくて、空きスペースに対して後ろから埋めるようにするので、順番はグチャグチャになります。その代わり余計な配列へのセットが発生しないので速い。そして膨らんだ配列は放置して膨らんだままにします、終端のインデックスだけ記録して管理するのみ(ところでアセットストアにアップデート申請出してから気づいたのですが、この配列の使い方なら定期的なお掃除じゃなくて、動かしながら埋めるようなコードにするのも可能っぽい感、なので次回アップデートでそうします)

というわけで、UniRxのMicroCoroutineは中央集権的なので多少膨らむことが許される(でしょう!)ことを利用して、とにかく高速にコルーチンを捌く、ということだけに集中してます。ので速い。下手に自前管理するよりも速いかもしれませんし、Updateで監視するよりもObserveEveryValueChangedのほうがむしろ速い、Rxで書いたほうが速い、みたいな逆転現象も全然発生しうるような話になります。

ObserveEveryValueChanged
EveryUpdate 
EveryFixedUpdate
EveryEndOfFrame
NextFrame
TimerFrame 
IntervalFrame
DelayFrame 
SampleFrame
ThrottleFrame
ThrottleFirstFrame
TimeoutFrame

この辺りのメソッドを使った場合、内部の実装がMicroCoroutineに差し替わったので自動的に恩恵に預かれます。コルーチン -> Observable変換に関しては FromMicroCoroutine が追加されました。基本的にはFromCoroutineと一緒なのですが、MicroCoroutineではyield returnするのはnullだけにしてください、それ以外には対応してません(UnityEditor上ではWarning出して警告します)。MicroCoroutineの制約はそれなんですが、なんだかんだで、8割ぐらいはyield return nullだけで成立するんちゃうんちゃうん、みたいな。賢くやろうとすればもう少しは出来なくもないんですが、シンプルで高速なコルーチンの回転を損ねちゃうのでナシ。IEnuemrator.Currentの呼び出しや、その型チェックすら省きたい。残り2割ぐらいなら普通にStartCoroutineすればいいじゃん、ということで。実際、UniRxの↑のメソッドはそれでかなり置き換えることが出来る、ということを発見できたので、全面的に導入する気になったのです。

また、最悪待ちたい場合は、isDoneのループを回すようにToYieldInstruction経由でIObservableを待てるので、大抵のことはなんでもできます。

IEnumerator MicroCoroutineWithToYieldInstruction()
{
    var www = ObservableWWW.Get("http://aaa").ToYieldInstruction();
    while (!(www.HasResult || www.IsCanceled || www.HasError)) // 3つもプロパティ並べるのダルいので次回アップデートでIsDoneを追加します予定
    {
        yield return null;
    }
 
    if (www.HasResult)
    {
        UnityEngine.Debug.Log(www.Result);
    }
}

もっとプリミティブに直接利用したい場合は、StartCoroutineの代わりにMainThreadDispatcherに3つ生やしてあります。

MainThreadDispatcher.StartUpdateMicroCoroutine
MainThreadDispatcher.StartFixedUpdateMicroCoroutine
MainThreadDispatcher.StartEndOfFrameMicroCoroutine

それぞれがコルーチンを消費するタイミングで、まぁ普通はStartUpdateMicroCoroutineを使えばよいでしょふ。もし大量のStartCoroutineがプログラム中にあるのなら、これに差し替えるだけで本当にすっごく速くなるでしょう。ほんと。

SubscribeWithState

ここから先はUniRxのアップデートの話だけ。そして本当にMicro Micro Microな最適化であんま意味はないんですが、まず、SubcribeWithStateを追加しました。これによって何が変わるか、というと、例えば……

// Before
public static IDisposable SubscribeToText(this IObservable<string> source, Text text)
{
    return source.Subscribe(x => text.text = x);
}
 
// After
public static IDisposable SubscribeToText(this IObservable<string> source, Text text)
{
    return source.SubscribeWithState(text, (x, t) => t.text = x);
}

という感じの使い方ができます。どういう違いが出るのかというと、以前にUnityでのボクシングの殺し方、或いはラムダ式における見えないnewの見極め方という記事の中で説明したのですが、ラムダ式はその中身によってコンパイル時に生成されるコードがかなり変わってきます。で、最速なのはそのメソッド内だけで完結していて外部の変数等には一切触っていない状態。onNextはActionなので、副作用かける際にどうしても外部変数をキャプチャしてしまうことが多いんですよね。そこでSubscribeWithStateを使うと、必要な変数を閉じ込めることができるので最速ゴミなしの形で記述できます。

ただまぁ、これやると、じゃあSelectやWhereなんかもState取れたほうがいいんですか?(理屈上はそうです)、とか、ああクロージャ殺さなきゃ死ね死ね死ね、とか思ったりしそうなのですけれど、Subscribeの回数ってパイプライン内の実行頻度に比べれば圧倒的に少なくなるはずなんですよね。だから全体のバランスで見たら無視できるといっても過言ではないはず、特にクロージャでちょっとゴミが出る程度の話は。

なのであんま神経質にやることはないんですが、↑のSubscribeToTextのようなそんな手間もかからないし、UIとかシーンの初期化時にいっぱい登録される可能性があるようなものでライブラリ的な部分でカバーできる質のものならば、少しだけ気を使ってあげると気は安らぐかもしれません。

ReactiveCommand

ReactiveCommandは.NET版のReactiveProeprtyにあった、最後のパーツなんですが、どうなんでしょうね、本来はViewModelのレイヤーのためなんですが、UnityだとPresenterにUI要素がセリ出してきてるのでイマイチベンリかどうか分からなくて入れてなかったんですが。一応、こんな風に使えます。

public class Player
{
   public ReactiveProperty<int> Hp;
   public ReactiveCommand Resurrect;
 
   public Player()
   {
        Hp = new ReactiveProperty<int>(1000);
 
        // If dead, can not execute.
        Resurrect = Hp.Select(x => x <= 0).ToReactiveCommand();
        // Execute when clicked
        Resurrect.Subscribe(_ =>
        {
             Hp.Value = 1000;
        }); 
    }
}
 
public class Presenter
{
    public Button resurrectButton;
 
    Player player;
 
    void Start()
    {
      player = new Player();
 
      // If Hp <= 0, can't press button.
      player.Resurrect.BindTo(resurrectButton);
    }
}

buttonのinteractableとonClickが抽象化されたもの、って感じですね。

その他

リリースノートから。

Add : ReactiveCommand
Add : MainThreadDispatcher.StartUpdateMicroCoroutine, StartFixedUpdateMicroCoroutine, StartEndOfFrameMicroCoroutine
Add : Scheduler.MainThreadFixedUpdate, MainThreadEndOfFrame
Add : ToYieldInstruction(cancellationToken)
Add : Observer.Create(onNext/onNext, onError/onNext, onCompleted) overload
Add : IReadOnlyReactiveProperty.SkipLatestValueOnSubscribe
Add : Observable.WhenAll overload (IObservable<Unit>(params IObservable<Unit>[] sources), this becomes breaking changes)
Add : Observable.FromMicroCoroutine
Add : Observable.AsSingleUnitObservable
Add : Observable.SubscribeWithState
Add : Observable.CreateWithState
Add : Disposable.CreateWithState
Improvement : Use MicroCoroutine on `ObserveEveryValueChanged`, `EveryUpdate`, `EveryFixedUpdate`, `EveryEndOfFrame`, `NextFrame`, `TimerFrame`, `IntervalFrame`, `DelayFrame`, `SampleFrame`, `ThrottleFrame`, `ThrottleFirstFrame`, `TimeoutFrame`
Improvement : Performance improvement for Observable.Range, Repeat when scheduler is Scheduler.Immediate
Improvement : Use Time.unscaledDeltaTime in IgnoreTimeScaleMainThreadScheduler
Fix : ReadOnlyReactiveProperty(source, initialValue) does not publish initial value on subscribe
Fix : IReadOnlyCollection has set indexer
Fix : Ambigious property of IReactiveCollection.Count, Indexer
Fix : Throw invalid error when ObservableWWW.LoadFromCacheOrDownload failed.
Breaking Changes : Added IReadOnlyReactiveProperty.HasValue
Breaking Changes : AsyncConvertsion scheduler to Scheduler.MainThread on WebGL build(WebGL doesn't support ThreadPool)
Other : Update UniRxAnalyzer 1.4.0.1 https://www.nuget.org/packages/UniRxAnalyzer

ToYieldInstructionはUniRx 5.0 - 完全書き直しによるパフォーマンス向上とヒューマンリーダブルなスタックトレース生成で説明しているのですが、Unity 5.3以降のCustomYieldInstuctionを応用したもので、IObservableをコルーチンで処理できるようにするやつで、結構お薦め機能です。MicroCoroutineで回すための補助にもなりますし。

SchedulerにMainThreadFixedUpdateとMainThreadEndOfFrameを足しました。ObserveOnやTimerなどで、その辺の細かい制動をしたい方にどうぞ。

(ReadOnly)ReactivePropertyへのSkipLatestValueOnSubscribe拡張メソッドの追加。これは、(UniRxの)ReactivePropertyはSubscribe時に必ず値をプッシュするようになってるんですが、そういった初期値を無視したいって局面は少なからずあるんですよね。Rx.NET用のReactivePropertyでは、コンストラクタでReactiveProeprtyModeとして、None | RaiseLatestValueOnSubscribe | DistinctUntilChanged を指定できるようなデザインを選んでいるのですが(というのも、Viewにデータバインディングするため構築時の初期値はnullであることが確定している、というシチュエーションが割とあるため)、UniRxのReactivePropertyではSubscribe側が選ぶというデザインにしています。この辺はフレームワークの性質の違いに合わせてるのですが、ともあれ、初期値を無視したい場合は rxProp.SkipLatestValueOnSubscribe().Subscribe() としてもらえれば。

Observable.WhenAllを、IObservable[Unit][]が相手の場合はIObservable[Unit]を返すようにしました。これは、別にUnit[]が返されても何の意味もないからというのと、それによって余計な配列確保をしないという最適化も入れています。この方が絶対に良いんですが、しかし戻り値の型が変わってしまったので破壊的変更にはなっています。最初から気づいておけば良かったですね、すびばせん。

AsSingleUnitObservableは LastOrDefault().AsUnitObservable() みたいな変換をかけるやつで、Async的な保証をかけるのにベンリというあれそれ。

あとは、んー、使ってる人は、うちの社内以外にないのでは疑惑も感じてますが、UniRxAnalyzerを更新してます。コンストラクタにIObservableを突っ込んでいた場合に誤検出していたのを修正しています。

これ、Visual Studio 2015を使って開発している人は絶対に入れたほうがいいですよ!Subscribe忘れて発火しないのに気づかなかったー、みたいなポカミスが圧倒的に防げますので。

まとめ

性能面でより気にせずにカジュアルに色々使えるようになった、というのはいいことかなー。性能面で問題出た際に「そういう使いかた想定してないから」といった却下の仕方って、あんましたくないですからね。聞いてていいものでは全くない。デザインとしてカジュアルに使えるようになっているなら、性能もちゃんと担保していかないし、そういうのが頻発するならライブラリの設計が悪い。と、思ってるので、今回のでよりちゃんと自然に使えるようになったかな、と。ObserveEveryValueChangedは個人的には最高にクールな機能だと思ってるので、気兼ねなく使って欲しいし、やっと本当に気兼ねなく使えるようになりました。

ObservableUpdateTrigger(UpdateAsObservable), Observable.EveryUpdate, Observable.EveryGameObjectUpdate とUpdateのハンドリングも3択、性能特性も三者三様。混乱との対話!別に特に何をレコメンドすることもなく、まあ素直に書くならUpdateTriggerが素直でよく。自身のUpdateループで周りますしね。EveryUpdateはMicroCoroutineなので性能特性的には良さげ、どうせAddTo(this)するならループのライフサイクルもUpdateTriggerと別に変わりはしないし(UpdateTriggerだとDisableでUpdateが回らなくなるので、まぁその辺で挙動に違いは出る)。EveryGameObjectUpdateはMainThreadDispatcherのSubjectに積まれるもので、UpdateTriggerが使える状況なら非推奨かな、あんまりSubjectに頻繁にAdd, Removeするのは性能特性的に悪手なので。UpdateTriggerもSubjectが駆動するのですが、性質的にグローバルじゃないのでAdd, Removeは局所化されるからそこまででは、に通常はなるでしょう、的な。

そんなこんなで、少なくともRxが性能面のネックでー、と言われるのは悔しい話なので、大きいものから小さいものまで、最適化ネタは常に考えてます。利用事例としても、結構ヒットしてる某社の某ゲーム(とは)や最近でた前作に続いてヒットの予感のする某ゲーム(とは)など、かなり使いこなしてる事例もあって(個人的にはとても感動した!)、ちゃんと実用的といってもいいレベルになってると思われます。弊社の開発中タイトルである黒騎士と白の魔王でもガッツリ使っているので、ご興味ある方は中途採用は絶賛募集中です:) 当たり前ですがドッグフーディングは凄く大事で、さすがにデカいバグは出てこないにしても軽微なものはちょいちょい上がってくるので、日々、堅牢さは担保されているな、とかかんとか。あと、使いすぎてるほどに使いすぎてるので、常に性能面でネックになってはいけない、性能面でネックになってはいけない、とマントラを唱えるプレッシャーになってるのもいいことです、多分きっと。

今回のアップデートでツメが甘かった案件としてはAsyncOperation.AsObservableやObservableWWWが内部的にまだFromCoroutine利用なので、FromMicroCoroutineに可能なら差し替えようかな、と。効果のほどとしては、やっぱり場合によりけりですが、初期化とかで大量に回る時は大きく変わるかも。しれない。ともあれ次回アップデートにご期待を。ただyield return wwwやasyncOperationした場合とyield return nullでisDoneチェックする場合とで、戻ってくるタイミングが異なるので、そこのルールを統一させないとかなあ。

Unityでのボクシングの殺し方、或いはラムダ式における見えないnewの見極め方

Happy boxing! UniRxの前回リリース(UniRx 5.0.0)でパフォーマンス向上を果たしたと書きましたが、まだやり残したことがありました。それがボックス化(boxing)の殺害です。ボックス化は単純に言うと、せっかくの値型が箱に入っちゃってGCゴミが発生してGCがーーー、というもの。避けれるなら避けるべし。あ、ちなみに今回の内容は特に別にUnityに限らないふつーのC#の話です。

それと、というわけかでUniRx 5.1.0リリースしました、アセットストアから落とせます。基本的な内容は以下に解説するボックス化を徹底的に殺害したことによるパフォーマンス向上です。

ボックス化とジェネリクス

GCって、別に見えてるnewだけで発生するわけでもありません。見えてるものを警戒するのは大事ですが、見えないものを見てないのは片手落ち感が否めない。そんな見えないものの代表例がボックス化です。実際どういう時に発生するのかというと

var x = (object)10;

みんな大好きint(ValueType)がobject(ReferenceType)に!これがボックス化の害です。なるほど、避けたほうが良さそうだ。とはいえこんなのやらないって?ですよね。ではこれは?

void Hoge(object o)
{
}
 
Hoge(10);

まぁまぁやらないかもしれませんが、まぁまぁやるといえなくもないです。というかやる時はあります。ではこれは?

bool IsSame<T>(T t1, T t2)
{
    return t1.Equals(t2);
}

一見何も悪くないのですが、実は悪いです。どこが?

public virtual bool Equals(Object obj);

ここが。ようするにEqualsはobjectになった後に比較されてしまうのです。というわけでボックス化が発生します。ジェネリクスは基本的にボックス化を避けれるのですが、一部のObjectに生えてるメソッド、というかようするにEqualsですが、を触る場合、気をつけないとうっかりしがちです。他に t1.GetType() と書いてもボックス化が発生します。その場合、 typeof(T) と書くことで避けられます。

EqualityComparer<T>を使う

ボックス化を避けた比較を行うインターフェイスにIEquatable<T>があります。

public interface IEquatable<T>
{
    bool Equals(T other);
}

これを使い、つまり

bool IsSame<T>(T t1, T t2) where T : IEquatable<T>
{
    return t1.Equals(t2);
}

にすればボックス化は避けれる問題なし。ではあるんですが、これでは不便すぎます(さすがにintとかはIEquatable<T>を実装してはいますが、普通の参照型はほとんど実装していないでしょう)。同じなのかどうかとりあえずチェックしたい、Equalsを普通に呼びたいケースは沢山あります。そこでEqualsを外部から渡せるIEqualityComparer<T>インターフェイスと、デフォルト実装を取得するEqualityComparer<T>.Defaultが使えます。

bool IsSame<T>(T t1, T t2)
{
    return EqualityComparer<T>.Default.Equals(t1, t2);
}

EqualityComparer<T>.Defaultは、TがIEquatable<T>を実装していればTがIEquatable<T>のEquals(T other)を、実装してなければEquals(object other)を呼んで比較します。これによりめでたく値型のボックス化が避けれました!UniRxでもDistinct、DistinctUntilChanged、ObserveEveryValueChanged、そしてReactivePropertyのSetValueでボックス化が発生していたのですが、UniRx 5.1.0からは発生しなくなっています。なんで今まで発生していたのかというと、EqualityComparer<T>がiOS/AOTで怪しくてあえて避けてたんですが、5.0.0からAOTサポートはきってIL2CPPのみにしたので無事性能向上を果たせました。

UnityとIEquatable<T>

Unityにおいては、それだけでメデタシではなく、もう少し話に続きがあります。Unityにおける代表的な値型であるVector2やRectなどは、全て、IEquatable<T>を実装して、いません。へー。==はオーバーライドされているので、素のままで扱って比較している限りは問題ないのですが、ジェネリックの要素として、また、DictionaryのKeyとして使った場合などでもボックス化が発生しています。

これが地味に困る話で、UniRxにおいてもObserveEveryValueChangedなどでVector2などが流れてくるたびにボックス化が発生したらちょっとよろしくない。

そこで、その対策として今回のUniRx 5.1.0では UnityEqualityComparer.Vector2/Vector3/Vector4/Color/Rect/Bounds/Quaternion というものを用意しました。これら代表的なUnityの値型に関しては、専用のEquals/GetHashCodeを実装してあります。また、 UnityEqualityComparer.GetDefault[T] により、それらが型から取り出せます。普通にUniRxを使っている範囲では(Distinct、DistinctUntilChanged、ObserveEveryValueChangedなど) IEqualityComparer の取得は UnityEqualityComparer.GetDefault[T] を通すようにしているため、極力ボックス化が発生しないようになっています。

ラムダ式と見えないnew

ボックス化、見えないGCゴミの話を書いたので、ついでにもう一つ見えないゴミを発生させるラムダ式について。ラムダ式は実際のところコンパイラ生成の塊みたいなもの、かつ、中身によってかなり生成物が変わってきます。ざっと6通りのパターンを用意してみました。

static int DoubleStatic(int x)
{
    return x * 2;
}
 
int DoubleInstance(int x)
{
    return x * 2;
}
 
void Run()
{
    var two = int.Parse("2");
 
    Enumerable.Range(1, 1).Select(DoubleStatic);           // 1
    Enumerable.Range(1, 2).Select(DoubleInstance);         // 2
    Enumerable.Range(1, 3).Select(x => x * 2);             // 3
    Enumerable.Range(1, 4).Select(x => x * two);           // 4
    Enumerable.Range(1, 5).Select(x => DoubleStatic(x));   // 5
    Enumerable.Range(1, 6).Select(x => DoubleInstance(x)); // 6
}

どんな感じになるか想像できました?では、答え合わせ。ちょっと簡略化しているので正確にはもう少しこんがらがった機会生成になっていますが、概ねこんな感じになってます。

static Func<int, int> cacheA;
static Func<int, int> cacheB;
 
internal static int LambdaA(int x)
{
	return x * 2;
}
 
class Closure
{
    internal int two;
 
    internal int LambdaB(int x)
    {
        return x * two;
    }
}
 
internal static int LambdaC(int x)
{
	return DoubleStatic(x);
}
 
internal static int LambdaD(int x)
{
	return DoubleInstance(x);
}
 
void Run()
{
    var two = int.Parse("2");
 
    // 1 - Select(DoubleStatic)
    Enumerable.Range(1, 1).Select(new Func<int, int>(DoubleStatic));
 
    // 2 - Select(DoubleInstance)
    Enumerable.Range(1, 2).Select(new Func<int, int>(DoubleInstance));
 
    // 3 - Select(x => x * 2)
    if(cacheA != null)
    {
        cacheA = new Func<int, int>(LambdaA);
    }
    Enumerable.Range(1, 3).Select(cacheA);
 
    // 4 - Select(x => x * two)
    var closure = new Closure();
    closure.two = two;
    Enumerable.Range(1, 4).Select(new Func<int, int>(closure.LambdaB));
 
    // 5 - Select(x => DoubleStatic(x))
    if(cacheB != null)
    {
        cacheB = new Func<int, int>(LambdaC);
    }
    Enumerable.Range(1, 5).Select(cacheB);
 
    // 6 - Select(x => DoubleInstance(x))
    Enumerable.Range(1, 6).Select(new Func<int, int>(LambdaD));
}

それぞれ似ているような違うような、ですよね?一つ一つ見ていきましょう。

パターン1、パターン2はメソッドを直接突っ込む場合。この場合、実際のところはデリゲートを生成して包んでます。そしてこのデリゲートはGCゴミになります。なります。全く見えないんですが地味にそうなってます。と、いうわけで、それを回避するには静的メソッドなら静的フィールドに静的コンストラクタででも事前に作ってキャッシュしておく、インスタンスメソッドの場合は、もし使うシーンがループの内側などの場合は外側で作っておくことで、生成は最小限に抑えられるでしょう。

パターン3は、恐らく最もよく使うラムダ式の形式で、使う値が全てラムダ式の中だけで完結している場合。この場合、自動的に静的にキャッシュを生成してそれを未来永劫使いまわしてくれるので、非常に効率的です。一番良く使う形式が効率的というのは嬉しい、遠慮無くどんどん使おう。

パターン4も、まぁよく使う形式、でしょう。ローカル変数をラムダ式内で使っている(キャプチャ)した場合。この場合、普通にクラスがnewされて、そこにラムダ式内部で使われる値を詰め込み、その自動生成のクラスのインスタンスメソッドを呼ぶ形に変換されます。というわけで、パターン4は見た目は人畜無害ですが、中身はそれなりのゴミ発生器です!いや、まぁたかがクラス一個。であり、されどクラス一個。画面上に大量に配置されるGameObjectのUpdateなどで無自覚に使っていたりすると危なっかしいので、それなりに気を留めておくと精神安定上良いでしょう。

パターン5、パターン6は内部でメソッドを使っている場合。ちなみにここではメソッドにしましたが、フィールドやプロパティでも同じ生成結果になります。抱え込む対象がstaticかinstanceかで変わってきて、staticの場合ならキャッシュされるので少しだけ有利です。

なお、この挙動は現時点でのVisual Studio 2015のC#コンパイラによって吐かれるコードであり(Unityの今のmonoもほぼ一緒、のはず、です、確か多分)、将来的にはそれぞれもう少し効率的になるかもしれません(メソッドを直接突っ込む場合のキャッシュとかは手を加える余地がある気がする)。とはいえ原理を考えたら、外部変数をキャプチャするラムダ式はどうやってもこうなるしかなさそうだったりなので、大筋で変わることはないと思います。

まとめ

正直なところ今回書いたのは細かい話です!別に気にしすぎてもしょうがないし、というかこんなの細部まで気にして避けながら書くのは不可能です。ギチギチに避けてラムダ式禁止だのLINQ禁止だの言い出すなら、早すぎる最適化の一種で、かなり愚かしい話です。が、ゲームの中にはひじょーにタイトな部分は存在するはずで、そこで無自覚に使ってしまうのも大きなダメージです。私だってタイトになることが想定されるUpdateループの中でLINQを貫くならやめろバカであり、普通にペタペタとforで書けとは思いますよ。

あんまりゼロイチで考えないで、柔軟に対処したいところですねえ。どこに使うべきで、使うべきでないか。まぁその見極めがむつかしいから全面禁止とかって話になるのは実際のところ非常によくわかる!のですが、それこそプロファイラで問題発見されてからでもいいじゃん、ぐらいの牧歌的な考えではいます。いやだって、そんなたかがLINQやラムダ式ぐらいであらゆるところがボトルネックになるわけないぢゃん?そんなのより大事なとこ沢山あるでしょう。それに比べたらLINQを普通に使えることのほうが、UniRxを普通に使えることのほうが100億倍素晴らしい。もちろん、地味な積み重ねでダメージが出てくるところであり、そして一個一個は地味だったりするから見つけづらくて辛いとかって話もありつつ。

そんなわけでUniRxは、かなり厳し目に考慮しながら作っているので、比較的概ね性能面でも安心して使えるはずです!まだもう少しやれることが残ってはいるんですが、ちょっと踏み込んで書いてみると謎のuNET weaver errorに見舞われて回避不能で死んでいるので、当面はこの辺が限界です(ほんとuNET絡みのエラーはなんとかして欲しい、理不尽極まりない)。とはいえ、何かネタがあれば継続してより良くしていきますので、よろしくおねがいします。

そういえば第一回Unityアセットコンテストでは、セミファイナリスト頂きました。ほぼほぼスクリプトのみの地味 of 地味なアセットであることを考えると全然上等で、嬉しい話です。

UniRx 5.0 - 完全書き直しによるパフォーマンス向上とヒューマンリーダブルなスタックトレース生成

UniRx(Reactive Extensions for Unity)のVer 5.0が昨日、AssetStoreにリリースされました。前回が4.8.2で6月なので、半年ぶりで、今回はメジャーアップデートとなります。現在の最新であるUnity 5.3(の新機能)に対応というのもあります、が、今回の目玉は書き直しです。半年間なにやっていたかというと、書き直そう!いよいよやっと重い腰を上げてスタックトレースに優しいコードにしよう!と思い立って始めてみたもののメンドウくささが極まって挫折して放置。してたんですが、先月ぐらいに、いい加減に手を付けたくて、ちょっとうちの会社の仕事時間を貰ってゴリゴリ進めてやっと終わりました。

とりあえず分かりやすい成果としては、スタックトレースです。

var rp = new ReactiveProperty<int>();
 
rp.Where(x => x % 2 == 0)
  .Select(x => x * x)
  .Take(10)
  .Subscribe(x => Debug.Log(x));
 
rp.Value = 100;

という人畜無害なコードがあるとして、以前のスタックトレースはこうです。

image

言ってることはわからんでもないコンパイラ生成の何かと、多量の中間物で埋まっていて、実に読み取りにくい。この程度のメソッドチェーンならまだマシで、もっと長大で、複雑なオペレータが絡んでる場合は困難極まってました。私も何度文句を言われて平謝りしたか分からないぐらいです。しかし、今回のバージョンからはこうです。

image

自動生成コードなし、中間物ナシ。圧倒的な読みやすさ!また、これはそのまま、書いたとおりに動いているということの証左でもあります。実行パイプラインの無駄がスタックトレースに出ているままに皆無になったので、パフォーマンスにも寄与しています(書き換えた今では、もはや前のが厚すぎた説はありますけれど、それはまぁ言わんといてください……)

実装はかなりメンドウで、ラムダ式を使うと問答無用でコンパイラ生成のクラスが吐かれてしまうので、ひたすら名前付きのクラスを作っていくお仕事をしました(一個のオペレーターにつき2~3のクラスを要求する、オーバーロードがあればその分だけ……)。また、Unityのコンソールの出力に合わせた細かい調整を施すことによって(+通常のスタックトレースへの吐かれ方に対しても調整して)作りました。すっかりスタックトレースのことを考えたプログラミングができる脳みそが出来上がったんですが、基本的に面倒くさ度100なので、ふつーのゲーム側のコードでは考えたくないしやりたくもないしやらなくていいと思ふ。

性能改善

じゃあ前のは遅かったのかよ、と言われると、うーん、そんなでもないですよ?、とは言いたいのですけれど、まぁカタログスペック的には実際3~10倍ぐらい速くなってます。これはねぇ、例えばMySQL 5.7が5.6の3倍速い!なるほど、じゃあ5.6はゲロ遅なのか?そうじゃあないっしょー、みたいな話なのですが、実際速くなったのは誰にとっても私にとっても嬉しい話です。

しかし、パフォーマンス低いとか気になるとか、漠然とした話で、何も言ってないに等しいんです。もちろん、3~10倍速くなったというのも何も言っちゃあいないです。プログラムの抱えている範囲に対して広すぎる、漠然としすぎていて何ら指標になっちゃいません。というのは気をつけてください。Rxのパフォーマンスを測るにあたって、フェーズ的に3つあって、

  • Observableを構築するフェーズ(さすがにこれはほとんど無視していい)
  • Subscribe = Observerを構築するフェーズ
  • OnNext

それぞれは独立して考える必要があります。また、ReactivePropertyはSubscribeと同時にOnNextも一回入るのでSubscribe + OnNextである、などなどがあるので、どこをどう測りたいかを明確にし、どう測るかを考えないとザルな結果になります。

基本的に、Rxのチェーンの寿命は長いのでOnNextの性能を最重要視して見るべきです。ここの区別は非常に大事です、長ければチェーン構築コストは相対的に無視できる範囲に収まるのでマイクロな結果で想像するのは違うってものです。が、初回に大量にSubscribeが発生するといった、ローディング的な意味合いでは、Subscribeのフェーズも鑑みる必要があります。

んで、これもザックリとしすぎでアレなんですが、OnNextは3~5倍ぐらい、Subscribeに関しては10~20倍速くなりました。OnNextは全体的なパイプラインの最適化のオペレーターの実装調整が効いてるんですが、Subscribeは抜本的に最適化/単純化したので、以前と全然違う結果になってます。これは、社内で大量のSubscribeがシーンロード初回に発生するという事案がありまして、Subscribeを改善しない限りロード長過ぎで終わぽ、だったのでなんとかしました、はい、すびばせん今まで手付かずで……(ちなみに本家Rx.NETとやり方変えてるので本家Rx.NETよりも速い)

あとのところはオペレーター次第です。WhereとかSelectとか、単純な奴は実装変わってないんで大差ないんですが、一部のメソッドの実装が素朴でしょっぱかったので、そういうのはきっちり直してるので以前のと全然性能変わってきてます。特にObserveOnが顕著かな。また、Observable.IntervalやTimerなどの一部の時間系メソッドも構造がガラッと変わってるので(MainThreadScheduler/ThreadPoolSchedulerが使われる場合には最適化パスを通るようにしてる)、かなり良好な結果が得られるのではないかと。

全体的にGCゴミも減ってます。まだもう少し減らせるポイントが残ってるので、次のマイナーアップデートではその辺の処理をする予定デス。

リリースノート

今回の。

破壊的変更:
iOS/AOTサポートは切りました。IL2CPPしかサポートしません。
Unit/Tuple/CancellationToken/TimeInterval/Timestampedをclassからstructに変えました。
MainThreadDispatcher.Postのメソッドシグネチャが変わり、T stateを要求します。
ObservableMonoBehaviour/TypedMonoBehaviourがObsoleteになりました。
AotSafe Extensions(WrapValueToClass)を消しました。
InputField.OnValueChangeAsObservableをOnValueChangedAsObservableにリネームしています(Unity 5.3の場合。Unity 5.3でInputField側で同様の変更が入っているため)
Subscribe in SubscribeでのException Durabilityを保証します。
 
追加メソッド/クラス:
Observable.ForEachAsync
Observable.Take(duration)
Observable.Aggregate
Observable.Zip(T3~T7)
Observable.CombineLatest(T3~T7)
Observable.Start(function, timeSpan)
Observable.ToYieldInstruction in Unity 5.3
Observable.DoOnError
Observable.DoOnCompleted
Observable.DoOnTerminate
Observable.DoOnSubscribe
Observable.DoOnCancel
Observable.CreateSafe
Progress
StableCompositeDisposable
MultilineReactivePropertyAttribute
 
その他色々修正:
色々色々(詳しくはGitHubのとこの正式なリリースノート見てくだしあ)

破壊的変更といっても、直撃することはないんじゃないかなあ、と思ってます。ただ社内ではUnit/Tupleのstructへの変更で引っかかったりはしました(想定外にもnullが代入されている場合があった!)。それは適切にdefault使うのと、Tupleに関してはTuple?にするなりする程度で対応はできます。struct化はAOTサポートを切ることで躊躇いなくできるようになって、ヨイことだなー、と。コードも全体的にAOTサポートのための余計なコードを順次切り落としています(パフォーマンスロスに繋がっていたので)。その辺はIL2CPPバンザイ、ですかねえ。

vs IL2CPP - Runtime UnitTest Runnner

IL2CPP万歳と言ったそばから言うのもアレですが、IL2CPP苦しい……。コンパイル死ぬほど遅いし、というのはおいておいても、まだ地雷は埋まっていて、たまに踏んで死ぬんですよね。その場合IL2CPPのバグなんで報告して直してもらうってことになるんですが、それはそれとして、なんで死ぬのかがAOTの場合は想像ついたし対処も比較的容易だったんですが、IL2CPPは踏むまで地雷かどうかを察知することが不能な上に、踏んだら踏んだで、何を踏んだからこうなったかがイマイチ分からなくて最小ケース作ってバグレポも辛いケースもちらほら。

とはいえ、それなりに安定してきてるのは確かだと思います。偉い。そこは賞賛されるべき。

のはいいんですが、実行するまで分からないじゃ(特にライブラリ側としては)困るので、iOS実機でユニットテストを動かしたいと思いました。Unity 5.3からEditor Test Runnerなども標準で入ってきましたが、端的に言えば、欲しいのはそれじゃない。実機で動かしたいの!エディターでの実行はどうでもいいの!

エディター上での実行も大事なんですが、元々UniRxは.NET用ライブラリとしても動くように設計されていて、ユニットテストも.NET用ライブラリとしてMSTestで書かれている(!)という特殊な環境なので、エディターでのテストサポートは完全に不要なのです。いや、だってVSのテストランナー使ったほうがやりやすいじゃん?

image

そうやってユニットテスト自体は書かれてるし、さすがに実機用に別のを書きなおすのは不可能なので、このユニットテストを実機で動かせるように持ってければそれでいいんだよねー。

ここで出てくるのがRoslyn。Roslynを使ってユニットテストプロジェクト内のユニットテストを、ソースコードのファイル単位ではなく、解析可能な構文木単位で取得し、T4 Text Templateで整形して吐き出せちゃえばいいんだ、という合わせ技で運搬することに成功しました。VS2015だから出来るハック、VS2015最高……。さすがにコード持ってくだけではMSTestの実体がなくて動かないんですが、そこは適当にモック(Shim)を用意して回避しました。

image

エクストリーム雑なUI。エラーが出た場合は赤くなってExceptionを表示します。これで、ちゃんとiOS/IL2CPPで全部パスしてるのを確認済みです。

ちなみにこのRoslyn + T4でコード生成するテクニック、今回のように別プロジェクトをターゲットにして運搬するというのもいいんですが、自プロジェクトを対象にすることもできます。T4で生成するためのコードのタネって、今まではT4側に書くしかなくて面倒だったんですが、もうその制限はありません。ありとあらゆるソースコードがコード生成のためのタネとして使えます。メタプログラミングの扉をまた一つ開いてしまった。

このテクニックは私の発明じゃなくてRoslynをT4テンプレート内で使う - ぷろじぇくと、みすじら。から拝借してますので、気になる人はそちらの記事をどうぞ。l

Unhandled Exception Durability

UniRx 5.0の変更のうち、ちょっとだけ重要なのがUnhandled Exception Durabilityというコンセプト。です。これは、Rxでイベントハンドリングするのはいいんだけどエラーでるとイベント購読が吹っ飛ぶの困るんだよねー、に対するUniRxからの回答ということで。内容ですが、Subscribe in Subscribe時の例外を外側に伝搬「しない」ことを保証しています(逆に言えば実は4.8では保証されてなくて解除されたりしてました。ちなみにRx.NETでも保証されてなくて解除されたりされなかったりします、ここはUniRx独自で挙動を明言する形に倒しています)。伝搬しない、というのは握りつぶすという意味ではなくて、ObservableのDispose処理を行わない、という意味です(例外自体はグローバルに飛ぶのでUnityのConsoleにExceptionが表示されるし、ログイベントでちゃんと捉えられます)

button.OnClickAsObservable().Subscribe(_ =>
{
    // もし内側でエラーが発生しても、外側のOnClickがデタッチされることはない
    ObservableWWW.Get("htttp://error/").Subscribe(x =>
    {
        Debug.Log(x);
    });
});

エラーハンドリングは難しい問題で、RxJavaのErrorHandlingの章を読んでも別にそんなワカラナイよね、とかって感じではある。UniRxでは Retry/OnErrorRetry でハンドルできなくはなく、まぁそれがスタンダードなRx WayではあるんですがRxJS の Operators (6) - Observable のエラーハンドリングのまとめコメント「これで本当にエラーハンドリングに十分なのか不安です。」とあるように、実に不安です。

で、入力用のハンドラーが吹っ飛ぶのは致命傷なので、どうしても救いたいその辺のとこに関してはSubscribe in Subscribeで処理するのがいいんじゃないかなー、というのを提唱します。入力イベントを合成したいって局面も多いと思うので、それはそれで合成してもらったうえで(そして、その合成パイプラインに関してはエラーが出ないよう厳重に作る!)、それを入力ストリームだと考えて、そこから先はSubscribe in Subscribe。あまり格好の良いものではないのも事実ですが、現実的っちゃあ現実的かなー、と。ちなみにこの挙動を保証するのはUniRxだけだと思うので他のRx系に持ってっても動きません(多分)

なお、Subscribe in Subscribeでの例外で解除されないのは最上流がHot Observableのものだけです。HotとColdに関してはRxのHotとColdについてなどを参照するといいと思いますが、とりあえず具体的にHotなのはUniRxデフォルトでは FromEvent/Subject/ReactiveProperty/ObservableTriggers/UnityUI.AsObservable です。ようはイベント的なやつです。Coldなのは Return/Interval/Timer/FromCoroutine などで、これらは例外で解除されます(そうじゃないとTimerとか無限に動き続けられても危なくて困るでしょ?FromCoroutineだって途中でエラーが出てる状態なのに回られても困るでしょ?)

CustomYieldInstuction

書き直しはいいんだけど、何か新機能ないと寂しいよなー、ということで、Unity 5.3用に一つ入れました。Unityブログでもカスタムコルーチンとして紹介されていますが、Unity 5.3からCustomYieldInstructionが搭載されました。というわけでUniRxもUnity 5.3以上ならToYieldInsturctionメソッドが使えるようになっています。

IEnumerator TestNewCustomYieldInstruction()
{
    // Rx Observableをyield returnで待ちます.
    yield return Observable.Timer(TimeSpan.FromSeconds(1)).ToYieldInstruction();
 
    // スケジューラを変える(Time.scaleを無視する)とかも当然可能
    yield return Observable.Timer(TimeSpan.FromSeconds(1), Scheduler.MainThreadIgnoreTimeScale).ToYieldInstruction();
 
    // 戻り値を得る場合はObservableYieldInstructionを変数に取れば、Result/Errorで受け取れます
    var o = ObservableWWW.Get("http://unity3d.com/").ToYieldInstruction(throwOnError: false);
    yield return o;
 
    if (o.HasError) { Debug.Log(o.Error.ToString()); }
    if (o.HasResult) { Debug.Log(o.Result); }
 
    // 当然こういう長めのものだって自由に書けます 
    yield return this.transform.ObserveEveryValueChanged(x => x.position)
        .FirstOrDefault(p => p.y >= 100)
        .ToYieldInstruction();
}

今までもToAwaitableEnumerator/StartAsCoroutineというメソッドで同様なことを出来るようにしていたのですが、ToYieldInsturctionのほうが効率的だし、使いやすいです。ToYieldInsturctionによるObservable->Coroutine変換のオーバーヘッドはないといっても過言ではない!Unity 5.3最高!

ちなみに、このToYieldInsturctionはCustomYieldInstructionクラスを実装してません。Unity 5.3のカスタムコルーチン対応というのは、yield returnでIEnumeratorを受け取ると毎フレームMoveNextを呼び出して待機する、というのが正しい話です。CustomYieldInstructionはあくまでIEnumerator実装のためのちょっとしたヘルパーなので、別にそれにこだわる必要はありません、ということで普通に独自の軽量なIEnumerator実装を刺しています。

ちなみに実行されるタイミングはCustomYieldInstructionの説明によると after MonoBehaviour.Update and before MonoBehaviour.LateUpdate だそうなので、実行タイミング調整のネタに使えるかもしれません。

まとめ

実際のトコver 2.0なんですが、諸事情で4始まりなのでver 5.0です!Unityのメジャーバージョンと偶然揃ったしいっか、という気がしますね!今回のコードはかなり自信あって、パフォーマンスがー、な局面であってもお薦めできます。どうせ、ライトウェイトを冠した超機能限定版の同じようなものを実装するなら、性能面であっても素直にUniRxを使ったほうがいいでしょう。と、言えます。言えます。

今月頭に書いたUnity 5.3のMulti Scene EditingをUniRxによるシーンナビゲーションで統合するなどのように、UniRxを前提に置くことで、やれることが大幅に広がります。根底から入れれば全体のプログラミングの世界観が(良くも悪くも)大きく変わります。が、まぁそれはエキセントリックすぎるということであれば、触りは単純なところからでも全然アリかな、とは。思います。特に非同期/マルチスレッド関連は、変なライブラリ入れるよりもずっと良いでしょう。

ところで半年前、今年6月に第一回UniRx勉強会を開催しましたが、第二回の需要ってありますか?もしありましたら、その前に発表者が必要!なので、是非話したい!人は、私のTwitterかメールかに連絡ください。開催するにも発表者いなければ開催もなにもないですからね……!

ついでにもはや触れちゃいけない扱いの気がしなくもないUnity アセットコンテストというのに応募していたのですが結果発表……。

Roslyn C# Scriptingによる実行できるコンフィグの手法と実活用例

Advent Calendar大遅刻組です。というわけでC# Advent Calendar 2015の10日目です!なんで遅刻したかというと、記事のネタのためのライブラリを作るのに思いの外時間がかかってしまったから…… コンセプトも固まってたしプロト実装も済んでたんですが、最終的な形に落としこむのが想定よりちょっと割と大変だった……。すびばせんすびばせん。

どうやらC# Advent Calendarは2011年から書いてるので5回目ですね、へぇー。過去を振り返るとModern C# Programming Style Guide、モダンつってもC# 4.0時代ですが、今ぱっと見直すと別にここで言ってることは今も変わらないですね、これに5.0, 6.0の話を足せばいいだけの話で。2012年はMemcachedTranscoder - C#のMemcached用シリアライザライブラリということで、このライブラリは別に私自身も使ってないので割とどうでもいー、んですが、まぁシリアライザにまつわる諸々についての知見が少しは入ってる模様。2013年の非同期時代のLINQはいい話だなー、これがC# 5.0のModern Styleの追記差分みたいなもので、実際、今現在においては超絶大事な部分。2014年はVS2015+RoslynによるCodeRefactoringProviderの作り方と活用法で、C# 6.0ではないですが、その世代ではAnalyzerは中心になってくるので、これがC# 6.0の差分といってもいいでしょう。多分きっと。

Roslyn C# Scripting

Roslyn、Compiler as a Serviceとか言ってましたが、やっぱスクリプティングが華形だと思うのです。が、しかし。が、しかし。今の今までRoslynに関する話題で、Scripting APIに関するお話はあまり上ってませんでした。理由は単純で、今の今まで未完成品だったから。先月末に出たVisual Studio 2015 Update 1でC# Interactiveが、そして同時にNuGetでもMicrosoft.CodeAnalysis.CSharp.Scriptingで、現在は1.1.1が配布されることにより(ところでこれのパッケージ名が中々定まらなくて実際これであってるのか不安だけどLast updatedが2015/12/3なのでこれでいいでしょう、まだDL数が405ですけど!)やっと全てのピースが揃った感じです。

Scriptingについてのドキュメントは、RoslynのWikiにある2つのページを見ておけば十分でしょう。Interactive-Windowには、csxの仕様っぽいもの、特殊なDirectiveの説明があります(#rとか#loadとか)。Scripting-API-Samplesにはプログラムから触った時のAPIとしてどんなものを持ってるか、どういう風に使えるかが書いてあります。かなりシンプルなので、そんな難しくなくすぐ使えます。

ちなみにC# Interactiveはまだまだ全然使えないって感じなので、期待するほどのものでもないですね。csxもエディタサポートが実質、シンタックスハイライトぐらいなので厳すぃ。黙ってLINQPad使いましょう、課金しましょう。

Roslyn時代のコンフィグ

最近というか数年前からずっと構造化ログにご執心で、EtwStream - ETW/EventSourceのRx化 + ビューアーとしてのLINQPad統合というのを作ってたんですが、今回はそれに、ファイル等への出力プラグイン(Sink)と外部サービス(EtwStream.Service)を作りました。アプリケーションから出力されるログは、ETWというWindows内部に流れてる高速なロギングストリーム機構を通して、別プロセスのEtwStream.Serviceで受け取ります。ログは特に最近ではファイル出力など比較的安定性が保証されているものだけでなく、ネットワークを通じて配信するケースも少なくありません。ログの扱いが別プロセスに別れることにより、アプリケーションに与える影響が少なくなるほか、アプリケーションの状態(アプリ自体の終了/デプロイでの入れ替わり等)に気を配る必要もなくなります。

というのが外部サービスであることの意義なのですが、問題はコンフィグです。コンフィグ。元々EtwStreamはObservableEventListenerという、IObservble<LogEvent>の形でログをストリームで受け取り、それをRxで自由にフィルタしたりグルーピングしたりマージしたりなんでも出来ますよね、という究極の自由度がウリでした。しかしコンフィグです、Rxのその柔軟性をコンフィグで実現するのは不可能です。物凄く機能を削った単純なSubscribeで我慢するか、あるいは超絶複雑なXMLでそれっぽいものを構築するか(log4netやNLogのXMLコンフィグが死ぬほど難解で複雑なのは、ログのルーティング自体が複雑で、それをコンフィグで表現することが困難だということなのです)になります。

せっかく、ログを現代的なReactive Extensionで表現することができたのに、外部サービスにした途端に破棄しなければならないのか。それでいいわけがなく、そこでC# Scriptingの出番になります。EtwStream.Serviceはコンフィグをconfiguration.csxとして、以下のように書きます。

// configuration.csx
 
// 5秒 or 1000件でバッファリング(ふつーのRxのBufferを利用)
// 出力フォーマットは普通にFunc<TraceEvent, string>で整形できる!
ObservableEventListener.FromTraceEvent("SampleEventSource")
    .Buffer(TimeSpan.FromSeconds(5), 1000, EtwStreamService.TerminateToken)
    .LogToFile("log.txt", x => $"[{DateTime.Now.ToString("yyyy/MM/dd hh:mm:ss")}][{x.Level}]{x.DumpPayload()}", Encoding.UTF8, autoFlush: false)
    .AddTo(EtwStreamService.Container);

基本的に完全にC#そのものなので、全てのRxのメソッドが使えて自由に合成・ルーティングが可能です。これはIn-Processで書いてる際(普通のロガーとしてC#コードで埋め込む場合)もOut-Of-Process Serviceでコンフィグとして書く場合も(ほぼ)同じコードで表現できるということです。もちろんC#で書けるということは、 System.Configuration.ConfigurationManager.AppSettings から設定を引っ張ってきたり、ネットワーク通信して何か引っ張ってきたりとかも自由自在やりたいほーだいです。

例えばこれをNLogで表現すると

<targets>
    <default-wrapper xsi:type="BufferingWrapper" bufferSize="1000" flushTimeout="5000" />
    <target name="file" xsi:type="File" fileName="log.txt" keepFileOpen="true"
		layout="[${date:format=yyyy/MM/dd hh\:mm\:ss}][${level}]${message}" />
</targets>
<rules>
    <logger name="*" minlevel="Debug" writeTo="file" />
</rules>

になります。NLogの独自フォーマットルールに従って書く必要があるし、メッセージ書式も独自テンプレートになります。とはいえ、これはまだ単体なので遥かにマシで、色々複合的なことをやろうとするとすぐに膨れ上がって意味不明なことになるのは、みんな経験のあることなのではないでしょうか?

バイナリがEtwStream/releases/EtwStream.Serviceに転がってるので、是非ちょっとだけ遊んでみてくださいな。

仕組み

csxをEvaluateしてるだけです。基本的にcsxの実行は即座に終わります、ObservableEventListenerをSubscribeしているだけですから。しかし、csxが終了してもSubscribeは生き続けています!(言われてみると当たり前のようで、最初はそうなの?と違和感はありました)。それにより、流れてくるログは(別スレッド上の)ObservableEventListenerを流れて、csx上のRxを通りcsx上でのSubscribeにより処理され続けます。というわけで、EtwStream.Serviceのcsxは、ただのXMLコンフィグがcsxに変わっただけ、ではなく、このcsxはコンフィグのようでコンフィグじゃなく、実行コードそのものなのです!

終了処理に関しては、ホスト側から渡しているTerminateTokenと、AddToを通してSubscriptionを登録していることにより制御されています。csxの評価としての実行が終わっていても、裏で生き続けている限り同じ参照を持っているので、ホスト側から干渉することが可能です。なので、ServiceのStopイベント時にはTerminateTokenにCancel命令をホストが出すことにより、Rxの残ってるBufferが送り出され、AddToで受け取っているSubscriptionを待つことにより、溜まったログの処理が完了するまで待機するといった、安全な終了処理を可能にしています。この辺はRxをフル活用してパイプライン組んだ成果ということで。

再びエディタとしてのLINQPad

さて、csxで書けるところのイイトコロはC#なのでコンパイルエラーも検出できるしシンタックスハイライトもあるし、などなど、なのですが、エディタサポートは……。Visual Studio 2015のUpdate 1によって確かにシンタックスハイライトはついた、が、それだけ……。IntelliSenseもDLL読み込ませたり色々しなければなので実質使えないみたいなもので厳しい……。

そこで出てくるのがLINQPad。

image

EtwStream.LINQPadには、EtwStream.Serviceのcsxで渡されてくるEtwStreamServiceクラスのShim成分が入っているので、csxと互換性があって、LINQPadで実際にコンパイルできる/動かして確認した結果をcsxに持っていくことが可能です。(というようなことが出来るようにAPIを調整したんです……)。C# Interactiveが使い物にならならいなら使い物になるまで我慢する、のではなくて、一時凌ぎでもなんでも、他の現実的な解法を探すのが正すぃ。クソだクソだと文句だけ言ってても何も動きませんしね。必要なのは今この場でどうするか、それだけ。

Topshelf

Windowsサービスの実装にはTopshelfというライブラリを用いています。これは、最高に良いです。もはやこれなしでWindowsサービスを実装するのは考えられません!Visual Studioのテンプレートからふつーにサービスを作ると、なんかゴチャゴチャしたのが吐かれてよくわからない上に実行も面倒だし(いちいちinstallしたくないでしょ?)デバッグも困難だし、実にヤバい最低な開発環境。Topshelfで作るとコンソールアプリケーションと同じ感覚で作れます。また、成果物のexeは、そのまんまふつーにコンソールアプリケーションとしても動くので、EtwStream.Serviceの場合、ビューアーとしてLINQPadを要求していましたが、EtwStream.Service.exeを実行すれば普通にビューアーになります(csxで書き出し先をConsole(LogToConsole)にすれば色分けもしてくれる)。サービスとしてのインストールは「install」をつけて実行するだけ。素晴らしい。

日本語ではWindowsサービスを楽に開発~TopShelf~TopShelf によるWindowsサービスの配置をDSCで自動化してみように説明ありますが、本当に簡単なので、サービスを作る機会がある人は是非使ってみてください。超お薦め。

ファイル出力時のロガーのパフォーマンス

今回ロガーを全部自作する都合上、さすがに単純なファイル書き出しと、ローテーションするファイル書き出しは用意しとかないとなぁ、ということで作ったんですが(FileSink, RollingFileSink)、作ってる上でなんとなく気づいたことなど。

そもそもファイルに吐くっていうこと自体がレガスィーで好きじゃないんですが、それはそれとしてもやはり重要なのは間違いありませんし、普通に使います。で、特にInProcessでのロギングの場合、これに気を配らないと普通にパフォーマンス上のボトルネックになってしまったりするわけですねー。さて、で、パフォーマンスは設定が全てです。とりあえず、バッファリングと非同期の二つのオプションを探しましょう。まず、ファイルに吐く場合のパフォーマンスはバッファするかしないかで全く変わるし、逆にバッファさえすればよほどタコな実装じゃない限りはそんな差はなくふつーに性能出ます(多分)。もう一点はasyncですね、これは別に大抵は非同期I/Oじゃなくて別スレッドで書くってだけのパターンなんですが、これが有効だとロガーの動作がアプリケーション自体に一切影響しなくなりますので。まぁバッファを有効にしてれば、例えば1000件に一回書く設定だったら1/1000回以外は書き込み処理に時間喰われることはなくなるので、ほぼ無視できてあってもなくても大差なくなるんですが、(起こるかもしれない)ちょっとしたスパイクは抑制できるかもしれません。また、あえてバッファはオフにしてasyncだけオン(+即時Flush)にすれば、ログが中々Flushされなくてリアルタイムで確認したいのにイライラ、というのがなくなって良いかもしれません。この辺は好みとか要件しだいで。

とりあえず言えるのはデフォの設定がどうなってるかはともかく、ノーバッファでノーエーシンクだと当然のように遅いです。更に設定によってはファイルストリームを都度閉じるか開きっぱがオプションになってるものもありますが、これは当然、開きっぱじゃないとゲロ遅いです。そういう項目がオプションにある場合は注意しましょう。デフォが都度閉じるだったりしてね……(NLogがそうです。NLogのデフォルトは安全寄りに倒し過ぎでパフォーマンスがヤヴァいことになってるので、NLog使う場合はそれなりに弄ったほうがいいでしょう。かといって他のロガーもそう変わりはなくて、大抵はそれなりに弄らないと遅いです)

かわりに、バッファや非同期ってのはログの消失の危険性があります。書いた瞬間には保存されてないってことですからね、アプリケーション終了への耐性が低くなります。気の利いたロガーは、可能な限り、終了を検知して(AppDomainが消える時のイベントとかをハンドリングして)、残ってるバッファを出力しに行ったり非同期の終了を待機しに行ってくれたりはしますが、パーフェクトではありません。例えばAppDomain.ProcessExitのタイムアウトは既定で2秒です。2秒以内にフラッシュが完了する保証はないわけで、そこで完了できなければログロストです。

それを避けるには、「パフォーマンス低下を承知してバッファや非同期オプションを使わない」というのも手ですが、EtwStreamは更に2つの選択肢を提供してます。一つは「Out-Of-Process Serviceでのログ収集」。ETWへのログ出力はほぼノーコストで即時に吐けるのでアプリケーションへの影響は一切無い上に、それを外部サービスで取り出せば、出力側の終了の影響を全く受けません。ただし当然、受け取る側の外部サービスが死んだらロストするという危険性はありますがね!そこに関しては知らんがなというかshoganaiというか精一杯堅牢性を高めますとしか言い様がないですにぇ。

もう一つは、プログラム的に終了が完全に待機できるSubscriptionシステム。もともとEtwStreamは設定をC#で、Rxで書く必要があるので、購読状態に関して100%コントロールできます。というわけでその辺に仕掛けを入れといて

static void Main()
{
    // ApplicationStartの部分でこの2つを用意する
    var cts = new CancellationTokenSource();
    var container = new SubscriptionContainer();
 
    // でログの設定する
    ObservableEventListener.FromTraceEvent("SampleEventSource")
        .Buffer(TimeSpan.FromSeconds(5), 1000, cts.Token)
        .LogToFile("log.txt", x => $"[{DateTime.Now.ToString("yyyy/MM/dd hh:mm:ss")}][{x.Level}]{x.DumpPayload()}", Encoding.UTF8, autoFlush: false)
        .AddTo(container);
 
    // --- 実際にアプリが動いてる部分 --- //
 
    // アプリが終了した時のイベントのところでハンドリングする(Form_ClosedでもApplication_Endでもなんでもいいですが)
    cts.Cancel(); // CancellationTokenのCancelによりBufferの残りが吐き出される
    container.Dispose(); // Subscriptionの完了を待機する
}

といった風にすれば、100%コントロールされて停止時のログ処理を完了させられます。csxでもEtwStreamService.TerminateTokenとか渡していたのと同じことをやればいいということで。

100%コントロールできる代わりに、逆にEtwStreamは自分でコントロールしないかぎりは、気の利いた終了の検知とか組み込んでないので、待たなければふつーにバッファは消えます。これに関しては、10年前はゴテゴテとブラックボックスの中で気の利いたことをしてくれるのが正義だったかもしれませんが、2015年の現代では仕組みはシンプルに、薄くしたうえで、自分でコントロールさせるのが正義だと思ってます。そういう流儀。どっちが正しいってこともないですが、まぁ、今風なんじゃないかな?

ついでに言えば、EtwStreamのFileSinkやRollingFileSinkのパフォーマンスはバッファしてる前提同士で比較しても、他のよりも高い性能を誇ります。理由は幾つかあって、そもそも性能を意識して書いてるから。というのと、.NET 4.6以外をサポートする気がないのでasync/awaitやTPL全開でコードを書いてるから。オプション自体も同期処理は一切なくて、書き出しは非同期I/Oのみに限定などの割り切り。そして、通常はログフォーマット整形などに独自テンプレート的なのを挟まなきゃいけないところを、csxのお陰でFuncで処理できるため、そもそもコードパスに一切のオーバーヘッドがない。C# Scriptingによるコンフィグはパフォーマンスにも寄与するわけです。

しかしまぁ、JavaではBlitz4jlog4j2のAsynchronous Loggerなどのスピード競争があるのに、.NETの牧歌的なこと、といった感じは否めませんねぇ。そんなだから私のとりあえずの雑実装でもfastestになってしまうわけで……。

出力先

EtwStreamが提唱するのは構造化ログ(Structured/Semantic Log)ですし、テキストログが終着点ではありません!テキストログは無視して、構造化されたペイロードを、そのままAzure EventHubsAmazon KinesisGoogle BigQueryのStreaming Insertに流して、ただたんに溜めるのではなくて、即座に分析可能な状態にするのが理想形です。特にお薦めなのは、というか弊社で使ってるのはGoogle BigQueryです。事例として株式会社グラニの Google Cloud Platform 導入事例: 「using CSharp;」という軸と BigQuery の活用で、先進性を求め続ける。を掲載してもらいました:)

今のとこEtwStream用のBigQuerySinkはないんですが(!)そのうち公開するかされるかするんじゃないでしょーか多分きっと。(本当はそれも作って持ってきたかったんですがもう完全に時間切れでして、すでに大遅刻だし……)。そういえばあとFromTraceEventでRegisteredTraceEventが取れるようになりました。これはつい数日前のTraceEventライブラリのアップデートでそうなったから、というだけなんですが、今まで取れてなかったんですよねー。これで大丈夫。というのと、SLABのOut-of-Process Serviceじゃダメな理由に.NET 4.6からEventSourceに追加されたself-describing events(超重要!)に対応してないとか色々あるんですが、そういった話はまたの機会にでも。

まとめ

ロガーの未来はこうあるべきだ、という構想自体は1年以上前からあったんですが、Roslyn C# Scripting APIが正式リリースされてやっと作れた!あと、基本的にはMicrosoft Patterns & PracticesのSemantic Logging Application Block(SLAB)の影響が濃くはあるんですが、更新されなすぎだし、v3はElastic Search + LogStash + Kibana on Azureとか言ってて、マジで終わってるなという感じでもはや見限るしかない……。P&Pは相変わらず本当にやっぱダメですねーという残念さ加減。なにがPatterns & Practicesだよっていう。

csxでコンフィグするライブラリとしてConfigRというのがあるんですが、XMLをcsxで置き換えるだけじゃ、あんま意味がないかな。必要なのは、コンフィグができることじゃなくて、それそのものが実行されて自走することだというのに気づいたので、使うことはなかったし、多分他のアプリケーションでもConfigRを使うことはないと思います。XMLはいうて設定ファイルとしては悪くないんですよねー、XSLTなんかもやり過ぎなければ良い機構ですし。逆にJSONを設定ファイルとして使うのは最低最悪なチョイス(なのでDNXへのやる気が0.1ミリも起きない)

何れにせよ、徐々にではあるでしょうが、csxの面白い活用例というのはどんどん出てくるのではないかと思います、と言いたいんですがfsxが別に対して面白い活用はされてないことを考えるとそんなに出てこないかもしれませんね、とも思いますが、いえいや面白い活用例はやっぱ出てくるかもしれません。batをcsxにしましたとかってだけだと別に面白くもないし意味もそんなにないですからねー、まぁあってもいいけど。もっと本質的に変わるような事例が増えてくれれば何よりです。

Unity 5.3のMulti Scene EditingをUniRxによるシーンナビゲーションで統合する

今回はUnity Advent Calendar 2015のための記事になります。昨日はtsubaki_t1さんによるUnity初心者を脱するためのデバッグ入門…的なやつでした。私はとりあえずVisual Studioでアタッチしてステップ実行、でしょうか……。最近はiOSのIL2CPPのスタックトレースが行番号出してくれなくて禿げそうというのが社内のホットトピックスらすぃ。

去年もUnity Advent Calendarには参加していて、その時はUnityのコルーチンの分解、或いはUniRxのMainThreadDispatcherについてという内容でした。今回も引き続き、私の作成しているUniRx - Reactive Extensions for Unityのお話ということでお願いします。とはいえ、中身的にはMulti Scene Editingや、シーン間での引数渡しをやるのにどうすればいいのか、みたいなところなので、Rxのメソッドは特に説明なくバンバン出てきますが、Rxワカラナイ人はそのへんは雰囲気で流し読みしてもらって、シーン遷移についてのお話を読み取ってもらえれば嬉しいですねん。

Multi Scene Editing

Multi Scene Editingは初出が2014/8/4のUnity Blogの記事でしょうか、1年経ってやっと正式リリース、までまもなく!ですね、5.3から搭載されることになりました。実際どういうことになるかというと、ヒエラルキーウィンドウがこんな感じに。

image

シーン加算で読み込んだシーンがヒエラルキー上でもきっちり分けられます。DontDestroyOnLoadがついたものは専用のところに隔離される。シーンを削除する場合も、そのまま指定してサクッと消したり、マージできたりと、随分とシーン管理がやりやすくなりました。Unity 5.3からはいよいよシーン加算で管理する時代が到来する!

コード的にはUnityEngine.SceneManagement.SceneManagerに全部のAPIがつまってます。基本的にはLoadScene/Asyncか、UnloadSceneぐらいで事足りるのではないでせうか。

// SceneA -> SceneBへボタン押したら加算
// 別にRx使う必要性はないけど無駄に使うエディション
button.OnClickAsObservable()
  .SelectMany(_ => SceneManager.LoadSceneAsync("SceneB", LoadSceneMode.Additive).AsObservable())
  .Subscribe(_ => { /* 完了時の処理何かあれば */ });

この程度だとRx使う必要性はゼロですが、一応、LoadSceneAsyncの戻り値であるAsyncOperationはAsObservableで直接サクッとRx的に変換可能です。

シーン間に引数を渡す

どういうこっちゃって話ですが、新しいシーンに遷移なり加算したいってことは、引数を渡したくて然りだと思うのです。そのシーンを表示する際の初期引数が。例えばアイテム一覧画面から、アイテムの詳細画面を出すなら、アイテムのIDを渡したいよね、とかね。別にAndroidやiOSアプリでも、ウェブのURLのクエリストリングなりなんなりでも、そんなのは普通によくある話です。さて、SceneManagerはその辺りのことは、別になにも面倒みてくれません。じゃあグローバル変数を経由してやりとりするのかというと果てしなくビミョウというかスパゲティ化まったなし。せっかく画面画面がシーンで独立しているなら、値の依存関係もシーン内に抑えてやりたい。

というわけで、遷移/加算時に引数を渡せるシーン遷移機構を作りましょう。

材料として使うのはUniRxのPresenterBaseです。これは何かというと、子要素の初期化の順序をコントロールするのと、値の受け渡しができる仕組みです。ご存知のとおりUnityのGameObjectの初期化順序は不定(Execution Orderでおおまかに指定できるけど、細かいコントロールのために使うものではない)ですが、PresenterBaseの管理下におくことで、Startフェーズにて決められた順序で起動するようにコード上で設定できます。

この性質は、シーンに引数が渡される、つまり全てのルートになるという条件にぴったりです!というわけで、引数を受け取るための基底クラス、SceneBaseをPresenterBaseを継承して作りましょう。

public abstract class SceneBase : PresenterBase
{
    // これがシーン遷移時にセットされる引数を表す
    public object Argument { get; set; }
 
    // 受け渡されたかどうかを管理するフラグ
    public bool IsLoaded { get; set; }
 
    protected override void OnAwake()
    {
        // 初期化が完了した際はロード済みと強制的にマークするおまじない
        this.InitializeAsObservable().Subscribe(_ => IsLoaded = true);
    }
}

こんなもので、割とあっさりめに。実際のシーンのクラスは

// このどうでもいいクラスを引数として渡していくということにする
public class Nanika
{
    public int HogeHoge { get; set; }
    public string Hugahuga { get; set; }
}
 
// 遷移元クラス、適当なボタン押したらSceneBに遷移する
public class SceneA : SceneBase
{
    public Button button;
 
    protected override IPresenter[] Children
    {
        get { return EmptyChildren; }
    }
 
    protected override void BeforeInitialize()
    {
    }
 
    protected override void Initialize()
    {
        button.OnClickAsObservable().Subscribe(_ =>
        {
            // 直接SceneManager.LoadSceneAsyncを呼ぶのではなく、
            // 独自に作成したNavigationService.NavigateAsync経由で引数を渡して遷移/加算する
            var arg = new Nanika { HogeHoge = 100, Hugahuga = "Tako" };
            NavigationService.NavigateAsync("SceneB", arg, LoadSceneMode.Additive).Subscribe();
        });
    }
}
 
// 遷移先クラス、Argumentに引数が渡されてきてる
public class SceneB : SceneBase
{
    protected override IPresenter[] Children
    {
        get { return EmptyChildren; }
    }
 
    protected override void BeforeInitialize()
    {
    }
 
    protected override void Initialize()
    {
        // 前のシーンから渡された引数が取れる
        var arg = Argument as Nanika;
        Debug.Log("HogeHoge:" + arg.HogeHoge + " HugaHuga:" + arg.Hugahuga);
    }
}

ちょっと長いですが、言いたいのは遷移元ではNavigationService.NavigateAsyncを使って引数を渡して遷移先を指定する。遷移先ではArgumentに渡されたものをキャストして取り出す。といった感じです。

作る上での制約としては、必ず各シーンに単一のSceneBaseがヒエラルキーの頂上にある必要があります。こんな感じに。

image

うーん、随分と大きな制約であり不格好ですね……、この手の制約は実際のトコ、ないほうが望ましいです。別に、この手のヘンテコな制約をつけるのがアーキテクチャ、ではないです。自由なほうがよほど良いのです。とはいえしかし、どうにもならなかったので、そこは受け入れるしかなかったということで。この辺が今のところの手札でできる精一杯の形かなぁ。

NavigationService

では、肝心要のNavigationServiceの実装を見ましょう!

public static class NavigationService
{
    public static IObservable<Unit> NavigateAsync(string sceneName, object argument, LoadSceneMode mode = LoadSceneMode.Single)
    {        
        return Observable.FromCoroutine<Unit>(observer => HyperOptimizedFastAsyncOperationLoad(SceneManager.LoadSceneAsync(sceneName, mode), observer))
            .Do(_ =>
            {
                // 型ベースでたぐり寄せる。Find系は避けたいとはいえ、シーン遷移時に一発だけなのでコスト的には許容できるでしょう。
                var scenes = GameObject.FindObjectsOfType<SceneBase>(); 
                var loadedScene = scenes.Single(x => !x.IsLoaded); // 一個だけになってるはず #雑
 
                loadedScene.IsLoaded = true;
                loadedScene.Argument = argument; // PresenterBase.BeforeInitializeが走る前にセットする
            });
    }
 
    static IEnumerator HyperOptimizedFastAsyncOperationLoad(AsyncOperation operation, IObserver<Unit> observer)
    {
        if (!operation.isDone) yield return operation;
 
        observer.OnNext(Unit.Default);
        observer.OnCompleted();
    }
}

なんてことはなく、LoadSceneAsyncが完了した時点でヒエラルキーに新しいシーンがぶちまけられているので、それのBeforeInitializeが走る前にArgumentにセットしておいてやる、というだけの割と単純なものです。ポイントは、BeforeInitializeの走るタイミングはStartということです。順序的に、LaodSceneAsyncが完了した時点で、新しいシーンのGameObjectのAwakeは走っています。なので、Awakeの前にArgumentを渡すのは何をどうやっても不可能です。しかし、Startの前に割り込むことは可能です。そこでルールとして遷移先のシーンでの初期化はStart以降に限定し(PresenterBaseがその辺を抽象化しているので実装者が意識する必要はない)、NavigateAsyncでは可能な限り最速のタイミングでArgumentをセットしにいきます。その秘訣がHyperOptimizedFastAsyncOperationLoadというフザケタ名前のコルーチンです。

yield return null vs yield return AsyncOperation

別にHyperOptimizedFastAsyncOperationLoadの中身は、見たまんまの超絶単純な yield return AsyncOperation です。そして、それこそが秘訣なのです。何を言ってるかというと……

IEnumerator WaitLoadAsyncA(AsyncOperation operation)
{
    while (!operation.isDone)
    {
        yield return null;
        Debug.Log(operation.progress); // 読み込み状態のプログレス通知
    }
}
 
IEnumerator WaitLoadAsyncB(AsyncOperation operation)
{
    yield return operation;
}

両者の違い、分かるでしょうか? WaitLoadAsyncA のほうはプログレスを受け取るためにyield return nullでisDoneを監視するスタイル。WaitLoadAsyncBは直接待つスタイル。結果的に、どちらも待つことができます。プログレス通知は大事なので、WaitLoadAsyncAのようなスタイルを多用するほうが多いのではないかなー、と思います。WWWとか。が、しかし、両者には非常に大きな違いがあります。それは、完了時のタイミング。

image

わざわざ無駄に画像を作ってまで声を大にして言いたいんですが、直接AsyncOperationをyieldすれば、AwakeとStartの間に割り込めます。yield return nullでは普通に1フレ後になるのでStartまで完了しちゃってます。これは超絶デカい違いです、この微妙なコントロールが死ぬほど大事です。きっと役に立ちます。どこかで。ちなみに一番最初に説明したAsyncOperation.AsObservableという神メソッドはyield return nullで待ってます。クソですね。カスですね。ゴミですね。すみません……(これは次のUniRxのリリースではプログレス通知を使わない場合は直接yieldするように変更します、それまでの間は手動コルーチン作成で対応してください)

もう一つ、コルーチンの駆動を各SceneのStartCoroutineで行うと、LoadSceneMode.Single(遷移)の場合、遷移元シーンが破壊された瞬間に紐付いてるコルーチンも強制的に止まる(そしてDestroyは遷移先シーンのAwakeの前)ため、Argumentを渡すという行為は不可能です。が、UniRxのFromCoroutineで駆動させると、中立であるMainThreadDispatcherによるコルーチン駆動となるため、元のシーンが壊れるとかそういうのとは無関係にコルーチンが動き続けるため、その手の制限と付き合わなくても済みます。この辺は実際UniRx強い。

シーン表示を遅らせる

実は、今のとこ別にRx使う必要性はあんまありません、なくても全然出来るレベルです(まぁコルーチンが破壊される件は回避しにくいですが)。それではあんまりなので、もう一歩次のレベルに行きましょう。例えばシーン遷移時に、引数を元にネットワークからデータを読み取って、その間はNow Loadingで待つ。ダウンロードが完了したら表示する。こうした、なんとなく良くありそうな気がする話を、NavigationServiceで対応させてみましょう。

animation

この、あんまり良くわからない例、SceneAボタンを押すとヒエラルキーにSceneBが表示されているけれど画面上には表示されていない、実際にはネットワークからデータをダウンロードしていて、それが完了したら、その結果と共にSceneBが表示される。というものです。なるほど……?

まず、SceneBaseにPrepareAsyncメソッドを追加します。

public abstract class SceneBase : PresenterBase
{
    public object Argument { get; set; }
    public bool IsLoaded { get; set; }
 
    // このPrepareAsyncメソッドを新設する
    public virtual IObservable<Unit> PrepareAsync()
    {
        return Observable.Return(Unit.Default);
    }
 
    protected override void OnAwake()
    {
        this.InitializeAsObservable().Subscribe(_ => IsLoaded = true);
    }
}

PrepareAsyncが完了するまで表示を待機する、といった感じで、それをIObservableによって表明しています。これで遷移先のSceneBクラスを書き換えると

public class SceneB : SceneBase
{
    public WwwStringPresenter display; // インスペクターから貼り付けてUnityEngineによるデシリアライズ時にセットされる(Awake前)
 
    string wwwString = null;
 
    protected override IPresenter[] Children
    {
        get { return new[] { display }; } // Sceneにぶら下がってる子をここで指定する(コードで!原始的!)
    }
 
    // 呼ばれる順番はPrepareAsync -> BeforeInitialize -> Initialize
 
    public override IObservable<Unit> PrepareAsync()
    {
        var url = Argument as string; // 前のシーンからURL、例えば http://unity3d.com/ が送られて来るとする
 
        // ネットワーク通信が完了するまでこのシーンの表示を待機できる
        // (もし自分で試して効果が分かりにくかったら Observable.Timer(TimeSpan.FromSeconds(5)) とかに差し替えてください、それで5秒後表示になります)
        return ObservableWWW.Get(url)
            .Select(x => // 本当はForEachAsyncを使いたいのですがまだ未リリース。
            {
                wwwString = x; // 副作用さいこー
                return Unit.Default;
            });
    }
 
    protected override void BeforeInitialize()
    {
        // この時点で通信が完了してるので、小階層に渡す。
        display.PropagateArgument(wwwString); // PresenterBase.PropagateArgumentで伝搬するルール
    }
 
    protected override void Initialize()
    {
    }
}

変えたところは、PrepareAsyncでWWW通信を挟んでいるところ。これが完了するまではシーン全体の表示が始まらない(BeforeInitializeが呼ばれない)です。表示に関しては、この程度の超絶単純な例では直接SceneBにTextをぶら下げたほうがいいんですが、無駄に複雑にするために、ではなくてPropagateArgumentの例として、もう一個、下にUI要素をぶら下げてます。それがWwwStringPresenterで、

public class WwwStringPresenter : PresenterBase<string>
{
    public Text displayView;
 
    protected override IPresenter[] Children
    {
        get { return EmptyChildren; }
    }
 
    protected override void BeforeInitialize(string argument)
    {
    }
 
    // 親からPropagteArugmentで渡されてくる
    protected override void Initialize(string argument)
    {
        displayView.text = argument;
    }
}

こんな感じに、親(この場合だとSceneB)から値が伝搬されます、適切な順序で(ふつーにやってるとGameObjectの生成順序は不定なので、値の伝搬というのは単純なようで深く、やりようが色々あるテーマだったり)。さて、一見複雑というか実際、色々ゴテゴテしてきてアレな気配を醸しだしてきましたが、実際どんな状態なのかというと、こんな感じ。

image

この分かったような分からないような図で言いたいことは、値の流れです。シーン間はNavigateAsyncによりArgumentが引き渡され、シーン内ではPresenterBaseによって構築されたチェーンがPropagateArgumentにより、ヒエラルキーの上流から下流へ流れていきます。これにより、グローバルでの変数保持が不要になり、値の影響範囲が局所化されます。スコープが狭いというのは基本的にいいことです、見通しの良さに繋がりますから。分かっちゃいても実現は中々むつかしい、に対する小道具を色々揃えておくと動きやすい。

NavigateAsync最終形

おお、そうだ、PrepareAsyncに対応したNavigateAsyncのコードを出し忘れている!こんな形になりました。

public static class NavigationService
{
    public static IObservable<Unit> NavigateAsync(string sceneName, object argument, LoadSceneMode mode = LoadSceneMode.Single)
    {
        return Observable.FromCoroutine<Unit>(observer => HyperOptimizedFastAsyncOperationLoad(SceneManager.LoadSceneAsync(sceneName, mode), observer))
            .SelectMany(_ =>
            {
                var scenes = GameObject.FindObjectsOfType<SceneBase>();
                var loadedScene = scenes.Single(x => !x.IsLoaded);
 
                loadedScene.IsLoaded = true;
                loadedScene.Argument = argument;
 
                loadedScene.gameObject.SetActive(false); // 一旦非Activeにして止める
 
                return loadedScene.PrepareAsync() // PrepareAsyncが完了するまで待つ
                    .Do(__ =>
                    {
                        loadedScene.gameObject.SetActive(true); // Activeにして動かしはぢめる
                    });
            });
    }
 
    static IEnumerator HyperOptimizedFastAsyncOperationLoad(AsyncOperation operation, IObserver<Unit> observer)
    {
        if (!operation.isDone) yield return operation;
 
        observer.OnNext(Unit.Default);
        observer.OnCompleted();
    }
}

足したコードは、Argumentをセットしたら即座にSetActive(false)ですね。これで画面に非表示になるのは勿論、Startも抑制されます。そうしてStartが止まっている間にPrepareAsyncを呼んでやって、終わったら再度 SetActive(true) にする、ことによりStartが発生しだして、PresenterBaseの初期化機構が自動で上流→下流への起動を開始します。

まとめ

実際にはPrepareAsyncだけでは足りなくて、シーンから出る時、シーンから戻ってきた時、機能としてシーンをキャッシュしてやろうとか、遷移でパラメータ渡ってくる前提だと開発時にパラメータが足りなくてダルいので任意で差し込めるようにする/開発用デフォルト用意するとか、色々やれることはあります、し、やったほうがいいでしょふ。それらも全てUniRx上で、IObservableになっていることにより、表現がある程度は容易になるのではないかと思います。非同期を表現する入れ物、が必要だというのは至極当然の答えになるのですけれど、そこにUniRxが一定の答え、定番を提供できているんじゃないかなー、と思いますね!些か長い記事となってしまいましたが、これに限らず応用例の発想に繋がってくれれば何よりです。

Advent Calendarの次は、@Miyatinさんです!

UniRx vNext

ところで実はいまものすごい勢いで作り変えています!性能もかなり上が(って)るんですが、割と分かりやすく大きいのは、スタックトレースが物凄く見やすくなります。意味不明度が極まった複雑なスタックトレースはRx名物でデバッガビリティが最低最悪だったのですが、相当まともになってます。例えば、以下の様なふつーのチェーンのDebug.Logで表示されるスタックトレースは

var rxProp = new ReactiveProperty<int>();
rxProp
    .Where(x => x % 2 == 0)
    .Select(x => x)
    .Take(50)
    .Subscribe(x => Debug.Log(x));
 
rxProp.Value = 100;

Before

image

After

image

劇的!Unityのスタックトレースの表示形式に100%フォーカスして、読みやすさ第一にハックしたので、圧倒的な読みやすさだと思います。スタックトレース芸極めた。普通にWhere.Select.Take.Subscribeがそのまま表示されてますからね。勿論、メソッドコール数が減っているのは単純に性能にも寄与しています。ここまでやれば文句もないでせう。

そんなvNextの完成時期ですが、今までやるやる詐欺すぎたのですが、そろそろ実際本当に出します。来週ぐらいには本当に出します。これは意地でも仕上げます(想像通りだけれど作業量は多いわコーナーケースの想定が複雑すぎて頭が爆発しそうになるしで辛い……)。というわけでもうちょっとだけ待っててください。

同期(風)コードと対比させたUnity+UniRxで非同期を扱う場合のパターン集

UniRxのGitHubのStar数が500行きました!

image

今のところGitHub上でのUnity + C#でスター順の検索だと、世界5位です。おおー。更に上を狙いたいところですね。最近はちょっと更新が滞っていますが、ネタはあるのでより完成度を高めたい。(滞った理由は、PhotonWireとか色々他のところに手を出していたため……)

さて、本題。イベント結合に使う際はあてはまりませんが、Rx(UniRx)を非同期(長さ1のIOservableシーケンス)として扱う場合、それなりに癖があります。とはいえ、基本的には同期(或いはyield return)で書いていた際と、1:1で対比できるパターン化した形で概ね対応できるので、そのためのチートシートを考えてみました。コード例はC# 5.0のasync/awaitで出しますが、同期コード or IEnumeratorと同じように思ってもらえればいいです。例えば

public void Sync()
{
    /* before action */
    Method();
    /* after action */
}
 
public IEnumerator IEnumerator()
{
    /* before action */
    yield return StartCoroutine(Method());
    /* after action */
}
 
public async Task Task()
{
    /* before action */
    await MethodAsync();
    /* after action */
}

みたいな感じです、awaitに馴染みのない人も、なんとなくイメージしながら眺めてみてもらえると嬉しいです。

非同期汚染

コード例の前に非同期汚染、或いは非同期の伝搬について。まぁ、あんまし汚染という言い方は好きじゃないのですが、基本的に非同期、つまりTaskでもFutureでもPromiseでもIObservableでも、は、下層から上層まで伝搬していきます。メソッドが非同期であるなら戻り値はIObservableであり、そのIObservableを呼ぶメソッドもまた自然と非同期でなければならないので、IObservableになる、と。何故非同期の連鎖でなければならないのか。消費(Subscribe)してしまうと、その瞬間Fire and Forgetになってしまい、戻りを待ったりキャンセルしたりなどの別の操作が行えなくなってしまうからです。別にFire and Forgetしたければ、呼び元がそれを選択(Subscribeして放置)すればいいわけで、呼ばれる側が決定することではない。

もちろん、最終的にはどこかの層で消費(Subscribe)しなければならないので、そこで伝搬は止まるのですけれど、それは、基本的には上層であればあるほどよいということですね。どこが上層やねんって話はあるかもしれませんが、ユーザーインタラクションに近かったり、MonoBehaviourのイベント層に近かったり、あたりがそうですかねー。あとは、ごく一部でしか使わないんだ!という確固たる思いがあれば、早い段階でSubscribeして伝搬を止めるのも策ではあります、その辺はケースバイケースで。

非同期の伝搬に都合の良いメソッドが現状のUniRxには足りてません。実は!というわけで、次期バージョンではForEachAsyncというものを足したいのですが、それまでは以下のものをコピペって代用してください。挙動的にはシーケンスを消費して長さ1のIObservable[Unit]を返すもので、元シーケンスが非同期(長さ1)ならDoやSelectと、概ね一緒です。

// 次期バージョンに入るので、それまでの代用ということで。
// 元シーケンスが非同期なら .Select(x => { /* action(); */ return Unit.Default; }) とほぼ同様
namespace UniRx
{
    public static class UniRxExtensions
    {
        public static IObservable<Unit> ForEachAsync<T>(this IObservable<T> source, Action<T> onNext)
        {
            return Observable.Create<Unit>(observer =>
            {
                return source.Subscribe(x =>
                {
                    try
                    {
                        onNext(x);
                    }
                    catch (Exception ex)
                    {
                        observer.OnError(ex);
                        return;
                    }
                }, observer.OnError, () =>
                {
                    observer.OnNext(Unit.Default);
                    observer.OnCompleted();
                });
            });
        }
    }
}

また、副作用(外の変数への代入など)に関しては、あまり気にしないほうが吉です。いや、Rxのパイプラインに押し込めたほうが美しくはあるんですが、それがオブジェクトであるなら、副作用かけてフィールド変数を変えたり、ReactivePropertyに結果を伝えたりとかは、あって然りかな、と。考える際には「もしこれが同期コードだったらどうなのか」を意識したほうがいいかもしれません、同期コードで自然なら、別にRxでそれを行っても、構わないのです。とはいえ、以下に紹介するコードは全部、副作用大前提みたいな説明なので、それはそれで若干の狂気でもありますが、その辺は慣れてきてからでよいかと。

戻り値のない場合

public async Task Demo1_TaskAsync()
{
    /* before action */
    var x = await Task.Factory.StartNew(() => 100);
    /* after action */
}
 
public IObservable<Unit> Demo1_IOAsync()
{
    /* before action */
    return Observable.Start(() => 100)
        .ForEachAsync(_ =>
        {
            /* after action */
        });
}

メソッドに戻り値がない場合は、awaitの位置にForEachAsyncで、その中にactionを書く形になります。RxにおいてはIObservable[Unit]を戻り値のないことの表明として使います。

内部に複数の非同期がある場合

public async Task Demo2_TaskAsync()
{
    /* before action */
    var x = await Task.Factory.StartNew(() => 100);
    /* after action 1 */
    var y = await Task.Factory.StartNew(() => 200);
    /* after action 2 */
}
 
public IObservable<Unit> Demo2_IO_1Async()
{
    /* before action */
    return Observable.Start(() => 100)
        .SelectMany(x =>
        {
            /* after action 1 */
 
            return Observable.Start(() => 200);
        })
        .ForEachAsync(y =>
        {
            /* after action 2 */
        });
}

awaitの位置にSelectManyを置くことで繋げることができます。最後の消費だけForEachAsyncで。

パイプライン中に複数の値を伝搬したい場合

public IObservable<Unit> Demo2_IO_2Async()
{
    /* before action */
    return Observable.Start(() => 100)
        .SelectMany(x =>
        {
            /* after action 1 */
            return Observable.Start(() => 200);
        }, (x, y) => new { x, y }) // transport argument to next chain
        .ForEachAsync(o =>
        {
            /* after action 2 */
            // { o.x, o,y } 
        });
}
 
public IObservable<Unit> Demo2_IO_2_2Async()
{
    /* before action */
    return Observable.Start(() => 100)
        .SelectMany(x =>
        {
            /* after action 1 */
            var z = SyncMethod();
            return Observable.Start(() => 200).Select(y => new { x, y, z });
        })
        .ForEachAsync(o =>
        {
            /* after action 2 */
            // { o.x, o,y, o.z } 
        });
}

同期コードでは、そのスコープ中の全ての値が使えるわけですが、Rxのメソッドチェーンでは次のパイプラインに送り込める値は一つしかありません。というわけで、匿名型(もしくはUniRx.Tuple)を使って、次のパイプラインへは値をまとめて上げる必要があります。SelectManyには第二引数があり、それにより前の値と次の値をまとめることができます。また、SelectMany内部で作った値を送り込みたい場合は、戻り値のところでSelectを使ってスコープ内でキャプチャして返してあげればいいでしょう。(匿名型、Tupleともにclassなので、気になる場合はstructの入れ物を用意してもいいかもしれない、何か箱を作って運搬しなきゃいけないのは残念ながら仕様です)

非同期が連鎖する場合

public IObservable<Unit> Demo2_IO_2_MoreChainAsync()
{
    /* before action */
    return Observable.Start(() => 100)
        .SelectMany(x =>
        {
            /* after action 1 */
            return Observable.Start(() => 200);
        }, (x, y) => new { x, y })
        .SelectMany(o =>
        {
            /* after action 2 */
            return Observable.Start(() => 300);
        }, (o, z) => new { o.x, o.y, z }) // re-construct self
        .ForEachAsync(o =>
        {
            /* after action 3 */
            // { o.x, o,y, o.z } 
        });
}

SelectManyの連打になります。また、伝搬する値は自分で分解して付け直してあげる必要があります、これは面倒くさいですね!この辺はクエリ構文を使った場合、Transparent Identifierという仕組みで自動的にコンパイラが行うのですが(An Internal of LINQ to Objectsの35P、Rxでクエリ構文は結構頻繁にクエリ構文の範疇を逸脱するのと、副作用をパイプライン途中に書けないためあまり使い勝手は良くないので、面倒くさいながら手作業再構築を薦めます。

戻り値を返す場合

public async Task<int> Demo3_TaskAsync()
{
    /* before action */
    var x = await Task.Factory.StartNew(() => 100);
    /* after action */
    return x; // return value
}
 
public IObservable<int> Demo3_IOAsync()
{
    /* before action */
    return Observable.Start(() => 100)
        .Select(x =>
        {
            /* after action */
            return x; // return value
        });
}

ForEachAsyncではなく、Selectを使っていきましょう。戻り値の型が同一で副作用だけ起こしたいならDoでも構わないのですが、まぁどっちでもいいです。また、awaitが複数になる場合は、SelectManyになります。そのうえでSelectManyのままreturnするか、最後に再びSelect(もしくはDo)を使うかどうかは、状況次第、かな。

例外をキャッチ

public async Task Demo4_TaskAsync()
{
    /* before action */
    try
    {
        var x = await Task.Factory.StartNew(() => 100);
    }
    catch (Exception ex)
    {
        /* onerror action */
        throw;
    }
 
    /* after action */
}
 
public IObservable<Unit> Demo4_IOAsync()
{
    /* before action */
    return Observable.Start(() => 100)
        .Catch((Exception ex) =>
        {
            /* onerror action */
            return Observable.Throw<int>(ex);
        })
        .ForEachAsync(x =>
        {
            /* after action */
        });
}

これはCatchで賄えます。なお、Catchメソッドを使う際は、Catch<T>で例外の型を指定するよりも、ラムダ式の引数側で例外の型を書いたほうが書きやすいです(そうしたほうが型推論の関係上、ソースシーケンスの型を書かなくて済むため)。Catchの戻り値では再スローをObservable.Throw、握りつぶしをObservable.Return/Emptyで表現可能です。

Finally

public async Task Demo5_TaskAsync()
{
    /* before action(1) */
    try
    {
        var x = await Task.Factory.StartNew(() => 100);
    }
    finally
    {
        /* finally action(2) */
    }
 
    /* after action(3) */
}
 
// not equivant try-finally
public IObservable<Unit> Demo5_IO_PseudoAsync()
{
    /* before action(1) */
    return Observable.Start(() => 100)
        .Finally(() =>
        {
            /* finally action(3) */
        })
        .ForEachAsync(x =>
        {
            /* after action(2) */
        });
}
 
public IObservable<Unit> Demo5_IO_CorrectLightweightButIsNotDryAsync()
{
    /* before action(1) */
    return Observable.Start(() => 100)
        .Do(_ => { /* finally action(2) */}, _ => {/* same finally action(2) */})
        .ForEachAsync(x =>
        {
            /* after action(3) */
        });
}

Finallyに関しては、実は同じに扱える表現がありません!RxのFinallyはパイプラインの終了時の実行なので、実行順序がベタtry-finallyで書いた時と異なるんですよねえ。いちおう、DoでOnNextとOnErrorのところに同じコードを書くことでそれっぽい表現は可能ではありますが……。

並列処理

public async Task ParallelAsync()
{
    var a = Task.Factory.StartNew(() => 100);
    var b = Task.Factory.StartNew(() => 200);
    var c = Task.Factory.StartNew(() => 300);
 
    var xs = await Task.WhenAll(a, b, c);
    /* after action */
}
 
 
public IObservable<Unit> ParallelIO()
{
    var a = Observable.Start(() => 100);
    var b = Observable.Start(() => 200);
    var c = Observable.Start(() => 300);
 
    return Observable.WhenAll(a, b, c)
        .ForEachAsync(xs =>
        {
            /* after action */
        });
}

並列処理は非同期固有の実行ですが、WhenAllでドバッとまとめるというのが基本方針。

タイムアウト

public async Task TimeoutAsync(TimeSpan timeout)
{
    var task = Task.Factory.StartNew(() => 100);    
    var delay = Task.Delay(timeout);
    if (await Task.WhenAny(task, delay) == delay)
    {
        /* timeout action */
        throw new TimeoutException();
    }
    /* after action */
}
 
 
public IObservable<Unit> TimeoutIO(TimeSpan timeout)
{
    return Observable.Start(() => 100)
        .Timeout(timeout)
        .Catch((TimeoutException ex) =>
        {
            /* timeout action */
            return Observable.Throw<int>(ex);
        })
        .ForEachAsync(x =>
        {
            /* after action */
        });
}

タイマウトも非同期固有の処理。async/awaitの場合、特有のイディオムがあります。UniRxの場合はTimeoutだけでOK。特に例外時に処理するものもないなら、Catchは不要です。

IEnumeratorに戻す

public IObservable<Unit> Demo6_IE()
{
    /* before action(1) */
    return Observable.FromCoroutine(() => Demo6_IECore());
}
 
IEnumerator Demo6_IECore()
{
    // 戻り値の不要な場合
    yield return Observable.Start(() => 100).StartAsCoroutine();
 
    int ret;
    yield return Observable.Start(() => 100).StartAsCoroutine(x => ret = x);
}

SelectManyの連打が辛い場合、ふつーのコルーチンに戻して、更にIObservableでラップするという手段も取れます。まあ、この辺は複雑さ度合いで自由に!

だったらもはや最初から全部コルーチンでええやん!Rxでメソッドチェーン複雑だし見た目だけならコルーチン最強にスッキリじゃん!というのは正しい。正しいんですが、例外処理・戻り値・合成可能性・並列処理・マルチスレッド、などといった要素が欠落してるので、コルーチンはコルーチンで苦しいところが多いというか実際のところシンプルなケース以外では相当苦しいので、基本的にはRxのほうが有利です。

async/awaitは必要?

みたとーり、必要です。どう考えても。さすがにSelectManyの連打を同期コードほどスッキリと言い張るのは無理があるでしょう。とはいえまぁ、書いて書けないこともないので、今あるツールの中でベストを尽くすのまた良きかな、とは思いますねー。というわけで良き非同期生活を!UniRxでイベントを扱う際のパターン集は、またそのうちにでも!

第一回UniRx勉強会を開催しました+スライドまとめ

と、いうわけかでUniRx勉強会を開催しました。当日の模様はtogetterまとめで。登録が150人ほど、生憎の雨天でしたが130人以上来てくださってめっちゃ嬉しかったですね。慣れないというかはぢめての主催+司会でその辺アレだったのですが、会場をお貸し下さったgloopsさんの手厚い協力のお陰で、なんとか成立させることができ、ほんとうに感謝です。

私の発表資料は「History & Practices for UniRx UniRxの歴史、或いは開発(中)タイトルの用例と落とし穴の回避法」になります。

History & Practices for UniRx UniRxの歴史、或いは開発(中)タイトルの用例と落とし穴の回避法 from Yoshifumi Kawai

あまりUniRx固有、という感じでもなく、また凄い話、でもなんでもない地味な内容なのですけれど、ちょっとはまると嫌だなー、けどはまりがちなポイントを説明してみた、といった感。地味すぎてトリとしてはなんともいえない感じでしたね、うむむむ。ちなみにReal World UniRxというのは、Real World Haskell―実戦で学ぶ関数型言語プログラミングという本が名前的には元ネタです。Real World、現実世界で使われるUniRx。というわけで要約すれば事例求む、みたいな。

はじめてのUniRx

toRisouPさんの発表です。

はじめてのUniRx from torisoup

資料的価値が非常に高く、わかりやすい。めっちゃ読みこむと良いと思います、スゴクイイ!Cold/Hotとか大事なんですがむつかしいところですしねー。

若輩エンジニアから見たUniRxを利用したゲーム開発

gloopsの森永さんの発表です。

若輩エンジニアから見たUniRxを利用したゲーム開発 from Hirohito Morinaga

toRisouPさんのが中級者向けでしたので、こちらが初心者向けでしたね。UniRxがどういう風に自分の中で浸透というか理解が進んでいくか、というのがstep by stepで紹介されていて、伝わりやすいと思います。あと、全然紹介していなかったObservableTriggerまわりの応用が完璧に書かれていてすばら。

Interactive UI with UniRx

トライフォートの岩下さんのセッションです。

Interactive UI with UniRx from Yuto Iwashita

UniRxって基本的にスクリプティング領域の技術なので、とにかく地味!なのですが、このセッションは地味どころかDemo含め、めっちゃ伝わるし美しさ、手触りが伝わって凄かった。実際本日一番の感動でしたにゃ。

「ずいぶんとダサいライティングを使っているのね」〜UniRxを用いた物理ベースライティング制御〜

ユニティ・テクノロジーズ・ジャパンの名雪さんのLTです。

「ずいぶんとダサいライティングを使っているのね」〜UniRxを用いた物理ベースライティング制御〜 from Toru Nayuki

色々なものへのReactiveな入力/出力ができるんじゃもん!と言ってはいるし興味はかなりあるのだけれど、自分でやったことが全くない領域で、それが実際になされてる様を目にするとオオーッってなりました。

その他

そういえば、ブログに書いてなかったんですがちょっと前に「Observable Everywhere - Rxの原則とUniRxにみるデータソースの見つけ方」という発表をしていました。

Observable Everywhere - Rxの原則とUniRxにみるデータソースの見つけ方 from Yoshifumi Kawai

これ、自分的には結構良い内容だなー、と思っているので見たことないかたは是非目を通してもらえると。

まとめ

第一回、というわけなんですがかなり密度濃い内容になったのでは!?懇親会でも、自分の思っていたよりもずっと遥かに使い出している、注目している、という声をいただき嬉しかったですねー。もっとドンドン良くしていかなければ、と気が引き締まります。次回がいつになるかは完全不明(というか当分後かな?)ですが、やっていきたいなー、と思いましたです。

UniRxでの空呼び出し検出、或いはRoslynによるCode Aware Libraries時代の到来について

UniRx - Reactive Extensions for Unity用に、メソッド呼んだだけで何も処理してないIObservable<T>があったらWarningを出すAnalyzerを作ってみました。

AnalyzerはVisual Studio 2015からの機能です。というわけでVisual Studio 2015 RCが必要です。あとは、NuGetからAnalyzerが入れられるようになっているので

でOK。Unityのプロジェクトであっても問題なく使えます(ただしVSTUのcsproj自動生成でAnalyzerタグは吹っ飛ぶので、生成をフックして復元する必要はあります、フック方法の詳細はみんな大好き Boo.Lang を SATSUGAI する方法を参照のこと)。もし、他にこういうAnalyzerがあったら便利なのになー、とかってアイディアあったら気楽に言ってください!作りますので!

現在のうちの会社(グラニ)のプロジェクトはRxが土台から、ありとあらゆる全てで使われているので、ちょっとした呼び出しのつもりでやってたら何もおこらなくて(Susbcribe漏れ)クソが!となるシチュエーションが少なくなかったので、こういうAnalyzerが必需品だったのでした。

ようするにC# 5.0のTaskでawaitしてないと警告が出るのと同じ話なのですが、そういうのが言語組み込みキーワードでなくても自由に、(VS2015で動かせるなら)簡単にプロジェクト単位で追加出来る、というのがミソです。こういったライブラリとアナライザーの組み合わせは、Code Aware Librariesという言葉でまとめられます。.NET Compiler Platform (”Roslyn”): Analyzers and the Rise of Code-Aware Libraries。従来はライブラリのみの提供でしたが、そこにAnalyzerも組み合わせて、Best Practiceを一体化して伝えていくような世界観が広がっています。

例えば、私はLightNodeというWebAPIフレームワークを作っていますが、これは引数の型に幾つかの制約があります。また、メソッドのオーバーロードを許していなかったりします。それらは実行時のウォームアップのタイミングでフェイルファストとして気づかせるようにしていますが、それよりも前のタイミング、コードを書いている最中にリアルタイムで警告できれば、より良いでしょう。なので、Analyzerを同梱すれば、より良い形、より良いライブラリの有り様になります。

DiagnosticAnalyzerの作り方 Part2

以前にVS2015のRoslynでCode Analyzerを自作する(ついでにUnityコードも解析する)VS2015+RoslynによるCodeRefactoringProviderの作り方と活用法という記事を書きましたが、基本的にはそれらと同じです、アタリマエですが。↑の記事はCTPの頃のもので、若干インターフェイスが変わっちゃっていますが、少し修正するだけでほぼほぼ同じかな。

今回作ったのはCode Analyzerで、Fixは含めていないのでcsファイル一個だけで済んでいます。

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class HandleObservableAnalyzer : DiagnosticAnalyzer
{
    public const string DiagnosticId = "HandleObservable";
 
    internal const string Title = "IObservable<T> does not handled.";
    internal const string MessageFormat = "This call does not handle IObservable<T>.";
    internal const string Description = "IObservable<T> should be handled(assign, subscribe, chain operator).";
    internal const string Category = "Usage";
 
    internal static DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning, isEnabledByDefault: true, description: Description);
 
    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get { return ImmutableArray.Create(Rule); } }
 
    public override void Initialize(AnalysisContext context)
    {
        context.RegisterSyntaxNodeAction(AnalyzeMethodDeclaration, SyntaxKind.MethodDeclaration);
    }
 
    private static void AnalyzeMethodDeclaration(SyntaxNodeAnalysisContext context)
    {
        var invocationExpressions = context.Node
            .DescendantNodes(descendIntoChildren: x => !(x is InvocationExpressionSyntax))
            .OfType<InvocationExpressionSyntax>();
 
        foreach (var expr in invocationExpressions)
        {
            var type = context.SemanticModel.GetTypeInfo(expr).Type;
            // UniRx.IObservable? System.IObservable?
            if (new[] { type }.Concat(type.AllInterfaces).Any(x => x.Name == "IObservable"))
            {
                // Okay => x = M(), var x = M(), return M(), from x in M()
                if (expr.Parent.IsKind(SyntaxKind.SimpleAssignmentExpression)) continue;
                if (expr.Parent.IsKind(SyntaxKind.EqualsValueClause) && expr.Parent.Parent.IsKind(SyntaxKind.VariableDeclarator)) continue;
                if (expr.Parent.IsKind(SyntaxKind.ReturnStatement)) continue;
                if (expr.Parent.IsKind(SyntaxKind.FromClause)) continue;
 
                // Okay => M().M()
                if (expr.DescendantNodes().OfType<InvocationExpressionSyntax>().Any()) continue;
 
                // Report Warning
                var diagnostic = Diagnostic.Create(Rule, expr.GetLocation());
                context.ReportDiagnostic(diagnostic);
            }
        }
    }
}

戦略的には、メソッド呼び出し、つまりInvocationExpressionを拾いだして、そこからローカル変数代入/フィールド代入/return/LINQクエリ構文/メソッド呼び出しで使われていなければダメ扱いにする、という流れ。コード自体は行数も少なくて難しくはないのですけれど、戦略を決定するまでは割と悩みました。SyntaxTree自体も大量のメソッドがあり、SemanticModelも絡めると、色々な手段が取れそうでいて取れなさそうで、相当悩ましい。最終的にはかなり単純な手法に落ち着きましたが、直線距離で到達できるようになるまでには、かなり慣れが必要そうです。あと、最初作った時はクエリ構文のチェックを見落としてたりとか(さすがにこれを最初から気づくのは無理)、必要なケースを全て洗い出すのはそこそこ大変かな、といった感はあります。

DescendantNodesのdescendIntoChildrenという引数が中々面白くて、これは子孫ノードの探索を打ち切る条件を指定できます。これの何がいいって、例えばメソッド Observable.Range().Where().Select() があった場合、最上位のInvocationExpressionはObservable.Range().Where().Select()なのですが、その子孫に Observable.Range().Where() や Observable.Range() がいます。ふつーのDescendantNodesだとそれら全部を列挙してしまうんですが、今回は欲しいのは最上位だけなので、descendIntoChildrenで条件フィルタを足しています。

以前には紹介していない、ユニットテストのやり方も紹介しましょう。といっても、テンプレートに最初からTestプロジェクトと、便利クラス群が同梱されています。Analyzerだけの場合は基底クラスをDiagnosticVerifierに変えて……

namespace UniRxAnalyzer.Test
{
    [TestClass]
    public class HandleObservableAnalyzerTest : DiagnosticVerifier
    {
        protected override DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer()
        {
            return new UniRxAnalyzer.HandleObservableAnalyzer();
        }
 
        [TestMethod]
        public void UnHandle()
        {
            var source = @"
using System;
 
class Test
{
    IObservable<int> GetObservable() => null;
 
    void Hoge()
    {
        GetObservable();
    }
}";
            var expected = new DiagnosticResult
            {
                Id = UniRxAnalyzer.HandleObservableAnalyzer.DiagnosticId,
                Message = "This call does not handle IObservable<T>.",
                Severity = DiagnosticSeverity.Warning,
                Locations = new[]
                {
                    new DiagnosticResultLocation("Test0.cs", 10, 9)
                }
            };
 
            this.VerifyCSharpDiagnostic(source, expected);
        }
    }
}

ようするにVerifyCSharpDiagnosticにテスト用のC#コードと、期待するDiagnosticResultを渡すだけです、実に簡単。もしエラーじゃなくOKの場合はsourceだけをVerifyCSharpDiagnosticに渡せば、そういうことになります。

まとめ

Analyzer、かなりイイです。実際。とにかくとりあえず触ってみませう。現状リファレンスとかは特にないですが、まぁLINQ to XML辺りがわかっていればSyntaxVisualizerとIntelliSenseを頼りになんとか作り上げられるでしょう!メソッド名を見ながらカンを働かせましょう。大丈夫大丈夫。また、GitHubには既にお手本となるAnalyzerが出回っているので、それを参照にすればかなりいけます。代表的なところではNR6Pack, StyleCopAnalyzers, Code Crackerなどがあります。

では、よきRoslynライフを!

UniRx 4.8 - 軽量イベントフックとuGUI連携によるデータバインディング

UniRx(Reactive Extensions for Unity)のVer 4.8が昨日、AssetStoreにリリースされました。UniRxとはなにか、というと、巷で流行りのReactive Programming、の.NET実装のReactive Extensions、のUnity実装で、私が去年ぐらいからチマチマと作っています。実際のところ細かいリリースは何度も行っているんで(差分はGitHubのReleasesに書いてあります)、開発/アップデートはかなりアクティブな状態でした。その間に、Google PlayやiOSのAppStoreでもチラホラと使用しているタイトルがあったりと(ありがとうございます!!!)、案外存外しっかりRealWorldしていました。GitHubのStarも順調に伸びていて、まぁまぁメジャーになってきた気はします。

その間に、いくつか素晴らしいプレゼン資料も作っていただきました!@torisoupさんの未来のプログラミング技術をUnityで -UniRx-は、分かりやすく魅力を感じさせてくれる内容になっていて、とても素晴らしいです。読むべし読むべし。toRisouPさんはQiitaでも多くの記事を書いてくださっていて、(私がgdgd書くよりも)はるかに分かりやすくていいですね!

また、@Grabacr07さんのUniRx とか ReactiveProperty とかは、今回紹介するuGUI連携についての話が、分かりやすく綺麗に紹介されているので、こちらも必読です。必読。

UniRxの最初の発表は2014/04/19のUniRx - Reactive Extensions for Unityというところで、発端は非同期処理の解消、という一面からスタートしていたのですが、すぐにUnityの発する色々なイベント処理をRxで行おうという、本来の、でありつつも応用的なところが盛んに試されるようになったのは素晴らしいことだなぁ、と思っています。これはゲームプログラミングの持つ複雑さが、Reactive Programmingの使い道を無数に産むという、相性の良さがあるのかしらん。非同期だけじゃない、データバインドだけじゃないRealなReactive Programmingがここにあり、プログラミングを、C#の可能性を、パラダイムシフトを大いに楽しめる環境です。是非楽しんでください。もちろん、実用性もありますしね!

当然(?)フリーです。

ObservableTriggers

UniRx 4.8から、MonoBehaviourのイベントハンドリング手法をObservableTriggersという概念に全面移行しました。どういうことかというと、まず、ObservableMonoBehaviourは廃止です:) Obsoleteはつけていないし、動作はしますが、非推奨になりました。その代わりとなるのがObservableTriggersです。まず利用例を。

using UniRx;
using UniRx.Triggers; // この名前空間以下にTriggerは入ってるのでusingしときましょう
 
public class MyComponent : MonoBehaviour
{
    void Start()
    {
        // AddComponentでTriggerを付与する
        var trigger = this.gameObject.AddComponent<ObservableUpdateTrigger>();
 
        // すると*Event*AsObservableが使えるようになる
        trigger.UpdateAsObservable()
            .SampleFrame(30)
            .Subscribe(x => Debug.Log(x), () => Debug.Log("destroy"));
 
        // 3秒後に自殺:)
        GameObject.Destroy(this, 3f);
    }
}

Triggerは対象GameObjectがDestroyされると、OnCompletedを流してイベント発火を終了します。

ObservableMonoBehaviourを継承するのではなく、AddComponentで必要なイベントのためのTriggerを与えてください。そうすれば、そのイベントがRxで取り扱えるようになります。標準では ObservableAnimatorTrigger, ObservableCollision2DTrigger, ObservableCollisionTrigger, ObservableDestroyTrigger, ObservableEnableTrigger, ObservableFixedUpdateTrigger, ObservableUpdateTrigger, ObservableLastUpdateTrigger, ObservableMouseTrigger, ObservableTrigger2DTrigger, ObservableTriggerTrigger, ObservableVisibleTrigger, ObservableTransformChangedTrigger, ObservableRectTransformTrigger, ObservableCanvasGroupChangedTrigger, ObservableStateMachineTrigger, ObservableEventTrigger を用意してあります。「ほぼ」全部です。4.6から追加された新しいイベント(OnTransformChildrenChangedとか)も網羅しています。(とはいえ全部ではないので、足りなくて必要なものがあったら自分で追加するか、私にリクエストください、単純にあまり需要なさそうだと勝手に判断したものはオミットしちゃっているので……)

また、AddComponentが面倒くさい!ので、GameObject/Componentに対して、UniRx.Triggersをusingしている場合は、XxxAsObservableメソッドを直接拡張メソッドから呼べて、するとTriggerが自動付与されるようになっています。

using UniRx;
using UniRx.Triggers; // 必ずこのusingが必要です
 
public class DragAndDropOnce : MonoBehaviour
{
    void Start()
    {
        // OnMouseDownAsObservableが生えてる
        this.OnMouseDownAsObservable()
            .SelectMany(_ => this.UpdateAsObservable()) // UpdateAsObservableが生えてる
            .TakeUntil(this.OnMouseUpAsObservable()) // OnMouseUpAsObservableが生えてる
            .Select(_ => Input.mousePosition)
            .Subscribe(x => Debug.Log(x));
    }
}

なので、通常使う場合は、Triggerに関しては意識する必要はありません。(ObservableEventTrigger(uGUI用)とObservableStateMachineTrigger(Animation用)だけは自動付与がないので、これらの場合だけ自分で意識的に付与する必要があります)

ObservableMonoBehaviourは継承が必要だったり(基底クラスが強制される!)、baseメソッドの呼び出しが必須だったり、空イベントの呼び出しが必ず含まれるパフォーマンス低下などなど、決して使い勝手の良いものではありませんでした。というか使い勝手は最悪でした。なんで当初からObservableTriggerのようなやり方じゃなかったか、というと……、まぁ、単純に私のUnityへの理解不足です、すびばせん。ObservableTriggerは、Unityのコンポーネント指向を活かしつつ、Rxによってイベントを自然に外側で取り出せるようになっているので、圧倒的に便利な形になったのではないかなと思います。

uGUI

uGUIのイベントがUniRxでパーフェクトにハンドリングできます!この辺の話は前述のスライドUniRx とか ReactiveProperty とかに綺麗にまとまっているのですが、例えばボタンとかが

// インスペクタから貼っつけるとか
public Button MyButton;
 
// こんな感じで取る(onClick.AsObservable もしくは OnClickAsObservable)
MyButton.onClick.AsObservable().Subscribe(_ => Debug.Log("clicked"));

ほぅ……。普通だ。どうでも良さそうだ。と、いう具合にuGUIのEvent + AsObservableでイベントハンドリングができるようになっています。もう少し例を出すと

// ビューからのコントロールはインスペクタでペタペタ貼り付ける
public Toggle MyToggle;
public InputField MyInput;
public Text MyText;
public Slider MySlider;
 
// Startとかで宣言的にUIを記述していきましょう
void Start()
{
    // チェックボックスのオン/オフでボタンの有効/非有効が切り替わるようにします
    // OnValueChangedAsObservableは.onValueChanged.AsObservableのヘルパーで、単純に省略が楽という他に、
    // 初期値(最初のisOnの値)がSubscribe時に流れていきます
    // また、SubscribeToInteractableはUniRxのヘルパーで、 x => .interactable = x を省略できます
    MyToggle.OnValueChangedAsObservable().SubscribeToInteractable(MyButton);
 
    // 入力文字は1秒後にテキストラベルに反映されます
    MyInput.OnValueChangeAsObservable()
        .Where(x => x != null)
        .Delay(TimeSpan.FromSeconds(1))
        .SubscribeToText(MyText); // SubscribeToTextを使うと簡単に紐付けできます
 
    // SubscribeToTextの人間の読める形に変換したい場合用ヘルパ
    MySlider.OnValueChangedAsObservable()
        .SubscribeToText(MyText, x => Math.Round(x, 2).ToString());
}

こんな風になります。uGUIの標準コントロールに関しては直接EventAsObservableできるように拡張されてます。ともあれ、uGUIのイベントハンドリングはスクリプトで行いましょう。uGUI標準のAddHandlerなどはやりづらいですが、UniRxはそれを簡単に行える仕組みが用意してあります。uGUIのチュートリアルや解説本では、インスペクタのイベントの部分をクリックしてメソッドと紐付けてー、などとやるかもしれませんが、あのやり方は最低最悪なので忘れましょう。スクリプトレスでイベント設定できるとか幻想なんで、少なくともRxを使おうとしているようなプログラマなら、一切見なかったことにしましょう。100億パーセントどうでもいい次元の話なので無視しておきましょう。やりづらいだけです。

unityEvent.AsObservableのかわりに、全てのUnityコントロールにはUnityEventAsObservableが定義されています。ButtonのonClickの場合は違いはないのですが、一部の値が流れるものに関しては違いがあって、コントロールに直接生えているものは初期値が流れるようになっています。この初期値が流れる、という性質は非常に重要です。と、いうのも、今回のようにUIを宣言的に記述した場合、初期値が流れないと、初期値を設定して回らなければならなくて全体の構築が狂ってしまうからです。と、いうわけで、基本的にはコントロールに生えているAsObservableを使いましょう。

ReactiveProperty

UniRx 4.8からReactivePropertyという特別な型が用意されています(あとReactiveCollectionとReactiveDictionary)。これは何かというと、通知可能なプロパティ。なんのこっちゃ。うーん、イベントと値がセットになった型。うーん、なんのこっちゃ……。

// 変更通知付きなモデル
public class Enemy
{
    // HPは変更あったら通知して他のところでなんか変化を起こすよね?
    public ReactiveProperty<long> CurrentHp { get; private set; }
 
    // 死んだら通知起こすよね?
    public ReadOnlyReactiveProperty<bool> IsDead { get; private set; }
 
    public Enemy(int initialHp)
    {
        // 宣言的に記述していく。
        // ReactivePropertyはそれ自体がIObservable<T>なので、Rxでチェーン可能で、更にそれをReactivePropertyに変換も可能
        // 死んだかどうかというのはHPが0以下になったら、で表現できる         
        CurrentHp = new ReactiveProperty<long>(initialHp);
        IsDead = CurrentHp.Select(x => x <= 0).ToReadOnlyReactiveProperty();
    }
}
 
// こんなふうにして使う
// ボタンクリックしたらHPが99減ってくとする(実際はなんかCollision受けたら減るとか色々)
// ReactivePropertyの値は.Valueで取り出せる)
MyButton.OnClickAsObservable().Subscribe(_ => enemy.CurrentHp.Value -= 99);
 
// その変更を受けてUIに変更を戻す
enemy.CurrentHp.SubscribeToText(MyText); // とりあえず現在HPをTextに表示
 
// もし死んだらボタンクリックできないようにする
enemy.IsDead.Select(isDead => !isDead).SubscribeToInteractable(MyButton);

今まではイベント+普通の値で表現していたものが、プロパティ一個で表現できるようになります。また、イベント自体の取り扱いもRxなので合成可能になっていて、取り回しが向上します。というわけで、めちゃくちゃ便利。実際便利。通知が必要な値は片っ端からReactivePropertyにしましょう、それで幸せになれます!

更にReactivePropertyはInspectorで利便性が向上しています。

IntRxPropのところ、インスペクタに値を表示しているのですが、これの値をインスペクタで変更すると、紐付けていたイベント(.Subscribeしているもの)への通知も飛んでいきます。地味に捗る神機能。注意点としては、ジェネリックの型はインスペクタに表示できないという制限を引き継いでいるので、インスペクタに表示したいReactiveProeprtyは、専用のReactivePropertyを使いましょう。例えばIntReactivePropertyやBoolReactiveProperty、Vector2ReactivePropertyなどが標準では用意されています。EnumをReactiveProeprtyとして表示したい、というシチュエーションも多いと思います。その場合はSpecializedなReactivePropertyを定義していきましょう。例えば

// こんなEnumがあるとして
public enum Fruit
{
    Apple, Grape
}
 
// こういう特化したReactiveProeprtyを作ればOK
[Serializable]
public class FruitReactiveProperty : ReactiveProperty<Fruit>
{
    public FruitReactiveProperty()
    {
    }
 
    public FruitReactiveProperty(Fruit initialValue)
        :base(initialValue)
    {
    }
}
 
// また、InspectorDisplayDrawerにたいしてCustomPropertyDrawerを指定するとインスペクタでの表示が向上/イベント通知が可能になるので
// 特化ReactiveProeprtyの作成とワンセットで行いましょう
// ExtendInspectorDisplayDrawer自体は一個あればそれで大丈夫です
[UnityEditor.CustomPropertyDrawer(typeof(FruitReactiveProperty))]
[UnityEditor.CustomPropertyDrawer(typeof(YourSpecializedReactiveProperty2))] // 他、沢山ここにtypeofを追加していく
public class ExtendInspectorDisplayDrawer : InspectorDisplayDrawer
{
}

といった感じに拡張することで、より便利になっていきます。

MV(R)P

これらのUIの作り方を指して、Model-View-(Reactive)Presenterパターンというものを提唱します。

なぜMVPか、なんでMVVMではないか。まず、Unityはバインディングエンジンを持っていません。一般的にMVVMはViewとViewModelの間をバインディングエンジンが受け持ちます。なので、素の状態ではそもそもMVVMはできません。じゃあバインディングエンジンを作るか、となると、そんなレイヤーを挟むのは複雑になるしパフォーマンスも低下するし、デメリットがメリットを上回るバインディングエンジンを作るのは難しい。バインディングは誰かが動的レイヤーを引き受けなければならなくて(例えばName直書きなINotifyPropertyChangedであったり)、ピュアC#の世界とは相性が悪い。それをWPFではXAMLに押し付けているが、動的コード生成高速化の手段が取れないUnityでは、無理して実現する価値はない。

そんなわけで、MVVMはやらない。やらないとなると、バインディング機構が存在しない都合上、どこかで、だれかが、Vを知る必要がある(じゃなきゃViewのUpdateがかけれない)。というわけでVMは存在できず、Presenterを立てる。Model自体はPresenterにも依存しないし、Viewは知らない。ただしViewまで伝搬するため通知は可能でなければならない。それらをRxが繋ぎます。従来のMVPはステートの複雑化や伝搬に困難があったが、Observableはバインディングのようにシンプルに通知を行うことができるし、Viewへの適用もバインディングであるかのように綺麗に見せることができる。Rxを介すことによって、アプリケーションを作る上での問題が解消する。しかもレイヤー的にはないに等しく薄いので、一切のデメリットはない。

再度、コードと当てはめてみましょう。

// Presenter(Canvasのルートだったり、Prefabやパーツ分割単位のルート)
public class ReactivePresenter : MonoBehaviour
{
    // PresenterはViewのコンポーネントを知っている(さわれる)
    public Button MyButton;
    public Toggle MyToggle;
 
    // ModelからのState-Change-EventsはReactivePropertyによって伝搬される
    // Modelの変更は基本的に自身が上層に通知可能であり、それはReactiveProeprtyで表現される
    Enemy enemy = new Enemy(1000);
 
    void Start()
    {
        // Viewからのuser eventsはRxによって伝搬され、Modelにまでリアクティブに浸透していく
        MyButton.OnClickAsObservable().Subscribe(_ => enemy.CurrentHp.Value -= 99);
        MyToggle.OnValueChangedAsObservable().SubscribeToInteractable(MyButton);
 
        // Modelからの伝搬もまた、Presenterを介してRxによってViewのUpdateをかける
        enemy.CurrentHp.SubscribeToText(MyText);
        enemy.IsDead.Where(isDead => isDead == true)
            .Subscribe(_ =>
            {
                MyToggle.interactable = MyButton.interactable = false;
            });
    }
}

この場合、ViewとPresenterの紐付けはUnityのインスペクタでやります、ぴっ、ぴっ、ぴっってドラッグアンドドロップですねん。ふつーの(?)MVPだと、このViewをIViewとしてモックと差し替え可能にしたりもしたりしなかったりですが、そこまでやってもメリットゼロなんでそんなことはやらないでダイレクトにViewの実体とひもづける形でOK。

それぞれの伝搬ポイントにUniRxのメソッドやクラスが用意されているので、全てをシームレスに、Reactiveにつなぎ合わせることが可能です。UniRxならね。これの何が嬉しいかというと、見通しが良く、コード量が減ります。それがもう単純に嬉しい。また、イベントの関連付けはスクリプト側に寄っているので、インスペクタがカオティックにならずに済みます。かなりUnity(+Rx)の現実に沿った作り方なのではないかなー、と思うのですがどうでしょう?この辺は意見大募集中といったところです。

カスタムトリガーを作ろう

そんな風にアプリケーションを作っていくと、イベントはRx的に発動させるのが都合が良い、ということがわかってきます。実際そう。で、SubjectやReactivePropertyなどを駆使することによりModelをRx的に作っていくのは可能なのですが、ViewからのイベントをRx的に流すためにはどうすればいいのか。標準ではTriggerが用意されてますが、それだけじゃ足りない、例えばロングタップ作りたいとかジェスチャー作りたいとか……。という場合はTriggerを自作します。作り方は、ObservableTriggerBaseを継承して……

public class ObservableLongPointerDownTrigger : ObservableTriggerBase, IPointerDownHandler, IPointerUpHandler
{
    public float IntervalSecond = 1f;
 
    Subject<Unit> onLongPointerDown;
 
    float? raiseTime;
 
    void Update()
    {
        if (raiseTime != null && raiseTime <= Time.realtimeSinceStartup)
        {
            if (onLongPointerDown != null) onLongPointerDown.OnNext(Unit.Default);
            raiseTime = null;
        }
    }
 
    void IPointerDownHandler.OnPointerDown(PointerEventData eventData)
    {
        raiseTime = Time.realtimeSinceStartup + IntervalSecond;
    }
 
    void IPointerUpHandler.OnPointerUp(PointerEventData eventData)
    {
        raiseTime = null;
    }
 
    public IObservable<Unit> OnLongPointerDownAsObservable()
    {
        return onLongPointerDown ?? (onLongPointerDown = new Subject<Unit>());
    }
 
    protected override void RaiseOnCompletedOnDestroy()
    {
        if (onLongPointerDown != null)
        {
            onLongPointerDown.OnCompleted();
        }
    }
}

こんな感じ、これで他のTriggerと同じノリ、OnPointerDownAsObservableでタップを拾えるように、OnLongPointerDownAsObservableでロングタップを拾えるようになります。Subjectでイベント通知することと、RaiseOnCompletedOnDestroyのところでOnCompletedを発行するのが原則です。こういう形でイベントを拡張すると、よりスムーズにRxで全てが繋がっていきます!

ライフサイクル管理

で、全部がRxになると、イベントをSubscribeしたのをどこで解除すればいーんですかー、って話になってきたりこなかったりする。基本的にTrigger系は自身が死んだ時に終了するからいいんですが、それ意外のもの、例えばObservable.TimerやObservable.EveryUpdateは自動的に止まらないので、自分で登録解除する必要があります。そのためのヘルパーとして、IDisposable.AddToがUniRxには用意されています。また、CompositeDisposableがSubscriptionの管理に使えます。

// CompositeDisposableはList<IDisposable>のようなもので、複数のIDisposableが管理できます
CompositeDisposable disposables = new CompositeDisposable(); // これをfieldにおいておいて
 
void Start()
{
    Observable.EveryUpdate().Subscribe(x => Debug.Log(x)).AddTo(disposables); // AddToで詰める
}
 
void OnTriggerEnter(Collider other)
{
    // .Clear() => 中の全てのdisposableのDisposeが呼ばれて、Listが空になります
    // .Dispose() => 中の全てのdisposableのDisposeが呼ばれて、以降はAddされたら即対象をDisposeするようになります
    disposables.Clear();
}

よくあるシチュエーションとして、Destroyした瞬間に解除したい、というのがあると思います。その場合AddTo(gameObject/component)が使えます。

void Start()
{
    // 自分が消滅したらDispose
    Observable.IntervalFrame(30).Subscribe(x => Debug.Log(x)).AddTo(this);
}

DisposeじゃなくてOnCompletedを出して欲しい、という場合にはTakeWhile, TakeUntil, TakeUntilDestroy, TakeUntilDisable辺りが使えます。

Observable.IntervalFrame(30).TakeUntilDisable(this)
    .Subscribe(x => Debug.Log(x), () => Debug.Log("completed!"));

イベントを「繰り返す」場合に、Repeatが通常使われますが、実は危険です。源流がOnCompletedを発行すると無限ループ化するからです。ObservableTriggersが終了するとOnCompletedを発行するため、安易なRepeatの使用は無限ループ行きとなります。それを避けるには、RepeatUntilDestroy(gameObject/component), RepeatUntilDisable(gameObject/component), RepeatSafeが使えます。RepeatUntilDestroyとかは文字通りなんですが、RepeatSafeは連続してOnCompltedが発行された場合はRepeatを取りやめるという、無限ループ禁止機構のついたRepeatです。ベンリ。

最後に、ObserveEveryValueChangedを紹介します。これは、ラムダ式で指定した値を変更のあった時にだけ通知するという、つまり変更通知のない値を変更通知付きに変換するという魔法のような(実際ベンリ!)機能です(実際は毎フレーム監視してるんで、ポーリングによる擬似的なPull→Push変換)

// watch position change
this.transform.ObserveEveryValueChanged(x => x.position).Subscribe(x => Debug.Log(x));

これは監視対象がGameObjectの場合はDestroy時にOnCompletedを発行して監視を止めます。通常のC#クラス(POCO)の場合は、GCされた時に、同様にOnCompletedを発行して監視を止めるようになっています(内部的にはWeakReferenceを用いて実装されています)。ただのポーリングなので多用すぎるとアレですが、お手軽でベンリには違いないので適宜どうぞ。

パフォーマンス

パフォーマンスの話は一口で言うには結構難しいところです。まずいうと、RxとLINQを関連付けてLINQだからパフォーマンスがー、というのは微妙にあてはまりません。RxはPush型、最初のタイミングでパイプラインを構築し、それを(大抵の場合)かなり長い期間(最長でObjectが消滅するまで)購読する形になります。つまり、ライフサイクルが非常に長い。だから、パイプライン構築のためのオブジェクト(のGC)のコストというのは、そんなでもないと思ってもらっていいでしょふ。Updateの度に頻繁に数千構築/解体を繰り返すようなものではない、ということですねん。パイプラインに流れる値に関しては、その頻度と書きよう次第ですけれど。また、Rxのメソッドも軽いメソッドと重いメソッドがあるので、それ次第という面もあります。とはいえそこまで気にするほどではないかなー、と。

全体的にRxを適用すると、アプリケーションはPushベースで構築されることになるので、頻繁な問い合わせ処理(Pullベース)が消え、つまり更新駆動の最小限の差分処理だけが走るので、逆にパフォーマンスは上がる、という見方もできなくもないですが、まぁさすがにそれは都合の良すぎる捉え方でしょう:) ともあれ、そういったアプリケーション構築手法の変革もあるので、そこのところも含めて評価しなければなりません。

単純なコルーチンの代替、非同期通信処理の代替レベルでなら、実質ない、と言っても過言ではないところなので、それぐらいならばもうまるっきり気にせず、ですね。また、今まではObservableMonoBehaviourが不要な場合にも空イベントを回していて、それが若干の消費があったのですが、今回からは軽量なTriggerベースで必要なものにしかイベントを付与しないスタイルになったので、全体的にはかなり取り回しよくなってきたんじゃないかなー、と思います。

まだまだ全然パフォーマンスチューニングできる領域は沢山あるので、都度行っていくつもりです(分かりやすく効果の出るところでいえばWhere.Where.Whereチェーンは1個のWhereにできたり、かなり多用されるWhere.Selectチェーンも1個のWhereSelectチェーンにまとめあげられたり、などなど)

LINQ to GameObject

あと、これはUniRxとは関係ないのですがLINQ to GameObjectもアップデートしてます。

メインはLINQ風メソッドでtransformを自在に辿れるってところで、それはLINQ to GameObjectによるUnityでのLINQの活用を読んでいたたきたいのですが、もう一つの機能に、階層上の任意の位置にGameObjectをAddしたりMoveしたりするメソッドもあります。今回のアップデートで、これがuGUIのRectTransformに対応しました!uGUIはヒエラルキーの位置を表示情報としてかなり大事に扱うため、それのコントロールが容易になるLINQ to GameObjectは役立つはずです。

まとめ

今回のObservableTrigger、uGUI連携、そしてLifetime管理といった機能によって、より様々なところに導入しやすくなった、より使いやすくなったのではないでしょうか!

直近では4/16 18:30~の歌舞伎座.tech#7「Reactive Extensions」で「Observable Everywhere - UniRxによるUnityでのReactive Programming」と題して発表を行います。こちらはニコ生での放送もあるようなので、見るといいんじゃないかなー、ということで!

RxJava Night振り返り(Reactive Extensionsの歴史)

RxJava Nightというイベントで、Rxの歴史!というほど大仰なものでもないですが、誕生から今に至るまでをサラッと振り返った資料でLTしてきました。

The History of Reactive Extensions from Yoshifumi Kawai

普段は時間オーバー常習犯なんですが、今回はちゃんと時間を意識して収めましたよ!中身的にはまだまだアレもコレも詰めたい欲もあったりなかったりですが、まぁむしろこのぐらいが丁度良いのかもしれません。幸い、わりかし評判も良かったようで何よりです。

Rx across languages

改めて実感したのは、もうRxは.NETだけのものじゃない、ということです。むしろ他言語のほうがずっと盛り上がっているというのは実感するところで、ReactiveCocoa、そしてRxJava。こちらのほうが熱い。それはもう事実として。勿論いいことです。とはいえそうなると、もはや.NETのReactive Extensionsの存在すら知らない人も沢山いるというところなので、そこを少し知ってもらえたら何よりですね。会場のマック率が99%だったりC#erが2人ぐらいだったりと、ひぢょーにゲンジツを感じました!

もしこれ、RxJavaがRx***という名前付けないで別の名前だったら、それで普及して、完全にReactive Extensionsに言及されることはなくなっていたんだろうなあ、ということを思うと、名前が残って良かった良かった(笑)

.NETでRxがそんなに目立って使われないのはいくつか理由がなきにしもあらずなんですが、一番大きな理由は、今回の勉強会でも一番大きく言及されていた非同期関連においてはそんなに重宝しない、というとこかなあ、と思います。重宝しないというか、C# 5.0でasync/awaitが搭載されたので、そちらでやったほうがかなりスッキリ書けるという。

並列処理に関してはgihyoでのグラニがC#にこだわる理由 第1回 神獄のヴァルハラゲートの裏側をCTOが語り尽くす!の図の1の部分を見てもらいたいのですが、さっくり書けてますよね、と。コードは

var frontHPs = await field.OwnGuild.Members
    .Where(x => x.Position == Position.Front)
    .Select(async x => new
    {
        Name = await x.Name,
        CurrentHP = (await x.UserStatus).CurrentHP
    })
    .WhenAll();

とかね。これも含めてサーバーサイド全般での活用に関してはAWS Summit Tokyo 2014で発表した以下の資料をどうぞ。

AWS + Windows(C#)で構築する.NET最先端技術によるハイパフォーマンスウェブアプリケーション開発実践 from Yoshifumi Kawai

非同期系はそれとして、今回Androidのかたが多かったようにバインディングとかはどーなのか、というと、もともとC#にはView側がXAMLというバインディング前提のHTMLみたいなUIマークアップ言語があったので、Rxにフルに頼る必要がない、という事実はあります。なので、そこまで切羽詰まってない、みたいな。もちろん、組み合わせて使うというのは有り得るパターンで、GitHubでも使われている(というか作ってる人が中の人な)ReactiveUIや、私の作った(現在の機能向上は完全にokazukiさんに渡してます)ReactivePropertyといったライブラリもあります。それらのAndroid(+ Xamarin)への活用はamay077さんがXamarin.Forms と ReactiveProperty で快適MVVM生活といった記事も書かれていますし、色々ありますねというか、別に冷め切ってるわけでもなくて、やっぱRx熱いよ!ってのは全然あります!はい!

LINQ

C#erがRxを理解するにはLINQから入って考えるんですが、他言語の人はRxから入るんですよね。その辺が一番大きなギャップかもしれません。私的にはLINQから入って、IEnumerableとIObservableの関係性とか意識しながらのほうがスムースだったんですが(例えばIObservableでScanを考えるのは大変、このRxJava WikiのScanの図の意味不明さ!でも、IEnumerableでScanを考えるのは、まだ容易!)どーなんでしょふ。別にLINQじゃなくてStream APIとか、自分の言語のコレクション処理と少し付きあわせてみるといいかなあ、というのは本当に思っています。なぜ双対のソの字も出してないか。IEnumerableが前提じゃないから。そこの説明をする時間はない!(Erik Meijerの起こした会社の名前、Applied Dualityは勿論dualityから来てる)

Rxが流行るには?

懇親会で話したことなんですが、どーなんですかねえ、RxJava流行りますか!?個人的には勿論流行って欲しいんですが!言語関係なく共通のお話ができますし、悩みも活用も応用例も、言語が増えれば増えるほど盛り上がる、嬉しい、んですが、実際どうでしょう。まず、ラムダは必須。無名クラスで書くのは無理ですねえ。こういうの、IDEの自動生成でなんとかなるものとなんともならないものがあって、Rxぐらいラムダを使いまくるものは無名クラスで自動生成しまくると、生成後のブツの可読性が悪すぎて辛すぎます。AndroidではまだJava8対応していないようですし、いつするかもわからないということで、辛いですねえ。Groovyのほうがまだ可能性はありそうだけど、AndroidでGroovy、どうなんでしょう、それはそれでそれもまた流行るための壁が二段階増えてる感は否めない気もする。

学習コストは間違いなく大きい。うーん、Streamの時点でもそれなりに高いとは思うんですが、でもやってやれないこともないし、慣れてしまえば凄まじく便利でOKだと思います。ただ、Rxもそれと同じといえるかというとそうでもない。次元が1個増えたような感じなんですよね、シーケンス的な考え方に「時間」と「スレッド」の概念が混ざってくるので、慣れてても複雑に絡み合ったRxのチェーンを読み解くのは大変。二次元なら見えるけど四次元は辛いよね?的な。ただ、それはじゃあ普通に書いても複雑なステートになっているはずなので、Rxが悪いわけじゃあないといえばないんですがねえ。

Rxで非同期やるなら、下の層から上の層までIObservableで通す必要があると思っていて(そうでなければ途中の層でブロックしているということだ!)、ある種の非同期汚染みたいなのが発生します。これはC# 5.0のasync/awaitにもいえて、下から上までTask(Future/Promiseみたいなの)が貫く必要がある。そういった根本的な変化が生じるので、やるんならむしろ徹底的にやってしまったほうが大きな結果が得られるかな、と。

最後に全く関係なくそういえば懇親感で少し話したUnityでのLINQのAOT問題。Unity + iOSのAOTでの例外の発生パターンと対処法ではmonoからEnumerable.csを持ってきたらどーよ?と書いたんですが、OrderByは落ちます。で、うちの会社では(OrderBy以外にも)それなりに手を加えて調整しまくって、今現在はほとんど落ちない状態になっているんですが、とりあえずOrderByの改造点だけ。以下の様な変更をいれれば大丈夫です。

-abstract class OrderedEnumerable<TElement> : System.Linq.IOrderedEnumerable<TElement>
+public abstract class OrderedEnumerable<TElement> : IEnumerable<TElement>, IEnumerable
 
-abstract class SortContext<TElement> : IComparer<int>
+public abstract class SortContext<TElement> : IComparer<int>
 
-enum SortDirection
+public enum SortDirection

IOrderedEnumerableだけを露出させると危ないので、abstract classをpublicにしちゃいます。そうした調整のせいでSortContextとか本来はprivateにしてたいものも露出してっちゃって望ましくないんですが、まぁそこは背に腹は代えられないということで妥協しましょう。妥協は大事です。理想よりも現実が一番偉いんです。

LINQ to BigQuery - C#による型付きDSLとLINQPadによるDumpと可視化

と、いうものを作りました。BigQueryはGoogleのビッグデータサービスで、最近非常に脚光を浴びていて、何度もほってんとりやTwitterに上がってきたりしてますね。詳細はGoogle BigQuery の話とかGoogleの虎の子「BigQuery」をFluentdユーザーが使わない理由がなくなった理由あたりがいいかな、超でかいデータをGoogleパワーで数千台のサーバー並べてフルスキャンするから、超速くて最強ね、という話。で、実際凄い。超凄い。しかも嬉しいのが手間いらずなところで、最初Amazon RedShiftを検討して試していたのですが、列圧縮エンコードとか考えるのすっごく大変だし、容量やパワーもインスタンスタイプと睨めっこする必要がある。それがBigQueryだと容量は格安だから大量に格納できる、チューニング設定もなし、この手軽さ!おまけにウェブインターフェイスが中々優れていてクエリが見やすい。Query Referenceもしっかり書かれてて非常に分かりやすい。もう非の打ち所なし!

触ってすぐに気に入った、んですが、C#ドライバがプリミティブすぎてデシリアライズすらしてくれないので、何か作る必要がある。せっかく作るならSQLっぽいクエリ言語なのでLINQだろう、と。それとIQueryableは幻想だと思っていたので、じゃあ代替を作るならどうするのか、を現実的に示したくて、ちょうど格好の題材が出現!ということで、LINQで書けるようなライブラリを作りました。

ダウンロードは例によってNuGetからできます。今年はそこそこ大きめのライブラリを作ってきていますが、LINQ to BigQueryは特に初回にしては大きめで割と充実、非常に気合入ってます!是非使ってみてねー。GitHubのReadMe.mdはこのブログ記事で力尽きたので適当です、あとでちゃんと書く……。

簡単なDEMO

BigQueryの良い所にサンプルデータが豊富というところがあります、というわけでGitHubのデータを扱って色々集計してみましょう。データは[publicdata:samples.github_timeline]を使ってもいいのですが、それは2011年時点のスナップショットでちょっとツマラナイ。GitHub Archiveから公開データを引っ張ってくれば、現時点での最新の、今ついさっきのリアルタイムの情報が扱えて非常に素敵(あとBigQueryはこういうpublicなDataSetが幾つかあるのが本当に最高に熱い)。ひっぱてくるやり方は書いてありますが(超簡単)、テーブル名は[githubarchive:github.timeline]です。

まずは単純なクエリということで、プログラミング言語だけでグループ化して個数を表示してみます。github.timelineは、例えばPushしたとかBranch作ったとか、雑多な情報が大量に入っているので、別にリポジトリ数のランキングではなくて、どちらかといえばアクティビティのランキング、ぐらいに捉えてもらえれば良さそうです。とりあえずトップ5で。

この例では記述と表示はLINQPadで行っています。LINQPadは非常に優れていて、C#コードが入力補完付きでサクッと書けるほか、実行結果をDumpして色々表示させることも可能です。DumpChartはLINQ to BigQueryのために独自に作ったDumpなのですが、それにより結果のグラフ化がXとYを指定するだけのたった一行

.DumpChart(x => x.repository_language, x => x.count)

だけで出来てしまう優れものです。描画は.NET標準のチャートライブラリを使っているため、棒グラフの他にも円グラフでも折れ線グラフでも、SeriesChartTypeにある35個の表示形式が選べます。見たとおり、Tooltip表示もあるので個数が大量にあっても全然確認できるといった、チャートに求められる基本的な機能は満たしているので、ちょっとしたサクッと書いて確認する用途ならば上等でしょう。

(DumpChartやQuery.GetContextのコードはこの記事の末尾にコード貼り付けてあるので、それで使ってください)

Resultsタブのほうを開けば、クエリ結果の詳細が見れます。

クエリ文字列はBigQueryの性質上、色々なところで使うはずです。そうした他所で使える可搬性のために、生成結果を人間の読める綺麗なものにする事にこだわりました(TypeScript的な)。純粋なクエリビルダとして使う(ちなみにToString()すればRunしなくてもクエリを取り出せます)ことも十分可能でしょう。Rowsに関しては切り離してグリッド表示も可能で、そうすれば簡単なソートやCSVへの書き出しといった、データベース用IDEに求められる基本的な機能も満たしています。

TotalBytesProcessedが読みづらかったのでひゅーまんりーだぶるな形に直してあるのも用意してあるところが優しさ(普通に自分が使ってて困ったので足しただけですが)。

BigQueryはウェブインターフェイスが非常に優れている、これは正直感動ポイントでした。いやぁ、RedShift、データベース管理用のIDEがろくすっぽなくて(PostgreSQL互換といいつつ違う部分で引っかかって動かないものが非常に多い)どうしたもんか、と苦労してたんですが、BigQueryはそもそも標準ウェブインターフェイスが超使いやすい。スキーマも見やすいしクエリも書きやすい。まさに神。

てわけでウェブインターフェイスには割と満足してるんですが、表示件数をドバッと表示したかったり、グラフ化もサクッとしたいし(何気にGoogle SpreadSheet連携は面倒くさい!)、日頃からデータベースもSQL Server Management StudioやHeidi SQLといったデスクトップツールを使って操作するWindows野郎としては、デスクトップで使えるIDE欲しいですね、と。それに分析やる以上、結構複雑なクエリも書くわけで、そういう時に型が欲しいなーとは思ってしまったり。LINQ to BigQueryはAlt BigQuery Query、Better BigQuery Queryとして、ただたんにC#で書けます以上のものを追求しました。そして、LINQPadとの組み合わせは、現存するBigQuery用のIDEとして最も良いはずです(そもそもBigQuery用のIDEは標準ウェブインターフェイス以外にあるのかどうか説もあるけれど)。日常使い、カジュアルな分析にも欠かせない代物となることでしょう。

Why LINQ?

LINQ to BigQueryで書く場合の良い点。一つは型が効いているので、間違っていたらコンパイルエラーで(Visual Studioで書けばリアルタイムにエラー通知で)弾かれること。別にカラム名の名前間違いなどといったことだけじゃなくて、文字列であったりタイムスタンプであったりといった型も厳密に見えているので、型の合わない関数を書いてしまうといったミスもなくせます。例えばDate and time functionsの引数が文字列なのかタイムスタンプなのかUNIX秒なのか、そして戻り値もまた文字列なのかタイムスタンプなのかUNIX秒なのか、ってのは全く覚えてられないんですが、そんな苦痛とはオサラバです。

github_timelineのカラム数はなんと200個。さすがに覚えてられませんし、それの型だってあやふやってものです(例えばboolであって欲しいフォークされたリポジトリなのかを判定するrepository_forkというカラムには”false”といったような文字列でやってくるんですぜ!?)。

全ての関数はBqFuncの下にぶら下がっていて、引数と戻り値、それにドキュメント付きです。これなら覚えてなくても大丈夫!ちなみに、ということはクエリ中の全ての関数呼び出しにBqFunc.がついてきて見た目がウザいという問題があるのですが、それはC# 6.0のusing staticを使えば解決します。

// C# 6.0 Using Static
using BigQuery.Linq.BqFunc;

楽しみに待ちましょう(C# 6.0は多分2015年には登場するんじゃないかな?)。

LINQ to BigQueryはO/Rマッパーじゃありません。いや、もちろんクエリの構築やC#オブジェクトへのマッピングは行いますが、リレーションの管理はしません。かわりに、書いたクエリがほとんどそのままの見た目のクエリ文字列になります。なので意図しない酷いクエリが発行されてるぞー、というありがちななことは起きません。そして、LINQ to BigQueryで99%のクエリが記述できます、LINQで書けないから文字列でやらなきゃー、というシチュエーションはほぼほぼ起きません。LINQとクエリ文字列を1:1に、あえてほぼ直訳調にしているのはそのためです。

また、順序を強く規制してあります、無効なクエリ順序での記述(例えばGroupBy使わずにHaving書くとかLimitの後にWhere書いてしまうとか)やSelectなしの実行はコンパイルエラーで、そもそも書けないようにしています。

左はWhereの後のメソッド、これが全部でSelectとOrderByとWhere(ANDで連結される)しか使えない。右はSelect後で、GroupBy(奇妙に思えるかもしれませんが、GroupByの中でSelectの型が使えることを考えるとこの順序が適正)やLimit、そしてRunなどの実行系のメソッドが使えるようになっています。

これらにより、LINQ to BigQueryで書いたクエリは一発で実行可能なことが期待できるものが作れます(文字列で書くと、カラムの参照周りとかで案外つまづいてエラりやすい)。さすがにExpressionの中身は検査できないんですが、概ね大丈夫で、”守られてる感”はあるかと思います。ちなみにこんな順序で書けます。

From(+TableDecorate) -> Join -> Where -| -> OrderBy(ThenBy) -> Select ->                     | -> Limit -> IgnoreCase
                                       | -> Select | -> GroupBy -> Having -> OrderBy(ThenBy) | -> IgnoreCase
                                                   | -> OrderBy(ThenBy) ->                   |

そういうの実現するためにLINQ to BigQueryはIQueryableじゃないんですが、そのことはこの長いブログ記事の後ろのほうでたっぷりポエム書いてるので読んでね!あと、こんな割とザルな構成でもしっかり機能しているように見えるのは、BigQueryのSQLがかなりシンプルなSQLだから。標準SQLにできることは、あんま出来ないんですね。で、私はそこが気に入ってます。好きです、BigQueryのSQL。別に標準SQLにがっつし寄せる必要はあんまないんじゃないかなー、SQL自体は複雑怪奇に近いですから、あんまり良くはない。とはいえ、ある程度の語彙は共用されていたほうが親しめるので、そういったバランス的にもBigQueryのSQLはいい塩梅。

最後に、Table DecoratorsTable wildcard functionsが圧倒的に記述しやすいのも利点です。

// Table Decorators - WithRange(relative or absolute), WithSnapshot 
 
// FROM [githubarchive:github.timeline@-900000-]
.From<github_timeline>().WithRange(TimeSpan.FromMinutes(15))
 
// FROM [githubarchive:github.timeline@1411398000000000]
.From<github_timeline>().WithSnapshot(DateTimeOffset.Parse("2014-09-23"))
 
// Table wildcard functions - FromDateRange, FromDateRangeStrict, FromTableQuery
 
// FROM (TABLE_DATE_RANGE([mydata], TIMESTAMP('2013-11-10'), TIMESTAMP('2013-12-01')))
.FromDateRange<mydata>("mydata", DateTimeOffset.Parse("2013-11-10"), DateTimeOffset.Parse("2013-12-1"))
 
// FROM (TABLE_QUERY([mydata], "([table_id] CONTAINS 'oo' AND (LENGTH([table_id]) >= 4))"))
.FromTableQuery<mydata>("mydata", x => x.table_id.Contains("oo") && BqFunc.Length(x.table_id) >= 4)
 
// FROM (TABLE_QUERY([mydata], "REGEXP_MATCH([table_id], r'^boo[\d]{3,5}')"))
.FromTableQuery<mydata>("mydata", x => BqFunc.RegexpMatch(x.table_id, "^boo[\\d]{3,5}"))

Table decoratorは、例えばログ系を突っ込んでる場合は障害対応や監視で、直近1時間から引き出したいとか普通にあるはずで、そういう場合に走査範囲を簡単に制御できる非常に有益な機能です。が、しかし、普通に書くとUNIXタイムスタンプで記述しろということで、ちょっとムリゲーです。それがC#のTimeSpanやDateTime、DateTimeOffsetが使えるので比較にならないほど書きやすい。

FromTableQueryも文字列指定だったりtable_idってどこから来てるんだよ!?という感じであんま書きやすくないのですが、LINQ to BigQueryでは型付けされたメタテーブル情報が渡ってくるので超書きやすい。(ところでCONTAINSだけ、BqFuncじゃなくてstring.Containsが使えます、これはCONTAINSの見た目がこれだけ関数じゃないので、ちょっと特別扱いしてあげました、他の関数は全部BqFuncのみです)

Table DecoratorsとTable wildcard functionsは非常に有益なので、テーブル名の設計にも強く影響を及ぼします。これらが有効に使える設計である必要があります。TABLE_DATE_RANGEのために(垂直分割するなら)末尾はYYYYMMDDである必要があるし、Range decoratorsを有効に使うためには極力、水平シャーディングは避けたほうが良いでしょう。そこのところを無視して、ただ単にシャーディング、シャーディングって言ってたりするのは、ちょっと、ないなー。

複雑なDEMO

ひと通り紹介は終わったので、より複雑なクエリを一つ。同じく最新のGitHubのデータを扱って、一ヶ月毎に、新しく作られたリポジトリを言語毎で集計して表示してみます。まずはグラフ化の結果から。

LINQPadではちゃんと多重グラフもメソッド一発で書けるようにしてます。コードは後で載せるとしてグラフの説明ですが、縦がパーセント、横が日付、それぞれの折れ線グラフが言語。一番上はJavaScriptで今月は43000件の新規リポジトリが立ち上がっていて全体の19%を占めてるようです。2位はJava、3位はCSS、そしてRuby、Python、PHPと続いて、この辺りまでが上位組ですね。C#はその後のC++、Cと来た次の9位で9251件・全体の4%でした。

コードは、ちょっと長いよ!

Query.GetContext()
    .From<github_timeline>()
    .Where(x => x.repository_language != null && x.repository_fork == "false")
    .Select(x => new
    {
        x.repository_url,
        x.repository_created_at,
        language = BqFunc.LastValue(x, y => y.repository_language)
            .PartitionBy(y => y.repository_url)
            .OrderBy(y => y.created_at)
            .Value
    })
    .Into()
    .Select(x => new
    {
        x.language,
        yyyymm = BqFunc.StrftimeUtcUsec(BqFunc.ParseUtcUsec(x.repository_created_at), "%Y-%m"),
        count = BqFunc.CountDistinct(x.repository_url)
    })
    .GroupBy(x => new { x.language, x.yyyymm })
    .Having(x => BqFunc.GreaterThanEqual(x.yyyymm, "2010-01"))
    .Into()
    .Select(x => new
    {
        x.language,
        x.yyyymm,
        x.count,
        ratio = BqFunc.RatioToReport(x, y => y.count)
            .PartitionBy(y => y.yyyymm)
            .OrderBy(y => y.count)
            .Value
    })
    .Into()
    .Select(x => new
    {
        x.language,
        x.count,
        x.yyyymm,
        percentage = BqFunc.Round(x.ratio * 100, 2)
    })
    .OrderBy(x => x.yyyymm)
    .ThenByDescending(x => x.percentage)
    .Run()  // ↑BigQuery
    .Dump() // ↓LINQ to Objects(and LINQPad)
    .Rows
    .GroupBy(x => x.language)
    .DumpGroupChart(x => x.yyyymm, x => x.percentage);

規模感は全体で153GBで行数が2億5千万行ぐらいだけど、この程度は10秒ちょいで返してきますね、速い速い(多分)。

メソッドチェーンがやたら続いているのですが、実際のところこれはサブクエリで入れ子になってます。随所に挟まれてるIntoメソッドで入れ子を平らにしてます。入れ子の形で書くこともできるんですが、フラットのほうが直感的で圧倒的に書きやすいく、(慣れれば)読みやすくもあります。こういう書き方が出来るのもLINQ to BigQueryの大きなメリットだとは、書いてればすぐに実感できます。

(BqFunc.GreaterThanEqualが奇妙に思えるかもしれないのですが、これは文字列だけの特例です。数値やタイムスタンプの場合は記号で書けるようにしてあるのですが、文字列はそもそもC#自体に演算子オーバーロードが定義されていないのでコンパイラに弾かれる、けどBigQuery的には書きたい時がある、というのの苦肉の策でLessThan(Equal)/GreaterThan(Equal)を用意してあります)

チャート化はGroupBy.DumpGroupChartを叩くだけなんですが、ちょっと面白いのは、ここのGroupByはLINQ to Objects(C#で結果を受け取った後にインメモリで処理)のGroupByなんですよね。

.Run()  // ↑BigQuery
.Dump() // ↓LINQ to Objects(and LINQPad)
.Rows
.GroupBy(x => x.language)
.DumpGroupChart(x => x.yyyymm, x => x.percentage);

二次元のクエリ結果を、シームレスに三次元に起こし直せるってのもLINQの面白いところだし、強いところです。モノによっては無理にSQLでこねくり回さなくてもインメモリに持ってきてから弄ればいいじゃない?という手が簡単に打てるのが嬉しい(もちろん全件持ってこれるわけがないのでBigQuery側で処理できるものは基本処理しておくのは前提として、ね)。

例えば、実のところこれの結果は、言語-日付という軸だと歯抜けがあって、全ての月に1つは言語がないと、チャートが揃いません。グラフの見た目の都合上、今回は2010-01以降にHAVINGしてありますが、その後に新しく登場した言語(例えばSwift)なんかはうまく表示できません。まぁ主要言語は大丈夫なので今回スルーしてますが、厳密にやるため、その辺の処理を、しかしSQLのままやるのは存外面倒くさい。でも、こういう処理、C#でインメモリでやる分には簡単なんですよね。なんで、一旦ローカルコンピューター側に持ってきてから、少しだけC#で処理書くか、みたいなのがカジュアルにできちゃうのもLINQ to BigQuery + LINQPadのちょっと良いところ。

さて、実際に吐かれるSQLは以下。

SELECT
  [LANGUAGE],
  [count],
  [yyyymm],
  ROUND(([ratio] * 100), 2) AS [percentage]
FROM
(
  SELECT
    [LANGUAGE],
    [yyyymm],
    [count],
    RATIO_TO_REPORT([count]) OVER (PARTITION BY [yyyymm] ORDER BY [count]) AS [ratio]
  FROM
  (
    SELECT
      [LANGUAGE],
      STRFTIME_UTC_USEC(PARSE_UTC_USEC([repository_created_at]), '%Y-%m') AS [yyyymm],
      COUNT(DISTINCT [repository_url]) AS [count]
    FROM
    (
      SELECT
        [repository_url],
        [repository_created_at],
        LAST_VALUE([repository_language]) OVER (PARTITION BY [repository_url] ORDER BY [created_at]) AS [LANGUAGE]
      FROM
        [githubarchive:github.timeline]
      WHERE
        (([repository_language] IS NOT NULL) AND ([repository_fork] = 'false'))
    )
    GROUP BY
      [LANGUAGE],
      [yyyymm]
    HAVING
      [yyyymm] >= '2010-01'
  )
)
ORDER BY
  [yyyymm], [percentage] DESC

まず、ちゃんと読めるクエリを吐いてくれるでしょ?というのと、これぐらいになってくると手書きだと結構しんどいです、少なくとも私は。ウィンドウ関数もあんま手で書きたくないし、日付の処理の連鎖は型が欲しい。それと、サブクエリ使うとプロパティを外側に伝搬していく必要がありますが、それがLINQだと入力補完が効くのでとっても楽。Into()ですぐにサブクエリ化できるので、すごくカジュアルに、とりあえず困ったらサブクエリ、とぶん投げることが可能でめちゃくちゃ捗る。大抵のことはとりあえずサブクエリにして書くと解決しますからね!処理効率とかはどうせBigQueryなので何とかしてくれるだろうから、ふつーのMySQLとかで書く時のように気遣わなくていいので、めっちゃカジュアルに使っちゃう。

ところでどうでもいい余談ですが、LAST_VALUEウィンドウ関数はリファレンスに載ってません。他にも載ってない関数は幾つかあったりして(追加された時にブログでチラッと告知はされてるようなんですけどね、リファレンスにもちゃんと書いてくださいよ……)。LINQ to BigQueryならそういうアンドキュメントな関数もちゃんと網羅したんでひじょーにお薦めです!

Generate Schema

型付けされてるのがイイのは分かったけれど、それの定義が面倒なのよねー。と、そこで耳寄りな情報。まず、全部のテーブルのちょっとした情報(table_idとかサイズとか)はGetAllTableInfoという便利メソッドで取ってこれるようにしてます(実際便利!)。で、そこから更にテーブルスキーマが取り出せるようになってます。更にそこからオマケでC#コードをstringで吐き出せるようになってます。

var context = new BigQueryContext(/* BigqueryService, projectId */);
// Get All tableinfo(table_id, creation_time, row_count, size_bytes, etc...)
var tableInfos = context.GetAllTableInfo("mydataset");
// ToString - Human readable info
tableInfos.Select(x => x.ToString()).Dump();
 
// Get TableSchema
var schema = tableInfos[0].GetTableSchema(context.BigQueryService);
 
// Build C# class definition
schema.BuildCSharpClass().Dump();

まあ、そんなに洗練されたソリューションじゃないんでアレですが、一時凌ぎには良いでしょふ。publicdataとか自分のプロジェクト下にないものは直接MetaTableクラスを作ってからスキーマ取れるようになってます。

new MetaTable("publicdata", "samples", "github_timeline")
	.GetTableSchema(Query.GetContext().BigQueryService)
	.BuildCSharpClass();
 
// =>
 
[TableName("[publicdata:samples.github_timeline]")]
public class github_timeline
{
    public string repository_url { get; set; }
    public bool? repository_has_downloads { get; set; }
    public string repository_created_at { get; set; }
    public bool? repository_has_issues { get; set; }
    // snip...(200 lines)
	public string url { get; set; }
	public string type { get; set; }
}

TableName属性がついたクラスはFrom句でテーブル名を指定しなくてもそこから読み取る、っていう風になってます(今までのコードでテーブル名を指定してなかったのはそのお陰)

リアルタイムストリーミングクエリ

Streaming Insertによりリアルタイムにログを送りつけてリアルタイムに表示することが可能に!というのがBigQuery超イカス。今までうちの会社は監視系のログはSumo Logicを使っていたのですが、もう全部BigQueryでいいね、といった状態になりました、さようなら、Sumo……。

で、リアルタイムなんですが、リアルタイム度によりけりですが、1分ぐらいの遅延やそれ以上のウィンドウを取るクエリならBigQueryで十分賄えますね。Range decoratorsが最高に使えるので、定期的にそれで叩いてやればいい。そして最近流行りのReactive ProgrammingがC#でも使えるというかむしろC#はReactive Programmingの第一人者みたいなもんなので、Reactiveに書きましょふ。Rxの説明は……しないよ?

// まぁgithub.timelineがリアルタイムじゃないからコレに関しては意味ないヨ、ただの例
 
// [githubarchive:github.timeline@1411511274158000-1411511574167000]
// [githubarchive:github.timeline@1411511574167000-1411511874174000]
// [githubarchive:github.timeline@1411511874174000-1411512174175000]
// ...
Observable.Timer(TimeSpan.Zero, TimeSpan.FromMinutes(5))
    .Timestamp()
    .Buffer(2, 1) // Buffer Window
    .SelectMany(xs =>
    {
        var context = Query.GetContext();
        context.UseQueryCache = false;
        return context.From<github_timeline>().WithRange(xs[0].Timestamp, xs[1].Timestamp)
            .Select(x => new { x.repository_name, x.created_at })
            .ToArrayAsync();
    })
    .Dump();

アプリケーション側のStreaming Insertの間隔(バッファとかもするだろうし本当のリアルタイムじゃあないでしょう?)と、そしてBigQueryのクエリ時間(数秒)の絡みがあるので、まぁ1分ぐらいからでしょうかねー、でもまぁ、多くのシチュエーションでは十分許容できるんじゃないかと思います、障害調査で今すぐログが欲しい!とかってシチュエーションであっても間に合う時間だし。

よほどの超リアルタイム(バッファもほとんど取らず数秒がマスト)でなければ、もはやAmazon Kinesisのような土管すらもイラナイ感じですね。ストレージとしてもBigQueryは激安なので、Streaming Insertが安定するならば、もうBigQuery自体を土管として使って、各アプリはBigQueryから取り出して配信、みたいな形でも良いというかむしろそれでいい。Range decoratorsが効いてるなら走査範囲も小さいんで速度も従量課金も全く問題ないしねぇ。BigQuery最強すぎる……。

データ転送

本筋じゃないのでちょっとだけ話ますが、C#ってことは基本Windows Server(AWS上に立ってる)で、データをどうやってBigQueryに送るのー?と。もちろんFluentdは動かないし、(Windowsブランチあるって?あー、うーん、そもそも動かしたい気がない)、どうしますかね、と。ストレージに突っ込んでコピーは簡単明快でいいんですが、まぁ↑に書いたようにStreamingやりたいね、というわけで、うちの会社((株)グラニ。gihyoに書いた神獄のヴァルハラゲートの裏側をCTOが語り尽くす!とか読んでくださいな)では基本的にStreaming Insertのみです。ETW/EventSource(簡単な説明はWindows high speed logging: ETW in C#/.NET using System.Diagnostics.Tracing.EventSourceを)経由でログを送って、Semantic Logging Application Block(SLAB)のOut-of-process Serviceで拾って、自家製のSink(ここは今のところ手作りする必要あり、そのうちうちの会社から公開するでしょふ)でStreaming Insert(AWS->BigQueryでHTTP経由)。という構成。

今のとこリトライは入ってますが完全インメモリなんでまるごと死んだらログはロスト。といった、Fluentdが解決している幾つかの要素は解決されてないんですが、それなりに十二分に実用には使えるところかな、と。速さとかの性能面は全く問題ありません、ETWがとにかく強いし、そっから先もasync/awaitを活かした並列インサートが使えるので他のでやるよりはずっと良いはずきっと。

TODO:

実はまだRecord型に対応してません!なのでそれに関係するFLATTENやWITHIN句も使えません!99%のクエリが再現できる、とか言っておきながら未対応……。おうふ、ま、まぁ世の中のほとんどは入れ子な型なんて使ってませんよね……?そんなことはないか、そうですね、さすがに対応は必須だと思ってるので、早めに入れたいとは思ってます。

あと、LINQPadにはDataExplorerがあって、ちゃんとスキーマ情報の表示やコネクション保持とか出来るんですねー。というわけで、真面目にそのLINQPadドライバは作りたいです、というか作ろうとしていましたし、割と作れる感触は掴んだんです、が、大きな障壁が。LINQPadドライバは署名付きであることを要求するのですが、Google APIs Client Library for .NETが、署名されてない……。署名付きDLLは全部の参照DLLが署名付きであること必要があって、肝心要のGoogleライブラリが使えないという事態に。俺々署名してもInternalVisibleToがどうのこうのとかエラーの嵐で一歩も進めないよー。Googleが署名さえしてくれてれば全部解決なのに!だいたい著名なライブラリで署名されてないのなんかGoogleぐらいだよ!もはやむしろありえないレベル!なんとかして!

IQueryable is Dead. Long live Expression!

ちょっとだけC#の話もしよふ。以下、LINQ好きだからポエム書くよ!

LINQ to BigQueryはIQueryableじゃあ、ありません。この手のクエリ系のLINQはIQueryableでQuery Providerである必要が……、あるの?IQueryableは確かにその手のインフラを提供してくれるし、確実にLINQになる。けれど、絶対条件、なの?

私がLINQ to BigQueryで絶対譲れない最優先の事項として考えたのは、LINQで書けないクエリをなくすこと。全てのクエリがLINQで書ける、絶対に文字列クエリを必要としないようにする。そのためにはIQueryableの範囲を逸脱する必要があった。そして同時に強く制約したかった、順序も規定したいし、不要なクエリは(NotSupported!)そもそも書けないようにしたかった。これらはIQueryableに従っていては絶対に実現できないことだった。

LINQがLINQであるためにはクエリ構文はいらない。Query Providerもいらない。LINQ to XMLがLINQなのは何故?Parallel LINQがLINQであるのは何故?Reactive ExtensionsがLINQであるのは何故?linq.jsがLINQであるのは何故?そこにあるのは……、空気と文化。

LINQと名乗ること自体はマーケティングのようなもので、形はない。使う人が納得さえすれば、LINQでしょう。そこにルールを求めたがる人がいても、ないものはないのだから規定しようがないよ?LINQらしく感じさせる要素をある程度満たしてればいい。FuncもしくはExpressionを使ってWhereでフィルタしSelectで射影する(そうすればクエリ構文もある程度は使えるしね)。OrderBy系の構文はOrderBy/OrderByDescending/ThenBy/ThenByDescendingで適用される。基本的な戻り値がシーケンスっぽい何かである。うん、だんだん満たせてくる。別に100%満たさなくても、70%ぐらい満たせばLINQらしいんだよ。SelectManyがなくたって、いい。どうせNotSupportedExceptionが投げられるのなら、最初からないのと何が違うというの?

LINQ to BigQueryからはLINQらしさを感じられると思っています。最優先事項の全てのBigQueryのクエリを書けるようにすることやNotSupportedを投げないことなどを持ちつつも、可能な限りLINQらしさを感じさせるよう細心の注意を払ってデザインしました。極論言えば私がLINQだって言ってるんだからLINQなのですが(何か文句ある?)、多くの人には十分納得してもらえると考えています。LimitをTakeで”書けない”とかね、BigQueryらしくすることも使いやすさだし、LINQらしくすることも使いやすさ。この辺は私の匙加減。

と、いうわけでIQueryableは、データベース系クエリの抽象化というのが幻想で、無用の長物と化してしまったのだけど、しかし役に立たなかったかといえば、そうじゃあない。LINQだと感じさせるための文化を作る一翼をIQueryableは担っていたから。データベース系へのクエリはこのように定義されていると”らしい”感じになる。その意識の統一にはIQueryableは必要だった、間違いなく。しかし時は流れて、もう登場から6年も経ってる。もう、同時にかかった呪いからは解放されていいんじゃないかな?みんなでIQueryableを埋葬しよう。

と、いうのがIQueryableを使ってない理由。死にました。殺しました。IQueryableは死んだのですが、しかしExpressionは生きています!LINQ to BigQueryも当然Expressionで構成されています。空前のExpression Tree再評価の機運が!で、まぁしかしだからってふつーのアプリのクエリをExpression Treeでやりたいかは別の話ね。やっぱ構築コストとか、そもそもBigQueryは比較的シンプルなSQLだから表現しきれたけどふつーのSQLは複雑怪奇で表現できないだろー、とか、色々ありますからね。まぁ、あんま好ましく思ってないのは変わりません。

コストの話は、BigQueryの場合は完全に無視できるのよね。クエリのレスポンスが普通のDBだったら数msだけど、BigQueryは数千~数万msと桁が4つも5つも違う。リクエスト数もふつーのクエリは大量だけどBigQueryはほとんどない(一般ユーザーが叩くものじゃないからね)。なので、ほんとうの意味でExpression Treeの構築や解釈のコストは無視できちゃう。そういう、相当富豪的にやっても何の問題もないというコンテキストに立っています。だからLINQ to BigQueryはあらゆる点で完全無欠に有益。

LINQPad用お土産一式

Query.GetContextとかDumpChartとかは、LINQPadの左下のMy Extensionsのとこに以下のコードをコピペってください。それで有効になります。本当はLINQPad Driver作ってそれ入れれば有効になるようにしたかったんですが、とりあえず今のところはこんなんで勘弁してくだしあ。こんなんでも、十分使えますので。

// Import this namespaces
BigQuery.Linq
System.Windows.Forms.DataVisualization.Charting
Google.Apis.Auth.OAuth2
Google.Apis.Bigquery.v2
Google.Apis.Util.Store
Google.Apis.Services
 
public static class Query
{
    public static BigQueryContext GetContext()
    {
        BigQueryContext context;
        // Replace this JSON. OAuth2 JSON Generate from GCP Management Page. 
        var json = @"{""installed"":{""auth_uri"":""https://accounts.google.com/o/oauth2/auth"",""client_secret"":"""",""token_uri"":""https://accounts.google.com/o/oauth2/token"",""client_email"":"""",""redirect_uris"":[""urn:ietf:wg:oauth:2.0:oob"",""oob""],""client_x509_cert_url"":"""",""client_id"":"""",""auth_provider_x509_cert_url"":""https://www.googleapis.com/oauth2/v1/certs""}}";
 
        using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(json)))
        {
            // Open Browser, Accept Auth
            var userCredential = GoogleWebAuthorizationBroker.AuthorizeAsync(ms,
                new[] { BigqueryService.Scope.Bigquery },
                "user",
                CancellationToken.None, new FileDataStore(@"LINQ-to-BigQuery")) // localcache
                .Result;
 
            var bigquery = new BigqueryService(new BaseClientService.Initializer
            {
                ApplicationName = "LINQ to BigQuery",
                HttpClientInitializer = userCredential
            });
 
            context = new BigQueryContext(bigquery, "write your project id");
        }
        // Timeout or other options
        context.TimeoutMs = (long)TimeSpan.FromMinutes(1).TotalMilliseconds;
        return context;
    }
}
 
public static class MyExtensions
{
    public static IEnumerable<T> DumpChart<T>(this IEnumerable<T> source, Func<T, object> xSelector, Func<T, object> ySelector, SeriesChartType chartType = SeriesChartType.Column, bool isShowXLabel = false)
    {
        var chart = new Chart();
        chart.ChartAreas.Add(new ChartArea());
        var series = new Series { ChartType = chartType };
        foreach (var item in source)
        {
            var x = xSelector(item);
            var y = ySelector(item);
            var index = series.Points.AddXY(x, y);
            series.Points[index].ToolTip = item.ToString();
            if (isShowXLabel) series.Points[index].Label = x.ToString();
        }
        chart.Series.Add(series);
        chart.Dump("Chart");
        return source;
    }
 
    public static IEnumerable<IGrouping<TKey, T>> DumpGroupChart<TKey, T>(this IEnumerable<IGrouping<TKey, T>> source, Func<T, object> xSelector, Func<T, object> ySelector, SeriesChartType chartType = SeriesChartType.Line)
    {
        var chart = new Chart();
        chart.ChartAreas.Add(new ChartArea());
        foreach (var g in source)
        {
            var series = new Series { ChartType = chartType };
            foreach (var item in g)
            {
                var x = xSelector(item);
                var y = ySelector(item);
                var index = series.Points.AddXY(x, y);
                series.Points[index].ToolTip = item.ToString();
            }
            chart.Series.Add(series);
        }
        chart.Dump("Chart");
        return source;
    }
}

GCPの管理ページからOAuth2認証用のJSONをベタ貼りするのとプロジェクトIDだけ書いてもらえれば使えるかと。最初にブラウザ立ち上がって認証されます、2回目以降はローカルフォルダにキャッシュされてるので不要。まぁ色々ザルなんですが、軽く使う分にはいいかな、と。

まとめ

いやもう本当に、この手のソリューションではBigQueryが群を抜いて凄い。Azure使ってる人もAWS使ってる人(実際、うちのプロダクトはAWS上で動かしてますがデータはBigQueryに投げてます)もオンプレミスの人もBigQuery使うべきだし、他のものを使う意味が分からないレベル。とにかく試せ、であり、そして試すのは皆Googleアカウントは絶対持ってるはずだからワンポチするだけで立ち上がってるし、最初から膨大なサンプルデータがあるので簡単に遊べるし、一発で気にいるはず、間違いない。

そしてWindows(C#)の人には、LINQ to BigQuery + LINQPadがベストなツールとなってくれるはず。むしろあらゆるBigQueryを扱う環境の中でC#こそが最高といえるものになってくれるよう、色々やっていきたいですね。

Prev |

Search/Archive

Category

Profile


Yoshifumi Kawai
Microsoft MVP for Visual Studio and Development Technologies(C#)

April 2011
|
July 2019

Twitter:@neuecc
GitHub:neuecc
ils@neue.cc