Archive - Programming

LightNode 2 - OWINからASP.NET Coreへの移植実例

ASP.NET Core以前に.NET Coreをガン無視している昨今。というのも、.NET Coreというかようするところ最近の.NETは横、つまりクロスプラットフォームへの広がりなんですよね。それ自体は素晴らしく良いことではあるのですが、縦、つまり機能面での拡充があるのかどうかというと、あんまない気がしています。それは、クロスプラットフォームいうても基本的にはWindowsでしか現状/当分は使わないんだよなー、という私みたいな人間にとってはあまり興味を引かれるものではないのであった。

とかっていつまでも言ってるのもアレなので、とりあえずLightNode(という私の作ってるOwinで動くMicro REST Framework、ようはASP.NET Web APIみたいなやつ)をASP.NET Coreに移植してみました。アプリケーションの移植じゃなくてライブラリの移植なので、むしろ楽です。LightNodeはガチガチにOwinのみで構築していたので、ほとんど単純な置換のみでOKでした。

ASP.NET Coreで動作させるだけなら、OWIN - ASP.NET Coreのブリッジを使うという手もありますが、今回は完全にASP.NET Core向けに書き直しました。せっかくやるなら、ちゃんとしっかりしたものにしたいですしね、HttpContextのほうが望ましいのにIDictionaryなEnvironmentが露出してたりすると嫌じゃん。そんなわけでつまり、OWINに関連する部分は完全にASP.NET Core仕様に変わったので、互換性はありません。

ASP.NET Coreライブラリ開発の準備と移植手順

準備としてVisual Studio 2015 Update 3.NET Core SDKを入れればOK。が、しかしいきなり.NET Core SDKがUpdate 3が入ってねーよエラーが出てインストールできなくて泣いた。世の中厳しい。Forumによるとそういう事例多数。対応としては「DotNetCore.1.0.0-VS2015Tools.Preview2.exe SKIP_VSU_CHECK=1」で叩けば入るよって話で、そうして叩くことによってようやく準備OK。幸先は悪い。

そうして入ったらテンプレートに.NET Core系があるので、とりあえずClass Library(.NET Core)を作る。参照してるのは .NETStandard Library 1.6.0。この辺良く分からないんですが、corefx/.NET Platform Standardによると.NET 4.6.3ぐらいに相当するそうで。ふーむ、まぁASP.NET Core系がnetcoreapp 1.0で1.6と同じところらしいので、このままでOKっぽい。気がする。とりあえず。UWPとかが視野の場合はちょっと話は違うのでしょうけれど。

次にASP.NET系のライブラリをNuGetで入れる。のですが、どの参照をいれればいいのかがまず分からない:) 今回はOwin的なMiddlewareを作りたかったんですが、ここはMicrosoft.AspNetCore.Http.Abstractionsが最適のようですね。これでようやくスタートライン。

既存のLightNodeのコードを突っ込むと当然激しくコンパイルエラーが出るのでここからチマチマと直していきました。まず目につくのがリフレクション関連で、IsEnumとかTypeに生えてる判別系のメソッドが片っ端から動いてません。誰しもが通る.NET Coreの洗礼!これは、type.GetTypeInfo() によるTypeInfoのほうにIsEnumなどなどが生えてるので、ひたすらGetTypeInfoを書き加えるだけの簡単なお仕事をします。GetTypeInfoの嫌なところはSystem.Reflection名前空間への拡張メソッドとして実装されてるので、IntelliSenseに出てこなくてイラッとする率が高いこと……。まぁ、あと実際にひたすらGetTypeInfoを書きまくるのは面倒くさいので、Typeへの拡張メソッドとして GetTypeInfo().IsEnum とかコンパイルエラー出てるものだけ定義してやることで作業量低減(まぁプロパティは()を書かなきゃいけないのでアレですけど。拡張プロパティはよ)

また、Parallel.ForEach がない!これは.NETStandardには含まれてないそうなので、別途System.Threading.Tasks.ParallelをNuGetから拾ってくる。なんかこう、標準に入ってて当たり前だろ、みたいに思うものが別添えになってるの、不思議な感覚ですね。これだともはやReactive Extensionsが標準にないとかImmutable Collecitonsが標準にないとか、どうでもというか全く大したことない話に見えます。なんせParallel.ForEachすらないんだから!(ところでNuGetのVersion History見ると結構細かくアップデートされてはいるんですが、いったいなにが変わったのかRelease Note出して欲しくはある……)

AppDomain.CurrentDomain.GetAssemblies もない!対象アセンブリ内からControllerがわりのクラスを引っ張ってきたくて、読み込み済みのAssemblyからGetTypesして全部検査したい、というのをやりたいわけですが、ないんですねえ。そして実際、これの代替は今のところないらしい……(というかAppDomainが今のところない)。フレームワーク系の常套手段なのに……。Loaderがどうのこうのとか、あとASP.NET Core側で特化した何かはありそうな気配を感じなくもなかったんですが、今回はGetAssembliesじゃなくても回避可能なので(一手間ではあるんですが、外側からその対象AssemblyのTypeを渡してさえくれればAssembly拾えてGetTypesできる)、Typeを渡してもらう方式のみに制限することでとりあえず回避しました。

ここから先はASP.NET Core的なところ。 IDictionary[string, object]HttpContextに変える。そしてAppFuncRequestDelegateに変える。だけの簡単なお仕事。OWINとASP.NET Coreの差異はそれだけだし中身一緒なので、機械的に置き換えていくだけ。OWINに関してはどうだのこうだのと一悶着あったりなかったりで色々ありましたが、一番下のレイヤーで触ってる限りは、概念はほんと完全に一緒なので無駄なことは全くなかったですね。上のレイヤーで触っていても、それはそれで何も考えず置き換えられるはずなので、実際のとこOWINは良かったと思ってます。新しい、ASP.NET CoreのHttpContextは、昔のHttpContextというよりかは、OWINのEnvironmentそのものだったりしますからね。

これでコンパイル通ったので、実行確認のためASP.NET Core Web Application(.NET Core)テンプレートから新規プロジェクトを作成。ASP.NET Core的なテンプレートはもちろんEmptyで。Startupに以下のを書いて

using LightNode;
using LightNode.Server;
 
public class Startup
{
    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
    {
        app.UseLightNode(typeof(Startup));
    }
}
 
public class Toriaezu : LightNodeContract
{
    public string Echo(string x)
    {
        return x;
    }
}

http://localhost:15944/Toriaezu/Echo?x=hoge にアクセスでhogeが出力される。おー、ちゃんとできてますね!当たり前っちゃあ当たり前でしょうけれど、あまりに意外にすんなり動いたので普通に感動した。いやあ、いいじゃんASP.NET Core。

Swagger Included

LightNode 1の時はSwaggerは別添えだったんですが、今回はとりあえず一緒に突っ込んじゃいました。JSON.NET使ったJSON出力とかも同梱です(というかデフォルトがそれになってます)。まぁSwaggerに関してはDependencyが増えるわけでもないしいいじゃんといえばいいじゃん、なので。いいかな、と。LightNodeのSwagger統合はビュー(HTMLとかCSSとか画像とか)がDLLに埋め込んでやってたんですが、そうしたリソースを.NET Coreで埋め込むにはどうすればいいのか。今まではPropertiesで埋めてったんですが、.NET Coreではproject.jsonに書くのが正解のようですねー。

"buildOptions": {
        "embed": [
            "Swagger/SwaggerUI/**"
        ]
}

buildOptions.embedで指定できるようで、ああ、なるほど、これはこれで知ってれば凄く楽なので、全然良いですね。良いです。いいじゃん.NET Core。

というわけでサクッとSwagger統合も果たせた。

public class Startup
{
    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
    {
        app.Map("/api", builder =>
        {
            builder.UseLightNode(typeof(Startup));
        });
 
        app.Map("/swagger", builder =>
        {
            var xmlName = "AspNetCoreSample.xml";
            var xmlPath = Path.Combine(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), xmlName);
 
            builder.UseLightNodeSwagger(new LightNode.Swagger.SwaggerOptions("AspNetCoreSample", "/api")
            {
                XmlDocumentPath = xmlPath,
                IsEmitEnumAsString = true
            });
        });
    }
}

うーん、全然いけますね、じゃあ次行こう、次。

Glimpse not Included

LightNodeのウリはSwagger統合とGlimpse統合、特にGlimpseへの診断情報表示は力を入れていて(Glimpseへのハックも含めて)他にここまでやってるフレームワークはないほどでした。ので、当然ASP.NET Coreでもやりたいわけですが、んー、そもそもGlimpseがまだASP.NET Coreに本対応してない……。2.0 Betaで一応対応してるということで、あるだけマシか?と思いきや、かなり古いもので全然動かない。というかGitHubでの開発も(1.x系も2.x系も)なんかもうほとんど動いてない……。メイン開発者2名がMicrosoftに転職ということで、ASP.NET Core対応含めてよりアクティブになるのかなー、とか思ってたら、まさかの大失速……。多分、Microsoft内では別のことやっていて、そっちが忙しくて以前よりもなお作業できなくなってるんでしょうね。しかし、うーん、残念だなあ。

ASP.NET Coreに移れない/移りたくない理由があるとしたら、このGlimpseが全然対応してないってことでしょうかねえ。Glimpse自体はほんと素晴らしいので、なんとか再生してくれればいいのですけれど。

