C#での動的なメソッド選択における定形高速化パターン
- 2014-01-27
動的なメソッド選択、といってもなんのこっちゃというわけですが、身近な例だと、ようするにURLをルーティングして何のコントローラーの何のメソッドを呼ぶのか決めるって奴です、ASP.NET MVCとかの。ようするにLightNodeはいかにして速度を叩きだしているのか、のお話。自慢。嘘本当。
以前にExpression Treeのこね方・入門編 - 動的にデリゲートを生成してリフレクションを高速化という記事を書いたのですが(2011/04ですって!もうすっかり大昔!)、その実践編です。Real World メタプログラミング。
とあるController/Action
とあるControllerのとあるActionを呼び出したいとするじゃろ?あ、ちなみに別に例として手元にあるのがちょーどこれだったというだけで、別に同様なシチュエーション(動的にメソッドを選択する)では、うぇぶに限らず何にでも応用効きます。というわけでウェブ興味ね、という人も帰らないで!!!このサイトはC#のサイトですから!
// 何の変哲もない何か
public class HogeController
{
public string HugaAction(int x, int y)
{
return (x + y).ToString();
}
}
// コード上で静的に解決できるならこう書くに決まってるじゃろ?
var result = new HogeController().HugaAction(1, 10);
ただたんにnewして実行するならこうなります。が、これが動的にControllerやActionを選ばなきゃいけないシチュエーションではどうなりますん?ルーティング処理が済んで、呼び出すクラス名・メソッド名が確定できたというところから行きましょう。
// コントローラー名・アクション名が文字列で確定出来た場合
var controllerName = "ConsoleApplication.HogeController";
var actionName = "HugaAction";
var instance = Activator.CreateInstance(Type.GetType(controllerName));
var result = (string)Type.GetType(controllerName).GetMethod(actionName).Invoke(instance, new object[] { 1, 10 });
一番単純なやり方はこんなものでしょう。Activator.CreateInstanceでインスタンスを生成し、MethodInfoのInvokeを呼ぶことでメソッドを実行する。基本はこれ。何事も素直が一番良いですよ。おしまい。
動的コード生成事始め
あけましておめでとうございます。素直が一番、おしまい、で済まない昨今、リフレクションは実行速度がー、という亡霊の声が聞こえてくるのでshoganaiから対処しましょう。基本的にC#でリフレクションの速度を高める手段は一択です、デリゲート作ってキャッシュする。というわけでデリゲート作りましょう。
// ここから先のコードでは↓4つの変数は省略します
var controllerName = "ConsoleApplication.HogeController";
var actionName = "HugaAction";
var type = Type.GetType(controllerName);
var method = type.GetMethod(actionName);
// インスタンスが固定されちゃう
var instance = Activator.CreateInstance(type);
var methodDelegate = (Func<int, int, string>)Delegate.CreateDelegate(typeof(Func<int, int, string>), instance, method);
Delegate作ると言ったらDelegate.CreateDelegate。なのですが、静的メソッドならいいんですが、インスタンスメソッドだと、それも込み込みで作られちゃうので些かイケてない。今回は毎回インスタンスもnewしたいので、これはダメ。
が、Delegate.CreateDelegateの面白いところは、「オープンなインスタンス メソッド デリゲート (インスタンス メソッドの隠れた第 1 引数を明示的に指定するデリゲート) を作成することもできます」ことです。どういうことか、というと
// 第一引数にインスタンスの型が渡せる
var methodDelegate = (Func<HogeController, int, int, string>)Delegate.CreateDelegate(
typeof(Func<HogeController, int, int, string>), method);
// だからこう呼べる(キャストしてたりしてActivator.CreateInstanceの意味がほげもげ)
var result = methodDelegate((HogeController)Activator.CreateInstance(type), 10, 20);
あら素敵。素敵ではあるのですが、面白いだけで今回では使い道はなさそうです、Activator.CreateInstance消せてないし、HogeControllerにキャストって、ほげほげですよ。
というわけで、ちょっと込み入った生成をしたい場合はExpressionTreeの出番です。new生成まで内包したものを、以下のように捏ね捏ねしましょう。
// インスタンスへのnewを行う部分まで生成する、つまり以下の様なラムダを作る
// (x, y) => new HogeController().HugaAction(x, y)
var x = Expression.Parameter(typeof(int), "x");
var y = Expression.Parameter(typeof(int), "y");
var lambda = Expression.Lambda<Func<int, int, string>>(
Expression.Call( // .HugaAction(x, y)
Expression.New(type), // new HogeController()
method,
x, y),
x, y) // (x, y) =>
.Compile();
ExpressionTreeの文法とかは3年前だか4年前だかのExpression Treeのこね方・入門編 - 動的にデリゲートを生成してリフレクションを高速化を参照してください、というわけでここでは解説はスルー。ExpressionTreeの登場はVS2008, .NET 3.5, C# 3.0から。もう5年以上前なのですねー。
さて、Compileは非常に重たい処理なので(カジュアルに呼びまくるようなコード例をたまに見ますが、マヂヤヴァイのでやめましょう)、作ったらキャッシュします。
// Compile済みのラムダ式はキャッシュするのが基本!
// 以下の様なものに詰めときゃあいいでしょう
// .NET 4以降はキャッシュ系はConcurrentDictionaryのGetOrAddに入れるだけで済んで超楽
var cache = new ConcurrentDictionary<Tuple<string, string>, Func<int, int, string>>();
ConcurrentDictionaryのKeyは場合によりけり。Typeの場合もあればStringの場合もあるし、今回のようなCotrollerName/ActionNameのペアの場合はTupleを使うと楽ちんです。Tupleは辞書のキーなどに使った場合、全値の一致を見てくれるので、適当に文字列連結して代用、なんかよりも正確で好ましい結果をもたらしてくれます。簡易的に作る場合は、私も、とても多用しています。
大文字小文字比較とかの比較まではやってくれないので、そこまでやりたければ、ちゃんとGetHashCode/Equalsを実装したクラスを作りましょう(LightNodeではそれらを実装したRequestPathというクラスを作っています)
そういえばPCLはWindows Phoneを対象に含めるとConcurrentDictionary使えなくてイラ壁なのでWindows Phoneは見なかったことにしてPCLの対象に含めないのが最善だと思います!どうせ端末出てないし!
: Delegate
ところでFunc<int, int, string>という型をLambdaに指定しているけれど、これもまた決め打ちで、動的じゃあない。んで、そもそもこの手の作る場合、メソッドの型って色々あるのね。
// メソッドの型は色々ある!
public class HogeController
{
public string HugaAction(int x, int y)
{
return (x + y).ToString();
}
public double TakoAction(string s, float f)
{
return double.Parse(s) * f;
}
}
こんな風にActionが増えたら、というか普通は増えるというか違うに決まってるだろという話なわけで、問題色々でてきちゃいます。 まずデリゲートの型が違うのでキャッシュ不可能。Func<int, int, string>のConcurrentDictionaryにFunc<string, float, double>は入りません。そして"HugaAction"や"TakoAction"という動的に来る文字列から、コンパイル時にデリゲートの型は決められない。ていうかそもそもパラメータのほうもxだのyだのって不明じゃないですかー?ゼロ引数かもしれないし10引数かもしれないし。
どーするか。型名指定ができないなら指定しなければいいじゃない。
// Expression.Lambdaに型名指定をやめ、CacheはDelegateを取ってみる
var cache = new ConcurrentDictionary<Tuple<string, string>, Delegate>();
var dynamicDelegate = cache.GetOrAdd(Tuple.Create(controllerName, actionName), _ =>
{
// パラメータはMethodInfoから動的に作る
var parameters = method.GetParameters().Select(x =>
Expression.Parameter(x.ParameterType, x.Name))
.ToArray();
return Expression.Lambda(
Expression.Call(Expression.New(type), method, parameters),
parameters).Compile();
});
やった、大解決!
// 但しDelegateのキャッシュは呼び出しがDynamicInvokeでイミナイ。
// もちろん別に速くない。
var result = dynamicDelegate.DynamicInvoke(new object[] { 10, 20 });
はい、意味ありません。全然意味ないです。Funcなんちゃらの共通型としてDelegateで統一しちゃうと、DynamicInvokeしか手がなくて、ほんとほげもげ!
Everything is object
Cacheに突っ込むためには、あらゆるメソッドシグネチャの共通項を作らなきゃあならない。でもDelegateじゃあダメ。じゃあどうするか、というと、objectですよ!なんでもobjectに詰めればいいんです!
// 解決策・最も汎用的なFuncの型を作ること
// オブジェクトの配列という引数を受け取り、オブジェクトを返す関数である
// Func<object[], object>
// これを作ることによりDynamicInvokeを回避可能!(Boxingはあるけどそこは諦める)
// このキャッシュならなんでも入りそうでしょう
var cache = new ConcurrentDictionary<string, Func<object[], object>>();
良さそうな感じ、というわけで、object[]の配列を受け取りobjectを返すデリゲートを作っていきましょう。
// 作る形をイメージしよう、以下の様な形にするのを狙う
// (object[] args) => (object)new HogeController().HugaAction((int)object[0], (int)object[1])
// 引数はオブジェクトの配列
var args = Expression.Parameter(typeof(object[]), "args");
// メソッドに渡す引数はオブジェクト配列をインデクサでアクセス+キャスト => (cast)args[index]
var parameters = method.GetParameters()
.Select((x, index) =>
Expression.Convert(
Expression.ArrayIndex(args, Expression.Constant(index)),
x.ParameterType))
.ToArray();
// あとは本体作るだけ、但し戻り値にもobjectでキャストを忘れず
var lambda = Expression.Lambda<Func<object[], object>>(
Expression.Convert(
Expression.Call(Expression.New(type), method, parameters),
typeof(object)),
args).Compile();
// これでふつーのInvokeで呼び出せるように!
var result = lambda.Invoke(new object[] { 1, 10 });
これは完璧!若干Boxingが気になりますが、そこは贅沢は敵ってものです。というか、実際はパラメータ作る前工程の都合でobjectになってたりするので、そこのコストはかかってないと考えて構わない話だったりします。ちなみにvoidなメソッドに関しては、戻り値だけちょっと弄った別デリゲートを作ればOKですことよろし。その辺の小さな分岐程度は管理するが吉。
Task & Task<T>
Extra Stage。今どきのフレームワークはTaskへの対応があるのが当たり前です。(いい加減ASP.NET MVCもフィルターのTask対応してください、LightNodeのフィルターは当然対応してるよ!)。というわけでTaskへの対応も考えていきましょう。
TaskだってobjectなのだからobjectでOK!ではないです。Taskなメソッドに求めることって、awaitすることなのです。Taskで受け取って、awaitするまでが、戻り値を取り出す工程に含まれる。
var result = new Hoge().HugaAsync();
await result; // ここで実行完了される、的なイメージ
といってもTaskは簡単です、ようは戻り値をTaskに変えればいいだけで、↑のと変わらないです。Func<object[], Task>ということです。Taskに変えるといっても、元から戻り値がTaskのもののデリゲートを作るわけですから、ようするところObjectへのConvertを省くだけ。簡単。
// もちろん、実際にはキャッシュしてね
var lambda = Expression.Lambda<Func<object[], Task>>(
Expression.Call(Expression.New(type), method, parameters),
args).Compile();
var task = lambda.Invoke(new object[] { 1, 10 }); // 戻り値はTask
await task; // 待機
そう、Taskはいいんです、Taskは。簡単です。でもTask<T>が問題。というかTが。Tってなんだよ、という話になる。例によってTは静的に決まらないのですねえ……。こいつはストレートには解決できま、せん。
少しだけ周りっくどい道を通ります。まず、メソッド呼び出しのためのデリゲートはTaskと共通にします。Task<T>はTaskでもあるから、これはそのままで大丈夫。で、取り出せるTaskから、更にTを取り出します。どういうこっちゃ?というと、.Resultですよ、Result!
Task task = new Hoge().HugaAsync(); // Taskとして受け取る
await task; // ここで実行完了される
var result = ((Task<T>)task).Result; // ↑で実行完了しているので、Resultで取り出せる
こういう風なコードが作れればOK。イメージつきました?
// (Task task) => (object)((Task<>).Result)
// キャッシュする際は、キーはTypeでTを取って、ValueにFunc<Task, object>が省エネ
var taskParameter = Expression.Parameter(typeof(Task), "task");
var extractor = Expression.Lambda<Func<Task, object>>(
Expression.Convert(
Expression.Property(
Expression.Convert(taskParameter, method.ReturnType), // method.ReturnType = Task<T>
"Result"),
typeof(object)),
taskParameter).Compile();
// これで以下のように値が取れる!
await task;
var result = extractor(task);
ここまでやれば、非同期にも対応した、モダンな俺々フレームワーク基盤が作れるってものです。
C# is not LightWeight in Meta Programming
そんなわけでこれらの手法は、特にOwinで俺々フレームワークを作る時などは覚えておくと良いかもです。そして、定形高速化パターンと書いたように、この手法は別に全然珍しくなくて、実のところASP.NET MVCやASP.NET Web APIの中身はこれやってます。ほとんど全くこのとーりです。(べ、べつに中身見る前からコード書いてたしその後に答え合わせしただけなんだから!!!)。まぁ、このぐらいやるのが最低水準ってことですね。
で、しかし、簡単ではありません。ExpressionTreeの取り扱いとか、ただのパターンなので慣れてしまえばそう難しくもないのですけれど、しかし、簡単とはいいません。この辺がね、C#ってメタプログラミングにおいてLightWeightじゃないよねっ、ていう事実。最初の例のようにActivator.CreateInstanceで済ませたり、dynamicだけで済む範囲ならそうでもないんですが、そこを超えてやろうとするとねーっ、ていう。
ExpressionTreeの登場のお陰で、今どきだとIL弄りの必要はほぼほぼなく(とはいえゼロじゃあないですけどね)、楽になったとはいえ、もっと、もっとじゃもん、と思わないこともない。そこでRoslynなら文字列でソースコードベタベタ書いてEvalでDelegateが生成できて楽ちんぽん!な未来は間違いなくありそうです。ですがまぁ、あと1~2年であったり、あとPortable Class Libraryに落ちてくるのはいつかな?とかっていった事情を鑑みる、まだまだExpressionTree弄りスキルの重要性は落ちなさそうなので、学ぶならイマノウチ!損はしません!
メタプログラミング.NETは、満遍なく手法が紹介された良書だと思いますので、読んだことない人は是非是非読むといいかと思います。参考資料へのリンクが充実してたりする(ILのOpCodeとか)のも嬉しい点でした。まとめ
ExpresionTreeは優秀なIL Builder(Code as Dataとしての側面は残念ながらあまり活用できそうにもないですが、こちらの側面で活躍しているのでいいじゃないですか、いいの、か???)なんでも生成頑張ればいいってものではなく頻度によりけり。頻度少なけりゃDynamicで全然OK。ん……?
ちゃぶ台返しますと、ウェブのリクエスト処理的な、リクエストの度に1回しか呼ばれない場合だと、別にここがDynamicInvokeで実行されるかどーかなんて、ハイパー誤差範囲なんですねぇ、実は!クライアントアプリとか、O/R Mapperとかシリアライザとか、テンプレートエンジンのプロパティ評価とか、凄く呼び出されまくるとかじゃなければ、割とどうでもいい範囲になってしまいます。0.01msが0.001msになって10倍高速!なのは事実ですが、そもそもウェブは1回叩いて10msかかったりするわけで誤差範囲としか言い様がない次元になってしまう。
でも、ちゃんと頑張ったほうが格好はつくので、頑張ってみると良いです、みんなもやってるし!こんなのチキンレースですから。LightNodeの速さの秘訣はこれだけ、ではないですが、大事な一端には変わりないです。
C# ASP.NETのRESTフレームワークパフォーマンス比較大全
- 2014-01-14
LightNode(という私の作ってるOwinで動くMicro REST Framework)の0.2出しました。でも皆さんあんま興味ないと思うので(!)、先にベンチマークの話をしましょふ。0.1を出した時にもグラフを出したのですが、よく見るまでもなく詐欺グラフで非常に良くなかったので載せ直し&NancyとかWCF RESTとか他のフレームワークも追加しました。そして今回測りなおしてみると、そもそも前回のものは致命的に計測のための環境作りにミスッていたので、まるっきりナシでした、すびばせん。
パフォーマンステスト
各ソースコードはLightNode/Performanceに置いてあるので再現できます。数字はrequest per secondで、Apache Benchで叩いているだけです。実行環境は「Windows 8.1/CPU Core i7-3770K(3.5GHz)/Memory 32GB」という、私の開発環境のデスクトップPC上で動かしてます。ホスト先はIIS ExpressじゃなくてローカルIIS。
オレンジと緑はふつーのIISでホストしてるもの。グレーはちょっと別枠ということで色分けてます。
結果に納得いきます?イメージとちょっと違う結果に?まず、WCFのRestが一番遅いです。これはどうでもいいですはい。次点がASP.NET MVC、そこからちょっと僅差でASP.NET Web API。これはJSONシリアライザの問題もあるかなってところ(MVCのデフォルトシリアライザはJavaScriptSerializerで、あれは遅い)かどうかは知りませんけれど、ともかくWeb APIは言われるほど遅い遅いなんてことはない、ってとこでしょーか。安心して使っていきましょう。そこから別に大きく差がついてってことなくNancy、ちゃんと定評通りにLightweightで速そうです、偉い。ServiceStackは、一応謳い文句どおりちゃんとFastestでした。
生OwinHandler(Async)とHttpTaskAsyncHandlerはほとんど変わらない。といったところから、Owin(Katana)でラップされることは、そんなにパフォーマンスロスは発生しない、と考えられるでしょう。この事実はかなり安心できて嬉しい。で、LightNodeがそれらよりもグッと高速。そう、LightNodeはちゃんと最速フレームワークなのです。で、しかし生OwinHandler(Sync)と差がちょっと付いちゃってるので、ここはもう少し縮められないかなあ、といったところ(実際のところ、LightNodeのデフォルトは出力をバッファするようになってて、そのオプションをオフにすればもっと迫れますが、実用的にはオンにせざるを得ないので、ここはオン時で)。そして最速はSyncのHttpHandler。生ハンドラ強い。
SelfHostとHeliosはちょっと別枠。SelfHostは一節によるとSloooooooooowって話もあったのですけれど、手元でやると普通に速いのですよねえ。そのSloooowってコード見たら、なんかそもそも他のテストと出力物が全然違ったので、その人のミスなのかなぁ?って思ってますがどうなのでしょうね。ともあれ、私の環境でやってみた限りではSelfHostはかなり速いです。
Helios(Microsoft.Owin.Host.IIS)は、System.Webを通さずにIISネイティブをペシペシ叩くことで超最速を引き出すとかいう代物で、まだ0.1.2-preでプロダクション環境に使える代物ではないとはいえ、とにかく速い。すぎょい。これだけ違うとニヨニヨしちゃうねぇ。
Sync vs Async
ベンチ結果の突っ込みどころは二点ほどあるかな、と。ひとつはHttpHandler(Async)がHttpHandler(Sync)に比べて険しく遅いこと。もう一つは、RawOwinHandler(ASync)とRawOwinHandler(Sync)ってなんだよハゲ、と。RawOwinHandlerはこんなコードになっています。
app.Run(context =>
{
// 中略
if (context.Request.Query.Get("sync") == "true")
{
context.Response.Body.Write(enc, 0, enc.Length);
return EmptyTask; // Task.FromResult<object>(null)
}
else
{
return context.Response.Body.WriteAsync(enc, 0, enc.Length);
}
}
違いはWriteしてるかWriteAsyncしてるか。で、これだけで1000rpsも変わってしまうんですねえ、お、おぅ……。HttpHandler(Async)とHttpHandler(Sync)も同じ話です。これねえ、困った話です。しかも、Heliosだと遅くならなかったりするので、原因はSystem.Webのネットワークストリームに対するWriteAsyncに何らかの欠陥があるのかなあ、と。それ以外にとりあえず考えつくのは、そもそもレスポンスサイズが小さいとかlocalhost同士だから、とかなくもないので(所詮マイクロベンチですから)、厳密にどーこうというのは言いづらくてまだ要調査ってところ。でもHeliosだと遅くならなかったりするのでもうHeliosでいいよ(投げやり)
まぁネットワーク離したりサイズ大きくしたりとかは、そのうちやりませうか。そのうち。多分やらない(面倒くさいの!)
ベンチ実行環境の注意
Windows Defenderのリアルタイム保護が有効ならば、無効にしましょう。これ、有効か無効かでめちゃくちゃ結果変わります。3000rpsぐらい変わるしHeliosにしても別に大して差が出ないとか、もう根源的に結果が変わります。Defenderがオンの時に測った結果とかクソの役にも立たないゴミデータなので投げ捨てましょう。ネットにある計測しました、とかってのもそれの可能性があるので見なかったことにしましょう。ファイアウォールとかもとりあえず切っといたほうがいいんじゃないでしょーか。
IISかIIS Expressかは、傾向としてそこまで大きく違うってこともないですけれど、IISのほうが成績は良好なので、一応測るのだったらIISでやったほうが良いかと思います。以前にやってた時は横着してIIS Expressでやってたのですけれど、反省ということで。そこまでやるならWindows Serverでー、とか無限に要求は加速しますが、フレームワーク同士の相対的な比較なので、そこまでやる必要はないかな?
LightNode 0.2
ここからタイトル詐欺の抱合せ商法。じゃなくて、最初はこっち本題で書いてたんですが思ったよりパフォーマンス比較が厚くなったので上に持ってきただけなんですよ……。ということでLightNode 0.2出しました。LightNode自体は0.1の解説LightNode - Owinで構築するMicro RPC/REST Frameworkを読んで欲しいのですけれど、0.1はOwinへの理解度が足りなかった成果、安定性に難があったり、細部が詰められてなかったりしました。けれど、今回はかなり完成度上がってます。このバージョンからは普通に投下しちゃって問題ないレベルに達しているかな、と。ベンチマークを細かく取って検証しているとおり、パフォーマンスについてもよりシビアに詰めています。
変更点は割といっぱいあります。
Enumバインディングの高速化
Enumパースの厳密化
T4クライアントコード生成内容の変更、アセンブリロック回避
ContentFormatterのコンストラクタ修正
IContentFormatterにEncodingインターフェイス
Extensionを|区切りで受け付けるように
void/Task時は204を返す
ReturnStatusCodeExceptionを投げることで任意のステータスコードで返せる
Optionでstringはデフォルトではnull非許可に
IgnoreOperationAttribute追加
フィルター追加
こんなとこで。あとGitHub Pages -LightNode立てたのでトップページがちょっとオサレに。
Enum高速化・判定厳密化
C#のEnum自体は凄く好きなんですよ、プリミティブとほとんど変わらない、という、それがいい。Javaみたいにゴテゴテついてると逆に使い勝手悪かったりパフォーマンス上の問題で使われない(Android!)とかって羽目になってたりしますし、これはこれで良いかな、って思ってます。拡張メソッドによってちょっとしたメソッドは足すことが可能になりましたしね。ただ、静的メソッドが頂けない。リフレクションの塊なので速くない、なんか色々使い勝手悪い、などなどビミョー感半端ない。
しょうがないので、LightNodeではEnum専用のインフラ層を構築して回避しました。高速化しただけじゃなくて、値の判定を厳密化しています。Enumの値が1,10,100の時に5を突っ込んだらダメ、って感じです。更にビットフラグに対しても厳密な判定がされるように加えているので([Flags]属性がついてるかどうかを見ます)、1,2,4,8の時に100が来たら死亡、7ならOK、って感じですにぇ。ASP.NET MVCなどでも、Enumでゆるふわな値が渡ってくるのはかなり嫌だったので、良いんじゃないかと思います。他、デフォルトでstringもnull非許可、配列の場合は空配列になるので、デフォではnullや範囲外の値というのは完全排除しています。
なお、このEnumインフラストラクチャは、後日、もう少し機能を追加して専用ライブラリとして切り出そうと思っています。使い道はかなり多いんじゃないかなー、と思いますのでお楽しみに。
例外によるステータスコード変更
ASP.NET Web API 2 で追加された機能について見てて、いいですよねー、ということで。実際、戻り値の型をシンプルなオブジェクトで指定した場合って、例外でグローバルに吹っ飛ばすしか手段ないですしね。
public class Hoge : LightNodeContract
{
public int HugaHuga()
{
throw new ReturnStatusCodeException(HttpStatusCode.NotImplemented);
}
}
単純明快でいいと思います。IHttpActionResultなんて作りゃあそりゃ最大の柔軟性ですがResponseType属性とかは、まぁ、やっぱあんまりだな、って思いますよ、ほんと……。
さて、実際のとこWeb APIはやっぱり一番参考にしていて、機能眺めながら、どうするか考えてます。ルーティングや認証は他のMiddlewareがやればいい、Request Batchingは入れたい、ODataはOData自体がイラネ、フィルターオーバライドはうーん?とか。色々。色々。
Middleware vs Filter
そして、フィルター入れました。最初から当然入れる気ではあったのですが実装時間的に0.1では間に合わずで。まぁ、あと、0.1の時点ではミドルウェアパイプラインとフィルターパイプラインの違いを言語化出来なかったり、実装方法というかインターフェイスの提供方法についても全然考えが固まっていなかったので、無理ではあった。今はそれらはしっかり固まってます。
というわけで、こんなインターフェイス。
public class SampleFilterAttribute : LightNodeFilterAttribute
{
public override async Task Invoke(OperationContext operationContext, Func<Task> next)
{
try
{
// OnBeforeAction
await next(); // next filter or operation handler
// OnAfterAction
}
catch
{
// OnExeception
}
finally
{
// OnFinally
}
}
}
ASP.NET MVCとかの提供するOnActionExecuting/Executedとか、アレ、私は「大嫌い」でした。挙動が不明だから。Resultに突っ込むと何が起こるの?Executingで投げた例外はExecutedに届くの?(届かない)、などなど、分かりやすいとは言い難くて、LightNodeではその方式は採用したくなかった。
かわりにシンプルなパイプラインを採用しています。OWINのInvokeパイプラインとほぼ同等の。
// app.Use
Func<IOwinContext, Func<Task>, Task> handler
// LightNode Filter
Func<OperationContext, Func<Task>, Task> invoke
// インターフェイスでは
public abstract Task Invoke(OperationContext operationContext, Func<Task> next);
というか一緒です。見た目もやってることも一緒なミドルウェアとフィルターですが、違いは当然幾つかあります。第一に、ミドルウェアだとほとんどグローバルに適用されますが、フィルターはグローバル・クラス・メソッド単位の3つが選べます。そして、最大の違いは実行コンテキストを知っていること。フィルターが実行されるのはパラメータバインディングの後なので、実行されるメソッドが何かを知っています。どのAttributeが適用されているかを知っています。ここが、大きな違い。
フィルタはGlobal/Contract/Operation単位で設定可能ですが、順番は全て設定されたOrderに従います。AuthenticationとかActionとかの順序パイプラインはなし。だって、あの細かい分け方、必要です?認証先にしたけりゃOrderを-int.MaxValueにでもすりゃあいいんです。というわけで、そのへんは取っ払って、Orderだけ、ただしOrderのレンジは-int.MaxValueからint.MaxValueまで、かつデフォルトはint.MaxValue(一番最後に発火される)。です。ASP.NET MVCのデフォが-1で最優先されるってOrderも意味不明で好きじゃないんですよねえ……。
OperationContextはIsAttributeDefinedなど、属性のチェックや取り出しが容易になるメソッドが幾つか用意されてますにゃ。もし実行をキャンセルしたければ、nextを呼ばなければいい。その上で大きく結果を変えたければ、Owin Environmentを弄れば好きなようにStatusCodeでもResponseでもなんでも設定できます。十分十二分。
まとめ
パフォーマンスは、まぁ自分で測らないと納得しにゃいところもきっとあると思いますので自分で測るのが一番いーですね、そりゃそうかそりゃそうだ。生ハンドラに比べるとロスは少なくないなぁというのは現実かもしれませんねぇ(LightNodeは除く)。Owinを被せることのロスは少なめってのが確認できたのは安心できて良いですね、さぁ積極的に使っていきましょう。
LightNodeは超簡単。超速い。十分なカスタマイズ性。というわけで、真面目に実用的です!使うべき!さぁ今すぐ!あと、意図的に既存のフレームワークの常識と崩しているところも含めて、全ての挙動の裏には多くの考えが含まれています。全ての挙動は明確に選択しています。そーいうのも読み取ってもらえると嬉しいですね。
また、ちょっとした縁があって2014/2/8に北海道のCLR/Hにてセッションを一つ持ちます。北海道!あんまり東京から出ない私なのですが、こないだは大阪に行きましたし、ちょっとだけたまにはお外にも出ていこうかな、なんて思っていなくもないです。北陸とかにもそのうち行きたいですね、色々なのと重ならなければ……。
セッションタイトルは「LightNode Demystified - How to Make Extreme Fast Owin Framework」を予定しています。ネタは色々あるんですが、LINQはこないだ大阪でやったし、Real World Hyper Performance ASP.NET Architecture(Internal謎社)は、もう少し後でいいかなぁ(?)だし、前2つがうぇぶけー(Windows Azure 最新アップデート/最新Web アプリケーションパターンと .NET)なので、合わせて見るのが良いかにゃ、と。
LightNode Demystified、ですけれど、LightNodeを作ることを通してOWINとはどのようなものなか、どのような未来が開けるのか、というのを伝えられれば良いと思っています。私自身、実際に作ることによる発見がかなり多かったし、作ってみないと分からないことというのはかなり多いと思いますので、そうして開けた視野をシェアできれば嬉しいですね。
また、1/17には弊社で開催するめとべや東京#3にてLTでデモをするので、そちらのほうも都合がつく方は是非是非どうぞ。
OWINのパイプラインとMiddleware作成ガイド
- 2014-01-06
あけおめました。振り返る~系の記事はこっ恥ずかしいのでいつまでも先頭に出ていると嫌なので、割と流したくてshoganaiので、記事をでっち上げます。実際切実。記事あげてる場合じゃなくても、これはこれでsetsujitsuなので許してあげてほしいのね。
Node.jsでKoaというフレームワークが盛り上がっているらすぃ。で、新しいWebフレームワーク Koa についてを見てて、あー、まんまKatana - Microsoft.Owinで置き換えられるなぁと思ったので、書いてみました。
public class Startup
{
public void Configuration(IAppBuilder app)
{
// KoaとOwinを比較して
// http://blog.kazupon.jp/post/71041135220/koa
// 3. Response Middleware
app.Use(async (context, next) =>
{
Console.WriteLine(">> one");
await next();
Console.WriteLine("<< one");
});
app.Use(async (context, next) =>
{
Console.WriteLine(">> two");
await context.Response.WriteAsync("two");
await next();
Console.WriteLine("<< two");
});
app.Use(async (context, next) =>
{
Console.WriteLine(">> three");
await next();
Console.WriteLine("<< three");
});
}
}
比較すると、そっくりそのまま。Koaはフレームワークというか小さなツールキット、Connect/Koa = Katana, Express = ASP.NET MVC/Web API、みたいな図式で捉えればいいのでしょうね。
OWINの成り立ちについては、OWIN - Open Web Interface for .NET を使うで解説されていますが、Ruby- Rack/Python - WSGI/Perl- PSGIと同じようなものと捉えられます。OWINとKatanaに関しては、PerlのPSGIとPlackの関係性を見れば、そのまま当てはめることが可能です。
よって、OWINの基本的なことは、OWIN関連を漁るよりも、Plack Handbookを読んだほうがピッと理解できそうです。GitHubのリポジトリには日本語の生Markdown原稿もあるので、目を通しておくと、理解がとっても進みます。
Pluggable Pipe Dream
OWINがASP.NETにもたらしたものは2つ。一つはバックエンドの自由、IISでもSelfHostでもー、という側面。もう一つはプラガブルなMiddleware。そしてこれは、パイプラインになっているのですね、こちらのほうが開発者にとって注目に値する、影響力の大きなものです。
最初の例で書いた app.Use(async (context, next) => はMicrosoft.Owinによるもので、ラムダ式でその場でMiddlewareを定義していることに等しい(AnonymousMiddleware!)わけですが、まずはこっちから書いてったほうが、わかりやすいかな、と。(ちなみにRunはnextのないバージョン、つまりMiddlewareの終点)
app.Use(async (context, next) =>
{
try
{
// 実行前の処理が書ける
await next(); // 次のミドルウェアの実行(これを呼ばないことでパイプラインのキャンセルも可能)
// 正常実行後の処理が書ける
}
catch
{
// 例外時の処理が書ける
}
finally
{
// 後処理が書ける
}
});
こういったパイプラインは.NETには偏在しています。HttpClientのDelegatingHandler - HttpClient詳解、或いは非同期の落とし穴についてや、LINQ to Objects - An Internal of LINQ to Objectsの中身と変わらない話です、特にDelegatingHandlerは近いイメージ持ってもらうと良いかな、と。図にすればこんな感じ。
next()を呼び出すことで、円の中央、次のパイプラインに進む。きっと、一番最後、中心のMiddlewareはフレームワークとしての役割を担うでしょう(ResponseStreamに書いたりなど処理をかなり進めてしまうので、フレームワークが後続のMiddleware読んでも無意味というか逆に死んだりするので、フレームワーク部分では意図的にnext呼ばない方がいい←だから実際app.RunもNext呼ばないしね)、とはいえ構造上ではFrameworkとMiddlewareに別に区別はないです。処理が終わったら、今度は円の内側から外側に向かって処理が進んでいきます。Nextを呼ばなければ、途中で終了。1-2-3-4-5-4-3-2-1を、3で止めれば、1-2-3-2-1になる、といった感じです。これはASP.NET MVCのfilterでResultに小細工したり、Exceptionを投げたりして中断するようなものです。
HttpModuleだってHTTPパイプラインぢゃーん、というツッコミもあっていいですけれど、それよりもずっと単純明快な仕組みだというのがとても良いところ。こういった薄さであったり単純さであったり、をひっくるめたLightweightさって、とっても大事です。
Mapping
LightNode - Owinで構築するMicro RPC/REST FrameworkではURLは決め打ち!と言いましたが、むしろそもそも、URLのルーティングはLightNodeが面倒見るものではなくて、他のMiddlewareが面倒見るものなのです。例えば、APIのバージョン管理でv1とv2とで分けたい、というケースがあったとしましょう。その場合、MapWhen(Katanaに定義されてます)を使うと、条件指定で利用するMiddlewareのスタックを弄ることができます。
// Conditional Use
app.MapWhen(x => x.Request.Path.Value.StartsWith("/v1/"), ap =>
{
// Trim Version Path
ap.Use((context, next) =>
{
context.Request.Path = new Microsoft.Owin.PathString(
Regex.Replace(context.Request.Path.Value, @"^/v[1-9]/", "/"));
return next();
});
ap.UseLightNode(new LightNodeOptions(AcceptVerbs.Post, new JsonNetContentFormatter()),
typeof(v1Contract).Assembly);
});
app.MapWhen(x => x.Request.Path.Value.StartsWith("/v2/"), ap =>
{
// 手抜きなのでコピペ:)
ap.Use((context, next) =>
{
context.Request.Path = new Microsoft.Owin.PathString(
Regex.Replace(context.Request.Path.Value, @"^/v[1-9]/", "/"));
return next();
});
ap.UseLightNode(new LightNodeOptions(AcceptVerbs.Post, new JsonNetContentFormatter()),
typeof(v2Contract).Assembly);
});
LightNodeはサービスの記述された読み込むアセンブリを指定できるので、v1用アセンブリとv2用アセンブリを分けて貰って、Request.Pathを書き換えてもらえれば(/v1/部分の除去)動きます。これは単純な例ですが、複雑なルーティングだって頑張れば出来るでしょう。きっと。
OWINにはSuperscribeというグラフベースルーティング(何だそりゃ)とかもありますし、そういうのと組み合わせれば、実際LightNodeでうまく使えるかどうかは知りませんが、まぁ、そういうことです。やりたければ外側で好きにやればいいのです。プラガブル!
Headerが送信されるタイミング
話は突然変わって、Middleware実装上のお話。表題のことなのですけれど、原則的には「最初にWriteされた時」です。原則的には、ね。最初にFlushされた時かもしれないし、そもそもされないかもしれないこともあるかもですが、とはいえ原則的には最初にWriteされた時です。どーいうことか、というと
app.Run(async (context) =>
{
try
{
// Writeしてから
await context.Response.WriteAsync("hogehoge");
context.Response.Body.Flush();
// StatusCodeやHeaderを書き換えると
context.Response.StatusCode = 404;
}
catch (Exception ex)
{
// 例外出る
Debug.WriteLine(ex.ToString());
}
});
これをMicrosoft.Owin.Host.SystemWebでホストすると、「HTTP ヘッダーの送信後は、サーバーで状態を設定できません。」というお馴染みのような例外を喰らいます。ちなみにHttpListenerによるSelfHostでは無反応という、裏側のホストするものによって挙動は若干違うのだけは注意。とはいえ、どちらも共通して、ヘッダーが送信された後には幾らStatusCodeやHeaderを書き換えても無意味です。上の例だと、404にならないで絶対200になっちゃうとか、そういう。
当たり前といえば当たり前なのですが、生OWIN、生Katanaだけで色々構築すると、Middlewareの順序によっては、そーなってしまうことも起きてしまいがちかもしれません。
なお、Katanaのソースコード読む時はHttpListenerのほうを中心に追ったほうが分かりやすいですね。System.Webのほうは、つなぎ込みがややこしかったり、すぐにブラックボックスに行っちゃったりで読みにくいので。若干の挙動の差異はあるとはいえ、概ね流れや処理は同じですから、まずは読みやすいほう見たほうが楽でしょう。
バッファリングミドルウェア
さて、そんな、Writeが前後して厄介というのを防ぐためのMiddlewareを作ってみましょう。解決策は単純で、上流のパイプラインでバッファリングしてやればいいわけです。
app.Use(async (context, next) =>
{
var originalStream = context.Response.Body;
using (var bufferStream = new MemoryStream())
{
context.Response.Body = bufferStream; // 差し替えて
await next(); // 実行させて
context.Response.Body = originalStream; // 戻す
if (context.Response.StatusCode != 204) // NoContents
{
context.Response.ContentLength = bufferStream.Length;
bufferStream.Position = 0;
await bufferStream.CopyToAsync(originalStream); // で、コピー
}
}
});
単純簡単ですね!そう、Middlewareとか別にあんまり構える必要はなくて、Global.asax.csに書いていたのと同じようなノリでちょろちょろっと書いてやればいいわけです。そして、それが膨らみ始めたり、汎用的に切り離せそうだったら、独立したMiddlewareのクラスを立ててやれば再利用可能。これはIHttpModuleを作るのと同じ話ですけれど、Middlewareは、それよりもずっとカジュアルに作れます。
さて、上のコード、しかしこれだとMemoryStreamが中で使ってる奴にCloseされちゃったりするとCopyToAsyncでコケてしまいます。いや、誰がCloseするんだ?という話はありますが、でも、例えばStreamWriterを使って、usingして囲んでStreamに書いたりすると、内包するStreamまでCloseされちゃうんですねぇ。
usingしないように注意する、というのも、パイプラインに続くMiddleware全てで保証なんて出来ないので、ここもまた上流で防いでやるのがいいでしょう。LightNodeではUnclosableStream.csというものでラップしています。どういうものかというと
internal class UnclosableStream : Stream
{
readonly Stream baseStream;
public UnclosableStream(Stream baseStream)
{
if (baseStream == null) throw new ArgumentNullException("baseStream");
this.baseStream = baseStream;
}
// 以下ひたすらStreamを移譲
// そしてCloseとDisposeは空白
public override void Close()
{
}
protected override void Dispose(bool disposing)
{
}
}
という単純なもの。これを、ついでに独立したMiddlewareにしてみますか、すると、
public class BufferingMiddleware : Microsoft.Owin.OwinMiddleware
{
public BufferingMiddleware(OwinMiddleware next)
: base(next)
{
}
public override async Task Invoke(Microsoft.Owin.IOwinContext context)
{
var originalStream = context.Response.Body;
using (var bufferStream = new MemoryStream())
{
context.Response.Body = new UnclosableStream(bufferStream); // Unclosableにラップする
await this.Next.Invoke(context); // Microsoft.Owin.OwinMiddleware使うとthis.Nextが次のMiddleware
context.Response.Body = originalStream;
if (context.Response.StatusCode != 204)
{
context.Response.ContentLength = bufferStream.Length;
bufferStream.Position = 0;
await bufferStream.CopyToAsync(originalStream);
}
}
}
}
フレームワークレベルのものを作る時は、このレベルまで気を使ってあげたほうが間違いなくいいかと思います。
Owin vs Microsoft.Owin
Middleware作るのにMicrosoft.Owin.OwinMiddlewareを実装する必要はありません、InvokeとTaskと、などなどといったシグネチャさえあってればOKです。同様にIOwinContextはKatanaで定義してあるものであり、Owin自体はIDictionary<string, object>が本体です。
Katana(Microsoft.Owin)は便利メソッドの集合体です。Dictionaryから文字列Keyで引っ張ってResponseStream取り出すより、context.Response.WriteAsyncと書けたほうが当然楽でしょふ。他にも、Cookieだったりヘッダだったり、Middlewareの定義用ベースクラスだったり、などの基本的な、基本的な面倒事を全てやってくれる薄いツールキットがKatanaです。冒頭の、Node.jsのKoaみたいなものであり、PerlのPlackに相当するようなもの、と捉えればいいんじゃないでしょーか。
LightNodeはMicrosoft.Owinを参照していません。これは、依存性を最小限に抑えたかったからです。その分だけ、面倒事もあるので、楽したかったり社内用Middlewareを少し作るぐらいだったら、Katana使っちゃっていいと思いますですね。リファレンス実装、でありますが、どうせ事実上の標準として収まるでしょうし。フレームワークレベルでがっつし作ってみたいという時に、依存するかしないか、どちらを選ぶかは、まぁお好みで。依存したって全然構わないし、依存しないようにするのもそれはそれでアリだと思いますし。
HTMLを書き換えるMiddlewareを作る
というわけで、応用編行くよー。mayuki先生の作られているCarteletというHTMLパーサー/フィルターライブラリがあるのですが(某謎社で使われているらしいですよ)、それをOwinに適用してみましょう。Carteletのできることは
HTMLのそれなりに高速でそれなりなパース 出力時にCSSセレクターで要素に対してマッチして属性や出力フィルター処理 フィルターしない部分は極力非破壊 ASP.NET MVCのViewEngine対応 CSSのstyle属性への展開 (Cartelet.StylesheetExpander)
だそうです。
例として、class="center"という属性を、style="text-align:center"に展開するというショッパイ決め打ちな例を作ってみます。こんなMiddlewareを作ります。
// Cartelet Filter Middleware
app.Use(async (context, next) =>
{
var originalStream = context.Response.Body;
using (var bufferStream = new MemoryStream())
{
context.Response.Body = bufferStream; // 差し替えて
await next(); // 実行させて
context.Response.Body = originalStream; // 戻す
if (context.Response.StatusCode != 204) // NoContents
{
// Carteletによるフィルタリングもげもげ
var content = Encoding.UTF8.GetString(bufferStream.ToArray());
var htmlFilter = new HtmlFilter();
htmlFilter.AddHandler(".center", (ctx, nodeInfo) =>
{
nodeInfo.Attributes.Remove("class");
nodeInfo.Attributes["style"] = "text-align:center";
return true;
});
var node = HtmlParser.Parse(content);
var sw = new StreamWriter(context.Response.Body); // usingしない、stream閉じないために(leaveOpenオプションもあるのでそちらのほうが望ましいけど横着した)
var cartelet = new CarteletContext(content, sw);
htmlFilter.Execute(cartelet, node);
await sw.FlushAsync(); // usingしない時はFlushも忘れないように。。。
}
}
});
Carteletの受け取るのがStringなので、全パイプラインが完了するまではバッファリングします。で、それで手に入れたStringをCarteletに流し込んで、本来のBodyに流し込む。(Content-Lengthの設定を省いてるので直に流し込んでますが、設定が必要なら再再バッファリングががが、まぁ、どうせ更に上流でgzipとかするだろうから、ここでContent-Length入れる必要はあんまにゃいかな!)
実際に結果を見てみると、
// これを実行すると
app.Run(async context =>
{
context.Response.StatusCode = 200;
context.Response.ContentType = "text/html;charset=utf-8";
await context.Response.WriteAsync(@"
<html><body>
<div class=""center"">
ほげほげ!
</div>
</body></html>");
});
<html><body>
<div style="text-align:center">
ほげほげ!
</div>
</body></html>
というHTMLが出力されます。へーへーへー。色々応用効きそうですね!
OWINはパイプラインの夢を見るか?
色々出来る、しかも色々簡単!素晴らしい素晴らしい!プラガブル!はたして本当に?実際、フレームワーク書いたりミドルウェア書いたりしてると、ふつふつふつと疑問が湧いてきます。そういう時は先行事例を見ればいい、というわけでPythonのWSGIでは以下の様な話が。
すべてがプラガブルで、モノリシックなアプリケーションフレームワークを持つ理由がもはや一つもないような未来を思い描いていました。すべてライブラリ、ミドルウェア、デコレータでまかなえるからです。 悲しいことに、そんな理想的な未来はやってきませんでした。
OWINでも、一個のでっかいフレームワークを持つ必要なんてない!と言える時が来るか、というと、さすがにそれはないでしょうねえー。また、たとえ分離可能なコンポーネントであっても、フレームワークの提供するシステム(フィルターやプラグイン)から離れられるかというと、必ずしもそうではないのかな、って。
Middlewareのパイプラインは、ASP.NET MVC/Web APIとかのフィルターのパイプラインとも同じようなものです。だったらフィルターで作るよりMiddlewareで作ったほうが、フレームワークという制限から離れられて良さそう。でも、Middlewareの欠点は、後続のパイプラインのコンテキストを知らないことです。認証を入れるにしても、[AllowAnonymous]属性が適用されているかなんてしらないから、全部適用するかしないか、ぐらいにしか出来ない。filterContext.ActionDescriptorのようなもの、というのは、フレームワークの内側のシステムしか持ち得ないのです。でも、そうしてフィルターとして実装すれば、フレームワークに深く依存することになる。
そんな悩ましさを抱えつつも、それは、あんま無理せずに、コンテキスト不要なら最大限独立性の高いOwin Middlewareとして。そうでないならアプリケーションのプラグイン(フィルター)として。でいいかな、って思ってます。今のところ。何れにせよIHttpModuleなんかよりは遥かに作りやすいし、その手の話だって今に始まったことじゃあないのよね?HttpModuleだってHTTPパイプラインぢゃーん、って。はい。
2014/1/18(土)に開催されるめとべや東京#3(開催場所は謎社です)では、LTで5分でサービスAPIをOwin/LightNodeを使って作って実際にAzure Web Sitesにホストするまで、デモしようと思ってますので、OWIN知りたい、どう動かすのか見てみたい、って人もどうぞ。めととは。