感想

.NET CoreにせよASP.NET Coreにせよ、結構コマンド操作がフィーチャーされてて、ゆとりな私には辛いものがあるんですが、さすがに1.0、普通に書いてる限りは、Visual Studio使ってる限りは特にコマンドの必要性もなく、それなりに快適に書けますね。安定してねえー、とか不満に思うことも全然ないので、もう普通に良さそう。いや、思ってたよりも全然いい感じだった。

さて、じゃあASP.NET CoreでもLightNode使おうぜ!になるかというと、うーん、とりあえずまずは普通にASP.NET Core MVCでいいでしょう(笑)。時代がねー、ちょっと違いますからね。LightNodeも3年前ですから。まぁ、でも設計思想とか全然古くなってないというかむしろASP.NET Core MVCがようやく追いついてきたかな、ぐらいの勢いだとは思ってます!例えばASP.NET Core MVCのFilterは完全にLightNodeのフィルターと一緒ですからね。

// ASP.NET Core MVC
public class SampleAsyncActionFilter : IAsyncActionFilter
{
    public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        // do something before the action executes
        await next();
        // do something after the action executes
    }
}
 
// LightNode
public class SampleFilterAttribute : LightNodeFilterAttribute
{
    public override async Task Invoke(OperationContext operationContext, Func<Task> next)
    {
        // do something before the action executes
        await next();
        // do something after the action executes
    }
}

Filters vs. Middlewareの話なんかも、それは3年前に全部考えきって実装し実践してますから(ホジホジ。という話なんで、まぁ全然LightNode 2もいいじゃないでしょうか。LightNodeは他に、密接に統合されたクライアント自動生成などもありますしね。かわりにMVC + Web API的な、Razorのビューを返すコントローラーとWeb APIコントローラーとの統合、みたいなのができてないのは痛み。ここ馴染ませられないのは普通に不便だということを最近良く感じてるので、ASP.NET Core MVCいいですね。いいですね。

OWINベースで書いたものの移行はそこそこすんなり行けるだろうなあ、という感触はなんとなく掴めた気がします。逆に、やっぱASP.NET MVC 5あたりからの移行は厳しそう。厳しいでしょう。どうするんでしょうね、どうしようかな、参りましたね……。

ともあれせっかくの新しい.NETの幕開けなので、もう少しポジティブに情報掴んで行こうかなー、という気にはなれたのでめでたしめでたし。

BigQueryを中心としたヴァルハラゲートのログ分析システム

というタイトルでGoogle for Mobile | Game Bootcampで発表しました。4月なので3ヶ月遅れでスライド公開です。

BigQueryを中心としたヴァルハラゲートのログ分析システム from Yoshifumi Kawai

なんかあまり上手く話せなかったな、という後悔がなんかかなり残ってます:) スライドもフォント細くて吹き出しの文字が見辛いな!とりあえず、WindowsでBigQueryなシステムとしては一つの参考例にはなるのではないかなー、と思います。第一部完。

第二部はEtwStreamへの移行と、BigQuerySinkのOSS公開かなー、というところなんですがまだまだまだまだまだ先っぽいのでアレでコレでどうして。できれば誰もが秒速でASP.NETアプリケーションのログをBigQueryに流し込める、みたいな状況にしたいのですけれどねえ、そこはまだまだ遠いかなー、ですね。そのへんの.NETのエコシステムは弱いと言わざるをえない。けれどまぁ、地道に補完していきたいと思ってます。

Japan VR Hackathonに参加し、AMD賞受賞しました

Japan VR Hackathonに参加してきました!の結果が昨日発表されまして、AMD賞(Best Graphics)を受賞しました。やったー。事前に決めた5人チームでの参加で、大賞取る!という気概でやってたので、入選できて良かったです。

今回作ったのは「Clash of Oni Online」というゲームで、HTC Vive用のVRゲームです。テーマである”日本らしさ”を(一応)イメージした(一応)和風の装い。

飛んで来る岩を

吹っ飛ばす

という、VRバッティングセンター。ViveはVRというだけじゃなくてコントローラーがあるのがいいですねー。

今回、2日間 31時間で242コミット(最初のコミットが2016/06/18 09:13で 最後のコミットが 2016/06/19 16:34)。時間制限のなかでは、ちゃんとゲームしてる(ゲーム性的にもとりあえず爽快に全部打ち倒すパターンと一球一球を狙い撃ちしないパターンを用意)し、グラフィックもまぁ見栄えがするレベルで、オンライン協力プレイも実装(ただしデモ時はオフラインモード)したのは結構頑張った。ハッカソン系参加が全員初めての割には綺麗に収められた感あります。

チーム編成と最終的な役割は

  • 私(プログラマ):プロジェクトセットアップ、敵ボス挙動、サーバーセットアップ、エフェクト発注、雑進行管理、プレゼンスライド作成、動画撮影
  • プログラマ:マルチプレイプログラム、シーン管理プログラム
  • プログラマ:Viveプログラム、エフェクト組み込み
  • プログラマ:企画、地形エディット、サウンド、アセット検索、デモ
  • テクニカルアーティスト:アセット検索、敵モーション作成、ライティング、エフェクト

という感じでした。全員ほぼViveのプログラミング経験はなし(SDK入れて雰囲気掴んだことはあります程度)

最初に入れたアセットは

  • UniRx - ないと無理
  • LINQ to GameObject - ベンリ、だけど今回は別にあまり使わず
  • PhotonWire - マルチプレイ前提なので。合わせてサーバー側プロジェクトもセットアップ
  • SteamVR Plugin - Viveがターゲットなので
  • The Lab Renderer - 使ったことなかったので結局あんま余裕なく使えなかった…

という編成から随時アセット追加追加。

タイムライン

1週間ぐらい前に参加を決める。3日前ぐらいに「2人マルチ協力プレイ」「背中合わせにして立ちまわる(時代劇にある格好いい感じのアレ)雑魚戦 + デカい鬼を撃退するボス戦というアクションゲーム」「グラフィックで魅せる」を軸にする。というのを決定。和風です、和風。コミュニケーション手段としてSlack(チャット)を前日に立てる。Unityのバージョンを5.4.0b21に決めて全員にインストールしておいてもらうように。持ち込み物としてHTC Viveを2台用意。当日、会場ついてからGitHub(リポジトリ管理)のPrivateリポジトリを立てて全員招待。

雑魚戦+ボス戦といっても、作りきれるか怪しいので(実際ボス戦で手一杯だった、そりゃそうだ)、ボス戦から先に作っていくように。最初に決めていた役割分担は

  • 私(プログラマ):マルチプレイ
  • プログラマ:ボスプログラム、プレイヤー行動プログラム
  • プログラマ:ボスプログラム、プレイヤー行動プログラム
  • プログラマ:地形アセット購入/組み込み・サウンド購入/組み込み、パーセプションニューロン触る(使えそうなら使う)
  • テクニカルアーティスト:モデルやらモーションやらエフェクトやら

でした。まぁマルチプレイといってもUnity側のプログラムがある程度できないとやることもないんで、まずは自分で持ってきたViveを自前PCのSurface Bookに繋ごうとしたらSurface Bookの出力をViveがうまく認識してくれなくて最終的に諦め(Forumとか見た限りだとノートPCの出力端子とのトラブル事案は結構多い模様なのでshoganaiね)。ということでViveのプログラミングは他の人に完全に任せることにして、ボスのプログラムを作っていこうかなー、ということに。ボス戦は、崖のような場所にボスが立っている(下半身は見せない)というイメージが共有されたので

迫り来るシリンダー撃退ゲーとして作成。雑に作ったこのシーンは、初日ずっとフル活用されることになったのだった。動画系は常時GifCamで撮ってSlackにあげてました。イメージが瞬時に共有されますし、良い内容だとテンションも上がりますし。

この時点でLeanTweenを追加。マルチプレイできるようにするので、非確定要素をいれないように、というのとそんな凝ることもないしなので弾は全部トゥイーンで制御しようかと。トゥイーンライブラリは色々ありますが、今のとこ私が選ぶならDOtweenかLeanTweenかなぁ。普通だったらDOtween選びます。ただ、今回はLeanTweenにしました、ちょっと慣れておこうかな、と思って。LeanTweenは複数のTweeenの制御とかの補助が入ってないんですが、その辺はUniRxで制御させたので全く問題なし。基本的に完了などのイベントをObservable化すれば既存トゥイーンライブラリとUniRxの統合は容易ですし、かなりいい具合にコントロールできます。実際今回は色々な挙動をそれで組みました。ところで、今ひっそりとUniRx前提のハイパフォーマンスでリアクティブなトゥイーンライブラリを作っているので、それが完成したら基本的にそれしか使いません:) まぁ、というのもあって色々なトゥイーンライブラリを試しているというのもあります。

このシーンをプレイヤー行動プログラム側に渡して、VIVEで弾き返したり防いだりを作ってもらう感じに。パーセプションニューロン触るマンはパーセプションニューロンを触りつつサウンド探しを、テクニカルアーティストはボスのモデル(これ自体は買ったもの)のUnityへのインポートとテクスチャ調整とモーション付けを、私は弾のバリエーションを作ってました。

豪速球を投げ込むバッティングセンター的イメージ。手前で弾が伸びてくるので振りにくい。二者択一(手前のキューブはプレイヤーAとプレイヤーBです、マルチ協力プレイだから!)でどっちに来るか分からないので、なんとなく緊迫感あってゲーム的でもあるよね、ということで最終的なボス行動にも採用。

ぶわーっと全方位に出すのが欲しい(VIVEのデモゲームのThe LabにあるXortexという360度シューティングのボス弾のような)というオーダーを受けて。中々いい感じに派手なので、これも採用。

あとはボツ案的な弾を作ったり、その他、この時点ではまだ夢膨らみんぐで、ボスの行動も腕をばちーんと振り下ろしてきてそれを斬撃の連打で防いで弾き返す(協力プレイなので、片方が防いでたらそれに加勢しないとダメ、とか)、などを想定したコードを準備したりAIは少しリッチにしようかな、とBehaviorTreeのライブラリ書いてたり(ノードエディタなしの基本的なランタイムだけ)、プレイヤープログラム作るチームは盾で防御する処理(最終的に削られたけれど最初は刀と盾の装備のつもりだった)をやってたら、夜0時。うーむ、時間が過ぎるのは早い。

この辺でさすがに未だにシリンダーとキューブが相手で完成形が全く見えてないのはヤヴァいでしょうということで、シーン統合しましょう祭り。特にテストで大量のアセットを抱えていたマップ作るマンがGitHubに中々Pushできないなどなどトラぶりつつも、2時ぐらいにようやく一段落。マップにモーション付きボスモデル配置して、とりあえず弾を出るようにして、でこんな具合に。

ezgif com-resize

色々アレですが、しかし中々格好よくてテンションあがりますね!その他プレイヤーのほうも入れこんだりなんなりで床に転がって仮眠とって、朝。キューブにも岩を当てはめてついに出来上がったのは……!

image

んー、悪くない。悪くないんだけど、和ではない。雲南省(適当なイメージ)とかそういう中国の高山っぽい気すらする。ここで実際完成させる仕様を概ね確定。

  • マップは明確に和テイストが出るものにリテイク
  • ボスは殴り攻撃などなし、弾のみ
  • 盾はなし、弾を打ち返してボスにダメージ与えて、一定回数食らわせたらクリア
  • マルチプレイは諦めないので作業は並走、ただし最終的にはシングルプレイが完全にプレイできるの優先

私は、ボスのモーションが二種あって、殴りつけてくるつもりでつけてもらったモーションはボスが弾を投げ飛ばしてくる(つもり)な雰囲気に適当に調整(タイミングは適当にdelayかけて目視で合わせただけでジャストとは程遠いんですが、まぁなんとなくそれっぽく見えなくもないのでヨシとした)したり、もう一個の大技っぽくやってくるモーションは、なんか岩を抱え込んでる感じにできそうな気がしたので、適当にそれっぽく位置合わせして破裂させてみることに。

resize

うん、それっぽい。地面にめり込んでってるのとかも、まぁ全然気にならないし。このボス行動は今回のハッカソンで私的な私が作った中では一番よくやりましたしょうでした。全部、偶然素材が揃っただけなんですがうまく噛み合ったということで。

この後は、マップを和テイストに差し替えて常時マップブラッシュアップ、ゲームの要素が確定したので、各種のヒットエフェクトを作ってもらって当て込みや効果音、プレイ感向上のための弾の動きなどの調整、そして諦めてないマルチプレイなどなどを時間ギリギリまで使ってなんとか完成……!(実際、最後の30分でボスのダメージエフェクトがつき、最後の15分前でボスが死ぬようになった程度にギリギリだった)

マルチプレイに関しては、Viveのセンサーが干渉してうまく二台プレイの調整ができなかったのと、もう一台のデスクトップPCを会場の無線LANに繋げなくて、というネットワーク的な問題で断念。いちおう、プログラム側はマルチ想定で動作するように最後まで組んでました、いやほんと。サーバー側、AzureのVMも一時的なものなのでということで、かなりマシンパワーの強いものに変更したりしたんですけどね、というわけでここをお見せできなかったのは残念。なので、最後の5分でマルチプレイ用のログイン待機処理を消して、リリースビルド完成。お疲れ様ー。

完成形

出来上がったものは、マップリテイクによって城が追加されたことにより「城下まで迫ってきた赤鬼を、手に持つサムライブレードにより撃退し、城を守る」という設定に。なっていた。完全後付けで。ゲーム名は特に何も考えもなく直前で「Clash of Oni Online」に大決定。

ハッカソンでの評価は特に会場でのプレゼンはなく、体験してもらって審査員が表を付けてく形式とのことで、あとはデモマンがいい感じに来場者に説明してるのを横目に私は会場を見学する:)最後の一秒までドタバタと調整を続けていた割には、目立ったバグもなくスムースにプレイできてて良かった良かった。

最終的には当日の審査ではなく後日の審査ということなので、プレゼン資料を作ったり動画を撮ったりして

Clash of Oni Online - VR Multiplay Sword Action from Yoshifumi Kawai

結果待ち……!そして発表……!受賞……!やったね!

反省点とか

当初の想定よりもViveのルームスケールを活かしてない、直立不動のスタイルになったのは、ちょっと想定外。弾を避けたりとか、近寄ったりとかもうちょっとだけアクティブなのをイメージしていたので、しょうがないといえばしょうがないのだけれど、次に何か作るのだったら動くタイプのを作りたいですねぇ。

効果音が足りなかったり、割れてたり。効果音足りないのは、岩の音を、ボス撃破音とか足すべき箇所はいっぱいですよねー。マップリテイクで時間が取れなかったのがその辺の敗因か。Viveコントローラーと刀の位置が微妙にあってなかったり、足が地面に設置してなかったりといった、プレイヤーに対する調整も甘め。shoganai。この辺はViveプログラミングにて慣れてれば、スッと合わせられる話だと思うので、経験値を積もう。ボスの全方位弾が実は全方位じゃなくて左に寄ってるのは普通にロジックのバグ……。リテイク後のマップのクオリティが急ぎで用意しただけあってリテイク前に比べると低い(雑に光源足すためだけの灯籠を並べるとかしたかった)、ボスを遠方に置く形になってしまったのでスケール感が出なかった、など。

とか、まぁアラはいくらでも見つかりますが、基本的にはよくやったと思ってる……!よ!チームメンバーが全員、より良くするために自分の仕事を探して作りきっていったというのは純粋なハッカソンの楽しさという感じで、疲労困憊だけど達成感はありますね。

それとViveでのプログラミングは、結構ゲーム作成入門(Unity入門)にいいかもですね。3Dのプレイヤーの操作ってモーションつけたり色々ハードル高いですが、Viveならすぐに手の動きがキャプチャされて自由に動かせるアクションが作れるので、よくあるシューティングとかブロック崩しとか作っていくよりも楽しいんじゃないかな。(今のVR経験値が少ない現状なら)VRで空間を見て、Viveコントローラーで自由に操作できるというのは、それだけで楽しい体験を作れちゃいますしね。

そんなわけで、家でもViveを設置したしGeforce GTX 1080搭載PCも買ったので、ちょいちょいとVive用に何か作っていきたいという気持ちを強くしました。ので、ちょいちょいと出していければいいですねー。

ObserveEveryValueChanged - 全てをRx化する拡張メソッド

ブードゥーの秘術により、INotifyPropertyChanged不要で、値の変更を検知し、IObservable化します。例えばINotifyPropertyChangedじゃないところから、WidthとHeightを引き出してみます。

using Reactive.Bindings.Extensions;
 
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
 
        this.ObserveEveryValueChanged(x => x.Width).Subscribe(x => WidthText.Text = x.ToString());
        this.ObserveEveryValueChanged(x => x.Height).Subscribe(x => HeightText.Text = x.ToString());
    }
}

wpfgif

なるほど的確に追随している。ソースコードはGitHub上に公開しました。

ReactivePropertyと組み合わせることで、そのままバインダブルに変換することも可能です。

public class MyClass
{
    public int MyProperty { get; set; }
}
 
public partial class MainWindow : Window
{
    MyClass model;
    public IReadOnlyReactiveProperty<int> MyClassMyProperty { get; }
 
    public MainWindow()
    {
        InitializeComponent();
 
        model = new MyClass();
        this.MyClassMyProperty = model.ObserveEveryValueChanged(x => x.MyProperty).ToReadOnlyReactiveProperty();
    }
}

ついでにokazukiさんが、ReactiveProperty v2.7.3に組み込んでくれましたので(今のところ).NET版では是非是非に使えます。UWP用とかXamarin用とかもきっとやってくれるでしょう(他人任せ)

仕組み

CompositionTarget.Renderingに引っ掛けて、つまり毎フレーム監視を走らせています。もともとUniRxのために作った機構を、そのままWPFに持ってきました。CompositionTarget.Renderingは、アニメーション描画などでも叩かれている比較的低下層のイベントで、これより遅いと遅れを人間が検知できちゃうので影響が出るし、これより早くても視認できないので意味がない。という、ぐらいの層です。こういった用途ではベストなところ。

毎フレーム監視がありかなしか。ゲームエンジンだと、そもそもほとんどが毎フレームごとの処理になっているので違和感も罪悪感もないのですけれど、全てがイベントドリブンで構築されている世界にそれはどうなのか。もちろん、原則はNoです。素直にINotifyPropertyChangedを書くべきだし、素直にReactivePropertyを書くべきでしょう。

ただ、アニメーションでも使われるしデバイスのインプット(LeapMotionとか)もその辺に引っ掛けるようなので、ここにちょっとプロパティに変更があるかないかのチェック入れるぐらい別にいいじゃん(どうせCPU有り余ってるんだし)、みたいな開き直りはあります。かなり。割と。

ObserveEveryValueChangedは、毎フレーム回っているような低下層の世界から、イベントドリブン(リアクティブ)な世界に引き上げるためのブリッジとしての役割があります。そう思うと不思議と、よく見えてきませんか?ただ「毎フレームポーリングかよ、ぷぷw」とかって一笑するだけだと視野が狭く、もう少しだけ一歩踏み込んで考えてみると思考実験的に面白い。私はコード片に意思を詰め込んでいくのが好きですね。哲学といってもいいし、ポエムでもある。そこには幾重も意味が込められています。

PhotonWire - Photon Server + Unityによる型付き非同期RPCフレームワーク

というのを作りました。Unityでネットワークマルチプレイヤーゲーム作るためのフレームワーク。といっても、100%自作じゃなくて、基本的にPhoton Serverというミドルウェアの上に乗っかるちょっと高級なレイヤーぐらいの位置づけです。去年の9月ぐらいに作った/作ってるよ、というのは発表していたのですが、それからかれこれ半年以上もpublicにしてないベイパーウェアだったのですが(グラニ社内では使ってました)、重たい腰を上げてやっと、やっと……、公開。

謳い文句は型付き非同期RPCフレームワークで、サーバー側はC#でasync/awaitを使ったメソッド実装、Unity側はそこから生成されたUniRx利用のメソッドを呼ぶだけで相互に通信できます。それでなにができるかというと、Visual StudioでUnity -> Server -> Unityと完全に一体化してデバッグできます。全部C#で。もうこれだけで最強でしょう。他は比較にならない。勝った。終わった。以上第一部完。

真面目に特徴、強みを上げると、以下のような感じです。

  • 完全タイプセーフ。サーバー-サーバー間は動的プロキシ、クライアント-サーバー間は事前クライアント生成。
  • IDLレス。C#サーバーコードのバイナリを元にして、クライアントコードを生成するので、普通にサーバーコードを書くだけ。面倒なIDLはゴミ箱ぽい。
  • 高性能。サーバーはasync/awaitで、クライアントはUniRxにより完全非同期で駆動。特にサーバーのC#コードはIL直書きも厭わずギチギチに最適化済み。
  • 事前生成シリアライザによるMsgPackでのシリアライズ/デシリアライズ。デシリアライズは更にマルチスレッド上で処理してUniRxでメインスレッドにディスパッチするのでフレームレートに一切影響を与えない。
  • Visual Studioとの完全な統合。高いデバッガビリティと、Analyzer利用を前提にしたフレームワーク構成はVS2015時代の新しい地平線。
  • 外部ツール「PhotonWire.HubInvoker」により外からAPIを直接叩ける。

HubInvokerは私にしては珍しく、ちゃんと見た目にこだわりました。これの外観の作り方はMaterial Design In XAML Toolkitでお手軽にWPFアプリを美しくで記事にしてます。

Photon Serverを選ぶ理由

Unityでもネットワーク系は色々な選択肢があると思います。

  • UNET
  • PUN + Photon Cloud
  • Photon Server(SDK直叩き)
  • モノビットエンジン
  • WebSocketで自作
  • MQTTで自作

このあたりは見たことある気がします。そのうちUNETは標準大正義だしAPIもProfilerも充実してる感なのですが、uNet Weaver Errorがムカつくので(コンパイルができなくなるという絶望!特にUniRx使ってると遭遇率が飛躍的に上昇!)、それが直らないかぎりは一ミリも使う気になれない。というのと、サーバーロジックを入れ込みたいどうしてもとにかくむしろそれがマスト、な状況の時にというか割とすぐにそうなると思ってるんですが、Unity純正だと、逆にUnityから出れないのが辛いかな、というのはありますね(ロードマップ的にはその辺もやるとかやらないとかあった気がしますが、まぁ遠い未来ということで)。Unity外で弄れるというのは、サーバーロジックだけじゃなくHubInvokerのようなツールを作れるっていうのも良いところですね。大事。なので、標準大正義は正しくも選べないのです。

モノビットはよく知らないので。C++でサーバーロジックは書きたくないなあ、今はC#も行けるんですっけ?

自作系は、あんまりそのレイヤーの面倒は見たくないので極力避けたい。別に動くの作るのはすぐでも、まともにちゃんと動き続けるの作るのは大変なのは分かりきってる話で。トラブルシュートも泣いちゃう。そこに骨を埋める気はない。あと、自作にするにしてもプロトコルの根底の部分で安定してるライブラリがあるかないかも大事で(そこまで自作は本当に嫌!)、Unityだとただでさえそんなに選択肢のないものが更に狭まるので、結構厳しい気がするのよね。実際。

Photonといって、Photon Cloudの話をしているのかPUN(Photon Unity Network)の話をしているのか、Photon Serverの話をしているのか。どれも違く、はないけれど性質は違うのだから一緒くたに言われてもよくわからない。さて、PUN。PhotonのUnityクライアントは生SDKが低レイヤ、その上に構築されたPUNが高レイヤのような位置づけっぽい感じですが、PUNは個人的にはないですね。秒速でないと思った。PUNの問題点は、標準のUnity Networkに似せたAPIが恐ろしく使いづらいこと。標準のUnity Network自体が別に良いものでもなんでもないレガシー(ついでにUnity自体も新APIであるUNETに移行する)なので、それに似てて嬉しい事なんて、実際のとこ全くないじゃん!もうこの時点でやる気はないんですが、更にPhoton Serverで独自ロジック書いたらそこははみ出すので生SDK触るしかないのだ、なんだ、じゃあいらないじゃん?Client-Client RPCも別になくてもいいし、というかなくていいし。

Photon Server。C++のコアエンジンってのは言ってみればASP.NETにおけるIISみたいなもので、開発者は触るところじゃない、直接触るのはサーバーSDKとクライアントSDKだけで、つまり両方ピュアC#。その上では普通にC#でガリガリと書ける。いいじゃん。両方ピュアC#というのが最高に良い。サーバーはWindowsでホストされる。それも最高に良い。プロトコルとかはゲーム専用で割り切ってる分だけ軽量っぽい。うん、悪くないんじゃないか。

また、ホスティングは結構優秀です。まず、無停止デプロイができる(設定でShadowCopy周りを弄ればOK)。これ、すっごく嬉しい。この手のは常時接続なのでデプロイ時に切断するわけにもいかないし、これ出来ないとデプロイの難易度が跳ね上がっちゃいますからねぇ。また、1サーバーで擬似的に複数台のシミュレートなどが可能です。実際、グラニでは6台構成クラスタのシミュレートという形で常に動かしていて、どうしても分散系のバグを未然に防ぐには重要で、それがサクッと作れるのは嬉しい。脚周りに関しては、かなり優秀と思って良いのではないでしょうか。

PhotonWireの必要な理由

Photon Serverがまぁ悪くないとして、なんでその上のレイヤーが必要なのか。これは生SDKを使ったコードを見てもらえれば分かるかしらん。

// 1. クライアント送信
var peer = new CliendSidePeer(new MyListener());
peer.OpCustom(opCode:10, parameter:new Dictionary<byte, object>());
// 2. サーバー受信
protected override void OnOperationRequest(OperationRequest operationRequest, SendParameters sendParameters)
{
    switch (operationRequest.OperationCode)
    {
        case 10:
           // Dictionaryに詰まってる
            var parameter = operationRequest.Parameters;
            HogeMoge(); // なんか処理する
            // 3. 送り返す
            this.SendOperationResponse(new OperationResponse(opCode:5), sendParameters); // 
            break;
        // 以下ケース繰り返し
        default:
            break;
    }
}
public class MyListener : IPhotonPeerListener
{
    // 4. クライアント受信
    public void OnOperationResponse(OperationResponse operationResponse)
    {
        // 返ってきたレスポンス
        switch (operationResponse.OperationCode)
        {
            case 5:
                // なんかする
                break;
        }
    }
}

問題点は明白です。原始的すぎる。byteパラメータ、Dictionaryを送って受け取りそれをswitch、送り返してやっぱswitch。こうなると当然長大なswitchが出来上がってカオスへ。また、クライアント送信とクライアント受信がバラバラ。コネクションで送信した結果を受け取るのが、独立した別のListenerで受け取ることになる、となると、送信時にフラグONで受信側でフラグチェック祭り、Listener側のフラグ制御が大変。送信したメッセージと戻り受信メッセージだという判別する手段がないので、並列リクエストが発生するとバグってしまう。

これをPhotonWireはHubという仕掛け(とUniRx)で解決します。

image

ようするにMVCのControllerみたいな感じで実装できます、ということですね。また、逆に言えば、PhotonWireはそんなに大きな機能を提供しません。あくまで、このswitchやちょっとしたシリアライゼーションを自動化してあげるという、それだけの薄いレイヤーになっています。なので、PhotonWireによるコードが素のPhoton Serverによるものと少し異なるからといって、あまり警戒する必要はありません。実際、薄く作ることは物凄く意識しています。厚いフレームワークは物事の解決と同時に、別のトラブルを呼び込むものですから……。

ちなみにPhotonWireを通すことによる通信のオーバーヘッドは4バイトぐらいです。それだけで圧倒的に使いやすさが向上するので、この4バイトは全然あり、でしょう。

Hub

HubというのはASP.NET SignalRから取っています。というか、PhotonWireのAPIはSignalRからの影響がかなり濃いので、ドキュメントはSignalRのものを漁れば20%ぐらいは合ってます(全然合ってない)

// Unityクライアント側で受け取るメソッド名はインターフェイスで定義
public interface ITutorialClient
{
    [Operation(0)]
    void GroupBroadcastMessage(string message);
}
 
[Hub(100)]
public class Tutorial : PhotonWire.Server.Hub<ITutorialClient>
{
    // 足し算するだけのもの。
    [Operation(0)]
    public int Sum(int x, int y)
    {
        return x + y;
    }
 
    // 非同期も行けます、例えばHTTPアクセスして何か取ってくるとか。
    [Operation(1)]
    public async Task<string> GetHtml(string url)
    {
        var httpClient = new HttpClient();
        var result = await httpClient.GetStringAsync(url);
 
        // PhotonのStringはサイズ制限があるので注意(デカいの送るとクライアント側で落ちて原因追求が困難)
        // クラスでラップしたのを送るとPhotonの生シリアライズじゃなくてMsgPackを通るようになるので、サイズ制限を超えることは可能 
        var cut = result.Substring(0, Math.Min(result.Length, short.MaxValue - 5000));
 
        return cut;
    }
 
    [Operation(2)]
    public void BroadcastAll(string message)
    {
        // リクエスト-レスポンスじゃなく全部の接続に対してメッセージを投げる
        this.Clients.All.GroupBroadcastMessage(message);
    }
 
    [Operation(3)]
    public void RegisterGroup(string groupName)
    {
        // Groupで接続の文字列識別子でのグループ化
        this.Context.Peer.AddGroup(groupName);
    }
 
    [Operation(4)]
    public void BroadcastTo(string groupName, string message)
    {
        // 対象グループにのみメッセージを投げる
        this.Clients.Group(groupName).GroupBroadcastMessage(message);
    }
}

async/awaitに全面対応しているので、同期通信APIを混ぜてしまっていて接続が詰まって死亡、みたいなケースをしっかり回避できます。属性をペタペタ張らないといけないルールは、Visual Studio 2015で書くとAnalyzerがエラーにしてくるので、それに従うだけで良いので、かなり楽です。

プリミティブな型だけじゃなくて複雑な型を受け渡ししたい場合は、DLLを共有します。

// こんなクラスをShareプロジェクトに定義して、Server側ではプロジェクト参照、Unity側へはビルド済みDLLをコピーする
public class Person
{
    public int Age { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}
// サーバーがこんなふうに戻り値を返して
[Operation(1)]
public Person CreatePerson(int seed)
{
    var rand = new Random(seed);
 
    return new Person
    {
        FirstName = "Yoshifumi",
        LastName = "Kawai",
        Age = rand.Next(0, 100)
    };
}
// Unity側では普通に受け取れる
proxy.Invoke.CreatePersonAsync(Random.Range(0, 100))
    .Subscribe(x =>
    {
        UnityEngine.Debug.Log(x.FirstName + " " + x.LastName + " Age:" + x.Age);
    });

プロジェクトの構成はこんな感じ。シームレス。

image

また、オマケ的に、Unity側でのエディタウィンドウではコネクションの接続状況と送受信グラフがついてきます。UNETの立派なProfilerに比べるとショボすぎて話にならないんですが、ないよりはマシかな、と。

サーバー間通信

Photon Serverはサーバーとサーバーを接続してクラスタを作れるのですが、その通信もHubを使ったRPCで処理しています。

// ServerHub(呼ばれる方)
[Hub(54)]
public class MasterTutorial : PhotonWire.Server.ServerToServer.ServerHub
{
    [Operation(0)]
    public virtual async Task<int> Multiply(int x, int y)
    {
        return x * y;
    }
}
 
// ClientHub(呼ぶ方)
[Hub(99)]
public class Tutorial : Hub
{
    [Operation(0)]
    public async Task<int> ServerToServer(int x, int y)
    {
        var val = await GetServerHubProxy<MasterTutorial>().Single.Multiply(x, y);
        return val;
    }
}

この見た目、直接呼んでるかのように書けるサーバー間通信は、実行時には以下のように置き換わってネットワーク呼び出しに変換されています。

image

なので、ServerHubはかならず戻り値はTaskじゃないとダメです(Analyzerが警告してくれます)。昔はこの手の処理を、メソッド呼び出しのように隠蔽する場合って、同期的になっちゃって、でもネットワーク呼び出しなので時間かかってボトルネックに、みたいなパターンが多かったようですが、今はTask[T]があるので自然に表現できます。このへんも含めてTask[T]が標準であることの意味、async/awaitによる言語サポートは非常に大きい。

この辺りの詳しい話は以下のスライドに書いています。

Metaprogramming Universe in C# - 実例に見るILからRoslynまでの活用例 from Yoshifumi Kawai

ネットワーク構成

PhotonWireは特に何の既定もしません。Photonが自由に組める通り、どんな組み方もできるし、どんな組み方をしてもPhotonWireでの呼び出しに支障は出ません。

のはいいんですが、その時、ClientPeer, InboundS2SPeer, OutboundS2SPeerの3種類のPeerを持つように、PhotonWireもまたHub, ServerHub, ReceiveServerHubとそれぞれに対応する3種のHubを持っています。3つ、これは複雑で面倒。

しかしPhotonWireはネットワークの複雑さの隠蔽はしません。やろうと思えばできますが、やりません。というのも、これ、やりだすと泥沼だから。賢くやりたきゃあAkkaでもなんでも使ってみればよくて、自分で書いたら一生終わらない。Photonのネットワークは本当に全然賢くなくて、ただたんに直結で繋いでるという、それだけです。そんなんでいい、とまではいいませんが、そうなら、それに関しては受け入れるべきでしょうね。勘違いしちゃあいけなくて、フレームワークは複雑さを隠蔽するもの、ではないのです。

ともあれ、最低限の賢くなさなりに、スケールしそうな感じに組み上げることは可能なので、全然良いとは思ってますよ!

できないこと

ポンと貼り付けてtransformが自動同期したり、いい感じに隙間を補完してくれたりするものはありません。ただ、Client-Server RPCがあれば、それは、その上で実装していくものだと思うので(いわゆるNantoka ToolkitとかNantoka Contribの範疇)、しゃーないけれど、自前で作ろうという話にはなってきますね。↑のネットワーク構成の話も、隠蔽とまではいかなくても、決まった構成になるのだったらそれなりにバイパスするいい感じのユーティリティは組んでいけるだろうから、その辺のちょっとした増築は、やったほうがいいでしょう。

まとめ

現状実績はないです(今、公開したばかりですからね!)。ただ、グラニで開発中の黒騎士と白の魔王というタイトルに投下しています。

半年以上は使い続けているので、それなりには叩かれて磨かれてはいるかなあ、と。大丈夫ですよ!と言い切るには弱いですが、本気ですよ!とは間違いなく言えます。DLLシェアや自動生成周りが複数人開発でのコンフリクトを起こしがちで、そこが改善しないと大変かなー、というところもありますが、全般的にはかなり良好です。

ちょっと大掛かりだったり、Windows/C#/Visual Studioベッタリな、時代に逆行するポータビリティのなさが開き直ってはいるんですが、結構使い手はあると思うので試してみてもらえると嬉しいですね!あと、大掛かりといっても、知識ゼロ状態からだったら素のPhoton Server使うよりずっと楽だと思います。そもそもにPhotonWireのGetting Startedのドキュメントのほうがよほど親切ですからねぇ、Visual Studioでのデバッグの仕方とかも懇切丁寧に書いてありますし!

VR時代のマルチプレイヤーって結局どうすんねん、と思ってたんですが、Project TangoのサンプルがPhotonだしAltspaceVRもPhotonっぽいので、暫くはPhotonでやってみようかなー。という感です。

MarkdownGenerator - C#におけるAPI Reference生成のためのドキュメントツール

APIリファレンス作りたい?Sandcastle。以上。終了。あるいはdotnet/docfxが良いのではないでしょうか。こいつはdotnet配下にあるように、MSの今後のOSS系のはこれでドキュメント生成されていく可能性があります。

というのは置いといて、私的には実のところ、あんまり重要視していませんでした、ドキュメントツール。.chmにはいい思い出がなくて、というか別に見ないじゃん?htmlで出力してもなー、なんかゴチャゴチャしてて汚いしなー。一方でJavaScriptなんかは様々な格好良くフォーマットされた形式で色々出てるのであった。いいじゃん。いいね。

さて、もう一つ。HTMLで出力しても置き場にこまる。GitHub Pagesにはそんないい思い出がない。別にあんなところをフロントにするよりもリポジトリのアドレス直のほうが断然いいじゃん、みたいな。というわけでアレだ、GitHub Wikiだ。あそこをゴミ置き場にすればいいんだ。という発想で、まずUniRxのリファレンスをGitHub Wikiに置いてみました。

UniRxは、namespaceを意図的にある程度平ったくしてるので、ちょっとごちゃってますが、まぁまぁいいんじゃない?それなりに見れる。悪くはない。少なくともないよりは100億倍良い。

私的にはIntelliSenseがドキュメントだ!みたいな意識がそれなりにあって、最初のチュートリアルみたいなドキュメントがあったら、あとはそれを手がかりにあとはIntelliSenseでなんとかしようぜ、的なところが。実際Roslynなんかはそんな感じがする。Getting Startedはそれなりに厚い、けど全貌からは程遠い。でもAPIドキュメントはない。さあ、IntelliSenseで宝探しだ。って。肯定もしないけれど否定もしない、そういうのも今風よね。でも、まあこの程度のAPIリファレンスでも生成してやると、それはそれで良いな、って思ったのだ。です。

MarkdownGenerator

生成は自家製こんそーるあぷりで行ってます。というわけで公開しました。

dllとxmlを渡すとmdと目次用のHome.mdをばらまくので、GitHub Wikiに投げ込みます。そう、GitHub Wikiはご存じの方も多い通り、それ自体がgitで管理されててCloneできるのです。さいこー。というわけでそのままPushするだけ。Good。完璧。これなら、CIなんかでフックして毎回生成して投げ飛ばしてあげてもいい。よね。

生成結果のStyleはちょっとまだまだ試行錯誤中。まあでも割とこんなもんでいいんちゃうんちゃうん?ユースケースの9割ぐらいはカバーできているでしょう。それ以上はノイズということで。

その他ツール

Sandcastleはそもそも出力をカスタマイズできるので、もう少し真面目というかガッチリしたものが必要ならば、maxtoroq/sandcastle-mdあたりを使ってMarkdownを出力してやると良いでしょう。これなら、きっちりとSandcastleで出力される情報が全部そのまま入ってるので、ちゃんとしてる感は圧倒的に高いです。また、繰り返しますけれどDocFXは今からやるなら最有力候補な気がします。DLLからじゃなくてRoslynでプロジェクトファイルから解析したりとか今風。あくまでstatic file generatorなのでmdじゃなくてhtml出力なので、Wikiに投げ飛ばす用途には向かないのと、ちょっと複雑、使いこなすのは難しい、かな、まあ相応には良さそうかとは。

MarkdownGeneratorは、ちゃんとしてないなりに、私的に重要視してる情報がパッと一覧で見やすくする、ということを重視しているので……。あと、なんかSandcastle使いたくないんだよねー、心理的に。なんだろうね、レガシー臭するからなのかな。食わず嫌いなだけって話でもあるのだけれど。

何れにせよ、GitHubであってもなくてもいいだろうけれど、API Referenceを投げ飛ばす場としては、そういうところ(どういうところ?)がいいですね。独立してるよりもリポジトリに近い場所のほうが素敵度は高い。気がする。あとはなんのかんのでGitHubに慣れきってるというのもあって、GitHubにあると情報がスムースに受け取れる気がするんだよね。これもなんでだろうね。でもそういうのってあるよね。

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チェックする場合とで、戻ってくるタイミングが異なるので、そこのルールを統一させないとかなあ。

Microsoft MVP for Visual Studio and Development Technologies(C#)を再々々々々受賞しました

今年の受賞で、6年目です。エキスパタイズとしてはVisual Studio and Development Technologiesという微妙な感じなので、C#です。C#です、と言わせてください。

春はあけぼの、というわけで会社も新たなステージに入りました。原点を振り返りつつも、新たな創業として、より強固な姿を掲示できればいいかな、と思います。

去年に私は

私が主に力を入れているのはUnityと、そのReactive Extensions実装のUniRxで、特にUniRxはかなりヒットさせられたとは思います。が、まだまだ兆しといったところなので、確固たるものにしなければならない。また、それを基盤にして、C#の強さというのを、ただの今までの.NETコミュニティにだけに留まらず、幅広い世界に届ける、伝えていきたい。

と言ってましたが、それは達成できました(いやほんと)。しかし、よくよく考えると、なんだか小粒です。今年はもう少し大きなヴィジョンで行きたいですね。最近のMicrosoftのメッセージとしても、C#の強さがより一層際立ってきました。それをしっかりと補強し、力強く、Real World C#というのを示し続けていきます。そう、Real Worldで。どういうことかといえば、そういうことです。今年はね。

引き続き主戦場はC#とUnityです。Unityも.NET Foundation加入やHololensやVRの最有力の開発環境であったりと、変わらず非常に魅力的で、強力な環境ですす。いろいろな形で、刺激的なことを掲示し続けられればいいかな、と思っています。

そんなわけで引き続き、今年もよろしくお願いします。

Roslyn Analyzerでコンフィグを読み込ませて挙動を変更する

方法。が、欲すぃ。例えば採番する時に0ベースなのか1ベースなのかプロジェクトによって変えたい。例えばCodeFix時の名前変更のルールを先頭アンスコ付けるのか付けないのかを自由に変えさせたい。NotifyPropertyChangedGenerator - RoslynによるVS2015時代の変更通知プロパティの書き方の時は、専用のAttributeを使うので、その中のインターフェイスを書き換えてコンフィグ代わりにしてね、という方法を取ったのですが、専用の属性が使えなきゃ適用できない手法で、全然汎用的っぽくないし、当然ながら全然イケてない。

ではどうするか。実は、Additional Filesという仕組みが用意されているので、それを用いることでコンフィグを読みこませることができます!詳細はroslyn/Using Additional Files.mdに書かれていますが、任意のテキストファイルを色々な手法(コマンドラインの引数やcsprojの定義としてなど)でプロジェクトに設定し、Analyzer側では AnalyzerOptions.AdditionalFiles で読めます。

では、やってみましょう。

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class AdditionalFileAnalyzer : DiagnosticAnalyzer
{
    static DiagnosticDescriptor Rule = new DiagnosticDescriptor("AdditionalFileAnalyzer", "AdditionalFileテスト", "AdditionalFiles:{0}", "Usage", DiagnosticSeverity.Error, isEnabledByDefault: true);
    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get { return ImmutableArray.Create(Rule); } }
 
    public override void Initialize(AnalysisContext context)
    {
        context.RegisterSymbolAction(Analyze, SymbolKind.NamedType);
    }
 
    private static void Analyze(SymbolAnalysisContext context)
    {
        // AnalyzerOptionsにAdditionalFilesがある。
        // CodeFixContextの場合はProjectに生えてるので、 context.Document.Project.AnalyzerOptions で取得可能。
        var additionalFiles = context.Options.AdditionalFiles;
 
        // Pathから引っ掛けて取る
        var config = additionalFiles.FirstOrDefault(x => System.IO.Path.GetFileName(x.Path) == "config.json");
        if (config != null)
        {
            // GetText().ToString()で文字列が取れるので、あとはJsonConvertでデシリアライズするなりなんなりどうぞご自由に……。
            var text = config.GetText().ToString();
            context.ReportDiagnostic(Diagnostic.Create(Rule, context.Symbol.Locations[0], text));
        }
        else
        {
            context.ReportDiagnostic(Diagnostic.Create(Rule, context.Symbol.Locations[0], "JSONが見つかってないぞ"));
        }
    }
}

これでJSONが見つかってないぞ、と怒られまくります。ファイルを追加するには、まずプロジェクトの適当なところにconfig.jsonを足して、Build Actionを AdditionalFiles に変更します。が、変更しようとするとやっぱり怒られるので、しょうがないからcsprojを手動で書き換えます。

<ItemGroup>
    <AdditionalFiles Include="config.json" />
</ItemGroup>

これで、以下の様な結果が得られます。

image

うん、ちゃんと読めてる!

コンフィグとして使うには、まぁイマドキのJSONでコンフィグだったらJSON.NETでJsonConvert.DeserializeObjectなんかでサクッと復元してやるのが楽でしょう。外部DLL読み込むのが嫌だという場合は、XMLにしてLINQ to XMLやXmlSerializer使うのも全然お手軽でいいとは思います。

コンフィグなんてほぼほぼ固定になるのに毎回デシリアライズ走らせるのは嫌だなあ、って場合はstaticオブジェクトにキャッシュしちゃうのも悪くない手だと思います。その場合は、書き換えた場合はプロジェクト再読み込み(or VS再起動)になってしまいますが、実用的には全然問題ないはず。といいたいのですが、複数のプロジェクト跨ぎで共有されちゃうと厄介だったりするので注意が必要なので、まぁちょっとDeserializeするぐらい大したことねーよということで、毎度Deserializeするのも全然構わないかな、とは。

このコンフィグを読み込む手法はStyleCopAnalyzer/Configuration.mdでも採用されているし、これがスタンダードのやり方だと思って良さそうです。

SerializableDictionary - Unityで高速に辞書復元するためのライブラリ

という、ScriptableObjectとかJsonUtilityとか、そもそもSerializeFieldとかでシリアライズできるDictionaryを作りました。

もともとDictionaryはシリアライズできないのですが、ISerializationCallbackReceiverを用いてシリアライズ/デシリアライズのタイミングでKeyの配列、Valueの配列に戻してやるなどで保存すること自体は全然可能でした。のですが、速度的には問題あるな、というのに直面しました。

その前に、JSONから復元するのがまず遅かった。じゃあMsgPackやProtobufに変更したら速いかといえば、別にそこまでそうではなかった。これはつまり、C#のレイヤーで大量の何かを舐めて何かを作るという行為そのものが遅い。ではScriptableObject化すればどうだろう、確かにデシリアライズのプロセスがUnityネイティブ(実体は不明)と化して、確かに速い。が、そこからDictionaryに変換してやるのをC#で書いたらやっぱりそこが遅い。

遅い、というとアレで、量次第ですけどね。今回、量がやたら多かったので結構かなり相当引っ張られてた。初期化のタイミングなどで大量のDictionaryを捌くような場合に、無視できない程度に結構引っかかる遅さを醸し出してる。結局、配列からであっても、C#のレイヤーで大量の何かを舐めて何かを作るという行為そのものが遅い。という悲しい現実をつきつけられるのであった。

というわけで、SerializableDictionaryはDictionaryの内部構造をシリアライズすることで、ネイティブプロセスのみで完結して爆速で復元します。

SerializableDictionaryではSerializableDictionary, SerializableLookup(MultiDictinary), SerializableTupleの3つを提供します。今のとこアセットストアに公開するつもりはそんなないので、使いたい場合はソースコードをZipでダウンロードするなりGitで落とすなりしてプロジェクトに投げ込んでください。

SerializableDictionary

例えばキーがint、値がstringの辞書を保存したい場合は、まず、継承したクラスを作ります。

[Serializable]
public class IntStringSerializableDictionary : SerializableDictionary<int, string>
{
 
}

わざわざ継承しなきゃいけない理由は、ジェネリックな型はシリアライズできないからです!しょうがないね。別にゆうてそんなに大量の型があるわけでもないでしょうし、素直にそれぐらいは作りましょう。あとは、普通に使えば普通にシリアライズ可能になってます。メデタシメデタシ。

インスペクタに表示するためのPropertyDrawerも用意してあります(こちらも定義しないとインスペクタに何も表示されなくて不安になる)。使う場合は、SerializableDictionaryPropertyDrawerを継承した型を一つ作って、そこに属性でひたすらぶら下げます。

#if UNITY_EDITOR
 
[UnityEditor.CustomPropertyDrawer(typeof(IntStringSerializableDictionary))]
[UnityEditor.CustomPropertyDrawer(typeof(IntDoubleSerializableDictionary))]
[UnityEditor.CustomPropertyDrawer(typeof(IntIntStringSerializableDictionary))]
public class ExtendedSerializableDictionaryPropertyDrawer : SerializableDictionaryPropertyDrawer
{
 
}
 
#endif

これを定義すれば、インスペクタ上では

image

なんとKeyとValueの確認しか用意されてなくて、エディット不能!ただのDump!うーん、気が向いたらエディット可能にします。そのうち(多分やらない)。

複数キーの辞書

Int + Intの組み合わせでキーにしたいとか、辞書にはよくあるケースです。そして、そういったよくあるケースではKeyにTupleを使うことが多いです。が、UnityにはTupleはありません。UniRxにTupleがあります、が、それはシリアライズ可能ではありません(Genericだからねー、structなので継承もできない)。と、いうわけで、辞書のキーにしたいよね専用にSerializableTupleを用意しておきました。使う場合はもちろんまずは継承してジェネリックを消すとこからはじめます。

[Serializable]
public class IntIntTuple : SerializableTuple<int, int>
{
    public IntIntTuple()
    {
 
    }
 
    public IntIntTuple(int item1, int item2)
        : base(item1, item2)
    {
 
    }
}
 
[Serializable]
public class IntIntStringSerializableDictionary : SerializableDictionary<IntIntTuple, string>
{
 
}

あとは普通にキーに使ってもらえれば、普通に使えます。ちょっと手間ですが、そこまで多いわけでもないでしょうし我慢できる範囲内。だといいかな。

SerializableLookup

ILookupは、Keyに対してValue側が複数になっている辞書です。Dictionary[Key, Value[]] みたいなイメージ。通常のILookupはLINQのToLookup経由でしか作成できない、Readonlyなシロモノです。これ、非常に便利な型でして、よく使います。ToLookupしらない人は覚えましょう。ただ、勿論シリアライズできないのでAddを加えたSerializableLookupを用意しました(Removeはありません!つまりBuilderのほうがイメージ近いかもしれません。Removeがない理由は実装しててバグッたからとりあえず消してるだけなのでそのうち入れるかもしれないかもしれない)

使い方はDictionaryと同様。

[Serializable]
public class IntIntSerializableLookup : SerializableLookup<int, int>
{
}
 
#if UNITY_EDITOR
 
[UnityEditor.CustomPropertyDrawer(typeof(IntIntSerializableLookup))]
public class ExtendedSerializableLookupPropertyDrawer : SerializableLookupPropertyDrawer
{
 
}
 
#endif

ちなみに中身は面倒くさいんでSerializableDictionaryの一部を改変して辻褄合わせてるだけなので、実効速度的な意味ではToLookupで生成したものに比べるとやや劣るかなー、といったところ。まぁハッシュキーの衝突具合とかにもよるので、いうほどそこまでではないと思います。実装の雑さは気にしてるのでそのうち直したい(絶対やらない)

TrimExcess

ListにせよDictionaryにせよ、任意個数をAdd可能なものは、内部である程度余分なバッファを持っています。しかし、ScriptableObjectなどにしてAssetBundleに載せたい場合は、その後の追加なども特になく個数は固定である可能性も少なくないはずです。と、いうわけで、TrimExcessメソッドを呼ぶことで余分なバッファを切り落とすことができます。もし個数が固定であることが見えているなら、事前にSerialize前に呼んであげておくことで、メモリ節約につながります。

Unityでシリアライズ可能なもの

内部構造の話なのですが、その前にUnityでシリアライズ可能なものの制限についておさらい。

  • [Serializable]のついた非ジェネリックな具象型
  • UnityEngine.Objectを継承した型
  • public、または[SerializeField]のついたインスタンスフィールド
  • int, float, double, bool, stringなどのプリミティブなデータ型
  • 配列、もしくはList[T]

Dictionaryに非対応なのは勿論ですが、Nullable[T]に非対応が割と痛かったりするかな!また、トップレベル以外でnullをサポートしていなかったりして、ちょっと複雑な型を作った場合、nullを入れたと思ったら全部0が入った謎データに置き換わっていた、とかが生じます。それらの制限の回避策としては、SerializableDictionaryと同じようにSerializableNullableのようなそれっぽい似非な型を自前で作ってあげればなんとかなります。nullのほうも同様にNullableClass(なんじゃそりゃ)を作ってあげることにより、nullかそうでないかの区別を可能にできます。面倒くさくはあるんですが、どうしても必要な場合はそうして回避できなくもないよ、ということで。

SerializableDictionaryは、Dictionaryの内部構造を、Unityでシリアライズ可能な範囲(ようするにひたすら単純な配列まみれにする)に修正することで実現しています。オリジナルのDictionary自体はdotonet/corefxのものです。

ハッシュコードを永続化することの安全性

ハッシュコードを永続化することは、推奨されないことが明言されています。というのも、そのオブジェクトに対するハッシュコードが一意であるかが、どのスコープまで保たれるかというのは、全くもって不明瞭だからです。参照型などはアプリケーション起動毎に異なってなにの役にも立たなくなる、では数字は?文字列は?保証はないんですねー。

実用的な意味では、問題ないと判断しても構わないと思っています。しかし、まず、monoと.NET Frameworkのような環境が違うもので生成したもの同士の互換性はないと考えたほうがいいでしょう(実際ない)。また、.NET Framework内だけでも、バージョンが異なれば、違うハッシュアルゴリズムが使われることにより異なるハッシュコードが使われる可能性は全然あります。今後も、Unityのバージョンアップ+(もしあるのなら)monoのバージョンアップが発生した際などは、互換性が崩れる可能性があります。最悪そうなった場合は、任意のComparerを挟み込めるようになっているので、そこで互換性を保ったハッシュコードを返してやることにより、一応大丈夫とはいえます。一応。

とはいえまあ、実用的な意味では大丈夫でしょう。タブンネ。まぁしかし、この辺グレーゾーンなきらいもあるから、Unity公式でサポートってのは難しいんじゃないかなあ、というのはしょうがないかなー。

まとめ

Unityって結局どこで動いているものなのよ、ってのを改めて突きつけられた感じがしました。C++のエンジンがあくまでも主だな、と。また、シリアライズを通して考えると、一見不思議なMonoBehaviourやpublic fieldなども納得がいくように見えてきて、ようするにネイティブとの境界線を接続している場所なんですね。COMとdynamicでやり取りするように、ネイティブレイヤーとフィールドでやり取りする。そう思えば、何もかも腑に落ちてきた気がします(悟り!)。

C#のレイヤーでいかに仕事をさせないかがキモで、そのためにC#を書く。ってのも、まぁ悪くない話だし、Unityアプリケーションとしての整合感やパフォーマンスが最も求めるべきことなのだ。というのは認識しておきたいな、なんて改めて思わさせられました。

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

EtwStream - ETW/EventSourceのRx化 + ビューアーとしてのLINQPad統合

EtwStreamというのをリリースしました。ETW(Event Tracing for Windows) + EventSourceが.NETで構造化ログをやる際の決定版というか、ETWの最強度が高すぎてそれ以外考えられないレベルなんですが、しかし、がETWは最強な反面ビューアーがありませんでした。ETWというブラックホールにログを投げ込むのはいいんですが、それが自分自身ですら容易に見れないのは不便すぎる!PerfViewとか骨董品みたいなゴミUIを操ってなんとかして見るのは、無理ゲーなわけで、カジュアルにDumpしたいだけなんだよ!テキストのようなログビューアーが欲しいだけなんだよ!に対する答えです。いや、ほんと自分自身が死ぬほど欲しかったのが、これ。

etwstreamgif

インストールはLINQPadのNuGetで「EtwStream.LinqPad」。だけ。デフォルトにでも登録しとけばLINQPadを立ち上げるだけですぐにビューアーに!

EtwStreamが提供するのは、ETWをIObsevable[TraceEvent]に変換することです。Logs are streamsですから、そしてストリームといったらRxですから。あとは、LINQPadのDumpをそのまま流用して、色付けとか加えてあげただけです。フィルタリングしたい?グルーピングしたい?色々混ぜたい?そんなの全部Rxなんだから、ちょっとクエリ書けばいいだけなのです。最強の柔軟性がある。

Observable.Merge(
    ObservableEventListener.FromTraceEvent("LoggerEventSource"),
    ObservableEventListener.FromTraceEvent("MyCompanyEvent"),
    ObservableEventListener.FromTraceEvent("PhotonWire")
)
.DumpWithColor(withProviderName: true);

EventSourceの提供する構造化ログ(Structured Logging)に関してはC#における構造化ログの手法、そしてデータ可視化のためのDomoの薦めで書いたのでそっちを見てくださいな。そうしてEventSourceに移行した場合の最大の懸念であるビューアーがなさすぎ問題を、このEtwStreamが解決します。た。

ちなみについでにTailっぽくファイルもIObservable[string]に変換するObservableEventListener.FromFileTailもオマケとして入れといたので、そっちもそっちでログビューアー的に使うならきっとベンリ。

もしEventSourceを使ったロギングをやっていなくても、.NET標準組み込みの、例えばTplEventSourceあたりを眺めてみると、色々な挙動が見えて面白かったりします。あとFromClrTraceEventではGCやThraedPoolの挙動が見れたり、FromKernelTraceEventで普段絶対気にしないカーネルイベントが凄まじい勢いで流れて行ったりが簡単に観測できて、普通に勉強になります。オモチャとしてかなり良いと思いますねー。

最初のEventSource

EventSourceって何のことだかさっぱりわからん!という人におすすめなのがLogging What You Mean: Using the Semantic Logging Application BlockというMSDNに転がってるSLABのドキュメントです。これはさすがにひじょーによく書けてるしいいですね。あと、EtwStreamが提供してるのはObservableEventListenerだけで、ロガー的なファイル書き出しとかは一切ないので、そういうのやりたい人は普通にSLAB「も」使いましょう。という感じです。

さて、EventSourceですが、いきなり構造化ログってのもかなりダルいので、まずは非構造化ログをEventSourceで実現するところから初めてみましょう。いや実際それに、こういういのがちょっとあるとそれはそれでベンリでもありますし。

[EventSource(Name = "LoggerEventSource")]
public class LoggerEventSource : EventSource
{
    public static readonly LoggerEventSource Log = new LoggerEventSource();
 
    public class Keywords
    {
        public const EventKeywords Logging = (EventKeywords)1;
    }
 
    string FormatPath(string filePath)
    {
        if (filePath == null) return "";
 
        var xs = filePath.Split('\\');
        var len = xs.Length;
        if (len >= 3)
        {
            return xs[len - 3] + "/" + xs[len - 2] + "/" + xs[len - 1];
        }
        else if (len == 2)
        {
            return xs[len - 2] + "/" + xs[len - 1];
        }
        else if (len == 1)
        {
            return xs[len - 1];
        }
        else
        {
            return "";
        }
    }
 
    [Event(1, Level = EventLevel.LogAlways, Keywords = Keywords.Logging, Message = "[{2}:{3}][{1}]{0}")]
    public void LogAlways(string message, [CallerMemberName] string memberName = "", [CallerFilePath] string filePath = "", [CallerLineNumber] int line = 0)
    {
        WriteEvent(1, message ?? "", memberName ?? "", FormatPath(filePath) ?? "", line);
    }
 
    [Event(2, Level = EventLevel.Critical, Keywords = Keywords.Logging, Message = "[{2}:{3}][{1}]{0}")]
    public void Critical(string message, [CallerMemberName] string memberName = "", [CallerFilePath] string filePath = "", [CallerLineNumber] int line = 0)
    {
        WriteEvent(2, message ?? "", memberName ?? "", FormatPath(filePath) ?? "", line);
    }
 
    [Event(3, Level = EventLevel.Error, Keywords = Keywords.Logging, Message = "[{2}:{3}][{1}]{0}")]
    public void Error(string message, [CallerMemberName] string memberName = "", [CallerFilePath] string filePath = "", [CallerLineNumber] int line = 0)
    {
        WriteEvent(3, message ?? "", memberName ?? "", FormatPath(filePath) ?? "", line);
    }
 
    [Event(4, Level = EventLevel.Warning, Keywords = Keywords.Logging, Message = "[{2}:{3}][{1}]{0}")]
    public void Warning(string message, [CallerMemberName] string memberName = "", [CallerFilePath] string filePath = "", [CallerLineNumber] int line = 0)
    {
        WriteEvent(4, message ?? "", memberName ?? "", FormatPath(filePath) ?? "", line);
    }
 
    [Event(5, Level = EventLevel.Informational, Keywords = Keywords.Logging, Message = "[{2}:{3}][{1}]{0}")]
    public void Informational(string message, [CallerMemberName] string memberName = "", [CallerFilePath] string filePath = "", [CallerLineNumber] int line = 0)
    {
        WriteEvent(5, message ?? "", memberName ?? "", FormatPath(filePath) ?? "", line);
    }
 
    [Event(6, Level = EventLevel.Verbose, Keywords = Keywords.Logging, Message = "[{2}:{3}][{1}]{0}")]
    public void Verbose(string message, [CallerMemberName] string memberName = "", [CallerFilePath] string filePath = "", [CallerLineNumber] int line = 0)
    {
        WriteEvent(6, message ?? "", memberName ?? "", FormatPath(filePath) ?? "", line);
    }
 
    [Event(7, Level = EventLevel.Error, Keywords = Keywords.Logging, Version = 1)]
    public void Exception(string type, string stackTrace, string message)
    {
        WriteEvent(7, type ?? "", stackTrace ?? "", message ?? "");
    }
 
    [Conditional("DEBUG")]
    [Event(8, Level = EventLevel.Verbose, Keywords = Keywords.Logging, Message = "[{2}:{3}][{1}]{0}")]
    public void Debug(string message, [CallerMemberName] string memberName = "", [CallerFilePath] string filePath = "", [CallerLineNumber] int line = 0)
    {
        WriteEvent(8, message ?? "", memberName ?? "", FormatPath(filePath) ?? "", line);
    }
 
    [NonEvent]
    public IDisposable MeasureExecution(string label, [CallerMemberName] string memberName = "", [CallerFilePath] string filePath = "", [CallerLineNumber] int line = 0)
    {
        return new StopwatchMonitor(this, label ?? "", memberName ?? "", FormatPath(filePath) ?? "", line);
    }
 
    [Event(9, Level = EventLevel.Informational, Keywords = Keywords.Logging, Message = "[{0}][{2}:{3}][{1}]{4}ms")]
    void MeasureExecution(string label, string memberName, string filePath, int line, double duration)
    {
        WriteEvent(9, label ?? "", memberName ?? "", FormatPath(filePath) ?? "", line, duration);
    }
 
    class StopwatchMonitor : IDisposable
    {
        readonly LoggerEventSource logger;
        readonly string label;
        readonly string memberName;
        readonly string filePath;
        readonly int line;
        Stopwatch stopwatch;
 
        public StopwatchMonitor(LoggerEventSource logger, string label, string memberName, string filePath, int line)
        {
            this.logger = logger;
            this.label = label;
            this.memberName = memberName;
            this.filePath = filePath;
            this.line = line;
            stopwatch = Stopwatch.StartNew();
        }
 
        public void Dispose()
        {
            if (stopwatch != null)
            {
                stopwatch.Stop();
                logger.MeasureExecution(label, memberName, filePath, line, stopwatch.Elapsed.TotalMilliseconds);
                stopwatch = null;
            }
        }
    }
}

ちょっと長いですが、これで

LoggerEventSource.Log.Debug("ほげほげ!");

とか書いていくだけです。それを書いたアプリを、LINQPadでは

ObservableEventListener.FromTraceEvent("LoggerEventSource").DumpWithColor();

で、ファイルとかを通さずそのままストリームで外から観測できます。

Logs are event streams

脱ファイル。ちなみにETWで流したのは最終的にBigQueryに流すのが超おすすめですね!そしてLINQ to BigQuery + LINQPadで解析する。完璧!これがC#の次世代ログのあるべき姿だ!と、オモイマス。というか逆にもう以前には戻れないかなあ、やっぱり世代が一つ変わった感あります、便利度が全然違うので。

Prev |

Search/Archive

Category

Profile


Yoshifumi Kawai
Microsoft MVP for .NET(C#)

April 2011
|
March 2017

Twitter:@neuecc