kazuk は null に触れてしまった

C# / .NET 系技術ネタ縛りでお送りしております

カテゴリーアーカイブ: .NET Framework

VS2010 後の世代のプログラミング (2) Pex


という訳でいくつもの連載するよと言っといて(2)が来ないとかやってるとアレなので、Visual Studio 2010 リリース後に出てきた物が開発にどう影響を与えるかという事で、Pex を取り上げます。

Pex って何?

Pex は内部的には Z3 証明器の論理的証明に対するインプットをメソッドシグニチャ、および、CLRのトレース機構と結びつける事でプログラムの動作が証明された状態を近づける事をサポートします。
って説明で解るわけないっすよね。
要するにです、プログラムの断片にどんな入力を与えたらどういう分岐が発生して、どういう条件が発生してというのをトレース情報を元に補完しながら解析分析するという事をします。
Z3証明器の結果としてプログラムの動作パスが何種類か得られ、その何種類かの動作パスに飛び込むにはこのパラメータセットでその処理を呼べば良いという事が解る物を、単体テストの形式で出力してくれるのが Pex な訳です。
中がどう動くかという事はPexがトレースを元にして進行的に解析しますので、実際に何が起こるのか解らない物に対してこういうパラメータセットを与えればこういう結果を返すというのを調べてくれるのでレガシー化してしまい文書化されていない物に対して非常に強い側面もありますが、停止性問題により全てのケースを網羅できない事は数学的に証明されていて、中身をいじる事ができない物、完全なブラックボックスに対してはどっかしらで限界がありますのである程度リファクタリングが可能な物に対して適用するのが最も望ましい結果を生み出します。(Pexがお手上げになった部分をリファクタリングで切り出してあげるとさらなる進行解析が可能となってより網羅性のある解析が実行されます。)
能書きはさておき、Pexを実際に使ってみましょう。

Pexによる実行パス解析の最も単純な例

C# での checked 演算を例にとって、四則および単項演算の単純ロジックをテストするとします。
namespace PexTarget
{
    public class Class1
    {
        public int Add( int p1, int p2 )
        {
            checked
            {
                return p1 + p2;
            }
        }
    }
}
このAdd を右クリックし Pex –> Create Parameterlized Unit Test した結果の出力は以下です。
// <copyright file="Class1Test.cs" company="Microsoft">Copyright ゥ Microsoft 2011</copyright>
using System;
using Microsoft.Pex.Framework;
using Microsoft.Pex.Framework.Validation;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using PexTarget;

namespace PexTarget
{
    /// <summary>This class contains parameterized unit tests for Class1</summary>
    [PexClass(typeof(Class1))]
    [PexAllowedExceptionFromTypeUnderTest(typeof(InvalidOperationException))]
    [PexAllowedExceptionFromTypeUnderTest(typeof(ArgumentException), AcceptExceptionSubtypes = true)]
    [TestClass]
    public partial class Class1Test
    {
        /// <summary>Test stub for Add(Int32, Int32)</summary>
        [PexMethod]
        public int Add(
            [PexAssumeUnderTest]Class1 target,
            int p1,
            int p2
        )
        {
            int result = target.Add(p1, p2);
            return result;
            // TODO: add assertions to method Class1Test.Add(Class1, Int32, Int32)
        }
    }
}

属性を無視してみれば単純に呼び出すコードが生成されているのが解ります。
この Class1Test の Add を右クリックして Run Pex Explorations すると
imageimage
2つの動作パスが検出されそれぞれの動作パスにおいてどういう結果となるのかが解析されました。
OverflowExceptionが望まれる結果という事でAllowException し再度 Run Last Pex Exploarations するとすべてグリーンになります。
image image image
インデントがあれとかはご愛嬌って事ですが、Class1を生成し、Addに指定パラメータを与えて実行する単体テストになっています。(TestMethod属性修飾されてますよね)
結果として以下のコードが生成されます。
namespace PexTarget
{
    public partial class Class1Test
    {
[TestMethod]
[PexGeneratedBy(typeof(Class1Test))]
public void Add602()
{
    int i;
    Class1 s0 = new Class1();
    i = this.Add(s0, 0, 0);
    Assert.AreEqual<int>(0, i);
    Assert.IsNotNull((object)s0);
}
[TestMethod]
[PexGeneratedBy(typeof(Class1Test))]
[ExpectedException(typeof(OverflowException))]
public void AddThrowsOverflowException587()
{
    int i;
    Class1 s0 = new Class1();
    i = this.Add(s0, -2147409852, -1040186368);
}
    }
}
単体テストになってますのでそのまま動かしてテストが可能です。
image
では、このテストは本当に有効なのでしょうか。テスト構成のデータ収集でコードカバレッジを有効にして
image image
リビルドしカバレッジデータを取るためにすべてのテストを再実行します。
image
はい100% ですね、これ以上有効なテストは存在しないわけです。
Pexはトレースと証明器によって少なくとも追える範囲のテストを自動的に実装しますので、単純にコードを書いてPex Explorartionした結果が望ましい物であればそれを受け入れるとするだけでカバレッジレートの非常に高いテストクラスが得られます。

Pex Driven Contract based development

という訳でPexでは Explorations の結果を見て望ましい結果が出るようにコードを直していくというサイクルでコードを実装していくことができます。
では int Sum( IEnumerable<int> sequence ) を実装してみましょう。
public int Sum( IEnumerable<int> seq )
{
    var result = 0;
    foreach (var i in seq)
    {
        result += i;
    }
    return result;
}
何も考えないで書いて、Create Parametelized Unit Test します。出来上がった Parametelized Unit Test 上で Pex Explorations すると NullReferenceException を投げるケースがあるという事が解ります。
seq が null の場合ですね。ここで Code Contracts を有効にして Contract.Require を使って seq!=null を Pre condition にすると
image image
はい、Pre condition 記述されたパラメータは自動的にPexでも認識されて無効なパラメータによる呼出しとなりました。
OverflowException が飛んでないですね。確かに checked ブロックの記述を忘れています。checkedを追加してAllow Exception すると以下になります。
imageimage
という訳で、入力で通るべきすべてのパスを網羅したテストが出来上がりました。
/// <summary>
/// 合計を計算します
/// </summary>
/// <param name="seq"> integer sequence </param>
/// <returns> sum value of sequence </returns>
/// <exception cref="ArgumentNullException"> when seq is null </exception>
/// <exception cref="OverflowException"> when overflow </exception>
public int Sum( IEnumerable<int> seq )
{
    Contract.Requires<ArgumentNullException>(seq!=null);

    var result = 0;
    foreach (var i in seq)
    {
        checked
        {
            result += i;
        }
    }
    return result;
}
Explorationsの結果を見れば exception のドキュメントコメントも容易ですね。
まぁ、これで良いのかというと、呼出し側に pre condition 規制の難しい所で例外が外に飛んで行ってしまう可能性があるわけで、設計上望ましい挙動ではないかもしれません。これを Explorations から読み取る事ができるので 「TrySum を作る」という選択肢を少なくとも検討する機会が得られるのは非常に重要な事です。
ではTrySumを作るとしましょう。
さくっと軽く書きますとこうなりますね。
public bool TrySum( IEnumerable<int> seq, out int result )
{
    Contract.Requires<ArgumentNullException>(seq != null);

    result = 0;
    foreach (var i in seq)
    {
        try
        {
            checked
            {
                result += i;
            }
        }
        catch (OverflowException)
        {
            return false;
        }
    }
    return true;
}
これを Explorations するとこうなります。
image
false 時に out パラメータに無効値が出てるのが気に入らないとか思いませんか?思う人は例外安全とか失敗時挙動の健全性なんて物に敏感という訳で。
Contract.Ensures の出番ですね。false 時には out result には0 が返ると表明します。
Contract.Ensures(
    !Contract.Result<bool>()
    ? Contract.ValueAtReturn(out result) == 0 : true );
Contractのチェックオプションを Pre condition より強くしていればこのように契約違反例外が飛びます。
image
契約違反にならない様にするために tempResult変数を導入して、tempResult 上で計算したうえで、成功時だけ値を返すようにすると以下の通り。
public bool TrySum( IEnumerable<int> seq, out int result )
{
    Contract.Requires<ArgumentNullException>(seq != null);
    Contract.Ensures(
        !Contract.Result<bool>()
        ? Contract.ValueAtReturn(out result) == 0 : true );
    result = 0;
    var tempResult = 0;
    foreach (var i in seq)
    {
        try
        {
            checked
            {
                tempResult += i;
            }
        }
        catch (OverflowException)
        {
            return false;
        }
    }
    result = tempResult;
    return true;
}
image
このように Contracts を絡めるとPexは本当に威力を発揮するわけですね。
そして出来上がったテストを本当にテストコードに含める時は Explorationsから Promote すると、本体のPex class にマージされます。
image image
当然にカバレッジレートは 100 % になりました。
このように Pex とCode Contractsを組み合わせていく事でプログラムの動作は常に把握され、表明された状態に制約されて動作する事が保障され、なおかつカバレッジレートは相当に高いテストが出来上がります。テストが出来上がるだけでなく、書いたコードがどのような動作バリエーションを起こすかをPexは素早く列挙しますのでコードを書き、それが示す挙動が自分の想定通りかを簡単に確認しながら実装を進めていくことができます。

Pexが苦手とする物

Pexにはいくつか苦手とするものがあります。多くは単体テストという方法論自体が苦手とする領域でもありますが、ここで列挙してみましょう。
  1. 値域が非常に広い物、シーケンスの後方に依存する処理
  2. MEFインポート
  3. 環境値(日付時刻、ランダム値)
Pexはトレースを元に進行解析を行いますので、値域が非常に広いものを苦手とします、また、小さなデータの組み合わせを順次拡張しながらパターンを作りテストを実行してトレース結果から新たなパスを検出するのでシーケンスの後方、たとえば 3000文字以降に何かがあれば何かするとかは生成するのに非常な繰り返し回数を必要としてしまいきっと失敗します。処理を単位に分割する等で巨大なテストケースが必要とならない様にリファクタリングをする必要があるでしょう。
Pexは非常に便利なのですが、その方法論固有の問題を内在しています。しかし実際問題としてPexがお手上げになるケースは人が単体テストを書こうとしても問題となるケースである事が多いです。であれば苦労はPex(機械)にしてもらうというのは正しい選択でしょう。これらの問題にぶつかってPexがお手上げになるケースは多くの場合リファクタリング等設計を直した方が良いケースを暗示しますのでとりあえず書いたものをPexに投げ込んでみてお手上げケースは「リファクタリングした方がいいんじゃない?」というメッセージとして素直に受け取ってみる事をお勧めします。

Pex と Code Contracts と WCF と MEF

はい、いきなりWCFです。脈絡感じないかもしれませんが WCF には Service Contract, Operation Contract, Data Contract と色々と Contract が並んでいるという訳で、これらの Contract を使ってサービスのプロトコルを記述する事ができます。このサービスのプロトコルに対して Code Contracts でインターフェース契約を記述する事ができます。実装上リモートエンドポイントに対して WCF で結合するか、MEFで実装を取り込むかは「好きにすれば?」の世界であり別に自由な話です。要するに WCF のコントラクトとしての属性記述をしなかった場合にはその自由が失われるだけで、 MEF という選択肢は残ります、MEFしない場合これまでのリポジトリパターンのように自分でサービス解決をしなさいと言われるだけです。
要するに手間数が許すなら WCF プロトコル属性でのマークアップはやっとけと、何も失うものはなく、サービスをリモートにおける自由度を獲得できるぞと。
サービスを実装したら Code Contracts と Pexで高いカバレッジを持ったテストを実装する事ができます。サービスは多くの場合サービスの利用者に対してテスト可能性を提供するモックを必要とします。では、モックを使った場合に処理が失敗したケースにおいて「モックが悪い?それともモックを呼ぶ実装が悪い?」これはややこしい問題です。モックの実装が正しく契約に従っていない等は最も駄目な話でモックの正しさは重要です。しかし、「モックをテストする?」というとこれまではやってられない話でした。しかしPexによってモックのテストの実装の大よそは実現されます。Code Contracts でのインターフェース契約が整っていれば、モックを使う側の実装が悪いといえる状況に容易に持って行けるはずです。
Code Contracts でのインターフェース契約はインターフェースの設計の重要な要素です。設計だからそれを日本語で書いて脳みそで解釈するか、コードで書いて機械が解釈するかは表現と解釈機の違いでしかない、この解釈機の厳密性と精度と速度を比較したら大よそ勝負になんてならない事は自明でしょう。
「TDDで設計をテストに!」、実行できるメリットは存在する事に異存はありません、実行できるので高い厳密性があるのも異存はありません。しかしその設計を解釈して展開するときに人間の脳みそに頼るのがTDDのテストが設計書だという時の弱さです。Code Contractsは本当に機械解釈できる設計書となり、コードを書くとき、そのコードのテストを実装するときの参照メタデータとして普遍的な表現として使われ続けます。この普遍的表現の解釈機が Code Contracts であり Pexという訳です。

ルーズスキーマと dynamic、ストロングスキーマと Contracts

LLでの動的性、ルーズスキーマを取り込むのが dynamic での C#4.0の進化点と、それだけじゃないよ、VS2010のリリースにはこぼれたけどCode ContractsとPexはストロングスキーマな静的言語の厳密性の大きな進化です。
という訳で VS2010 後の世代のプログラミング (2) でした。Code Contracts / Pex が設計に厳密性や検証性をもたらした逆方向という意味で自由度を提供する物、MEFを取り上げます。
広告

Sync Framework で Web Farming


というわけで(なんで?)Sync Framework ですよ。

Sync Framework の現状バージョンは 2.1 なんですが、DBの同期とか難しい事についての話が中心になってるようで、案外みんなノーマーク?ってぐらいに国内情報が薄いですな。

Microsoft からは Web Farm Framework がIIS拡張として提供されていて、これを使って複数のWebサーバにアプリケーションの展開とかできるわけなんですが、前提としてARRが入ってたりして敷居はかなり高いです。(Azure上の Full IIS にARR乗せた上で Web Farm Framework を展開するとか激しく人柱っぽいですよね、うまくいってるって人が出てもちっとやりづらいっていうか、Azureの仕組みとどこでかちあうかと考えるとガクブル)

さて、将来にクラウドに乗せるか等はさておき、Webサーバ数台にコンテンツを展開するのは必要な事です。

プログラムの機能に関わるようなdll類を展開するのはちゃんと管理してやりたいんで勝手に同期とかしちゃまずいよなーって思うんですが、画像とかHTML、CSSとかのコンテンツはぶっちゃけてあんまり面倒見たくない。

こんな場合の策なんていうとぶっちゃけ2通りしかない。

  1. どっかに入れれば各サーバに勝手に反映されるようにする
  2. 中央ストレージにおいてそれを返す

うちの会社のCMSはどっちかというと2のアプローチなんですが、2にはキャッシュ機構をちゃんとかましてストレージ負荷をフロント負荷と切り離さないとストレージがボトルネックになりかねないという面でお手軽さはアレでございます。

1のアプローチが今回の Sync Framework を使って Web Farming するって方法。

という訳でSync Framework によってネットワークフォルダからローカルフォルダに同期をする方法。

結構大変な手順を踏む事になります。

  1. Sync Framework 2.1 をインストールする
  2. Microsoft.Synchronization v2.1 と Microsoft.Synchronization.Files v2.1 を参照設定する
  3. 以下コードを必死に書く
    image

コピペできない様に画像にしてやったぜ(ぇ

後は設定で同期元ディレクトリ、同期先ディレクトリを指定できるようにして、タスクスケジューラなりサービスプロセスなにがしで同期元、同期先に権限のあるアカウントで定期的に実行する。なんらかの異常があればそれを管理者が察知できるようにエラーレポート機構を組み込む諸々の処置をすれば良いわけですな。

セキュリティ的な注意事項

同期元はさておき、同期先の権限が厄介で、これをWebアプリケーション内のコードにすると、コンテンツフォルダに書き込み権限をつける必要が出てしまいアプリケーションが陥落した場合にアプリケーション実行ユーザーでサイトの改ざんが可能になってしまいますので権限設計は慎重に。

Sync Framework はマークしとけ

実際にWeb Farming に使うかはさておき、ファイルシステムフォルダをリモートと同期するのは比較的簡単です。同期プロバイダを書けばファイルシステム以外と同期する事もできます。

Azure等の場合にはマスタデータを持った参照用の SQL Server Compact のデータベースファイルとかをローカルストレージに同期してから使うとかすれば SQL Azure を使って課金額が跳ね上がるとか、テーブルストレージに対するクエリの制約とかに悩む必要もありません。SQL Server Compact は LINQ to SQLでのクエリで利用できますし。Synclonizeメソッドは同期の結果サマリーを返しますんで同期受け入れ用のローカルストレージに向かっての同期を定期的にかけてなんかのファイルが同期されたらアプリケーションをリスタートして新しいマスタに乗り換えてとかの制御も出来ないわけではありません。

こうすると参照用データとかは同期でローカルにとられた物を使う事が結構できますんで、本当に多数のノードの間での一貫性や整合性が要求されるデータについてそれをちゃんと制御すれば良いという事になりますね。もちろんのことですが、ローカルストレージは課金なんてありません。そのうえでトランザクションは Queue介してシーケンシャル処理が保障されちゃう仕組みに投げちゃうと一貫性の為のトランザクションもあんまり要りません。

クラウドでKVS重要と色々言われてきました、一番重要なのはローカルストレージの使いこなしと、ローカルストレージをいかに外部と同期させるかだと思います。

まとめ

http://togetter.com/li/92062

Sync 重要。

NParsec + Open XML SDK で Excel を Managed CLI Language に(1)


ちょっと、奥さん聞いてよ。

JParsec – NParsec Tutorial

の一番上の downloaded here でとったソースと、一番したのフルコード

The final parser code is as following: …をべたっと cs ファイルに張り付けて

Binary, Unary, Grammar の double を System.Linq.Expressions.Expressionにする。

plus の delegate ( double a,double b ){ return a+b; } を Expression.Add に…それぞれ Expression のビルダ関数にマップする。

Grammar p_number = Terms.OnDecimal<double> を Terms.OnDecimal<Expression>にして、 return Expression.Constant( double.Parse(s) ); にする。

それを

[TestMethod]
public void TestMethod1()
{
    var calculator = new Calculator();
    var exp = Parsers.RunParser(“10+20”, calculator.Parser, “test”);
    var lambda = System.Linq.Expressions.Expression.Lambda(exp);
    var result = lambda.Compile().DynamicInvoke();
    Console.WriteLine(result);
    Assert.AreEqual((double)30,result );
}

でドライブすると Success!

やったねってことで、ちゃんとパースして、コンパイルして実行するって事ができたわけ。

ソースはこちら

でねでね、 Excel なんですよ。

どこの会社でもある事だと思うんだけど、日本語ベースで色々ゴニョゴニョお客とネゴされてきて、エンジニアからすると「で、いったいどんな式?」って聞き返す。Excel開いて、セルに数値入れて式設定して結果見ると何だか非常識な値になってたり、DB定義と突き合わせると明らかに式で見なきゃいけない物を見てなかったり、それを組み入れるとお客との折衝内容と食い違っちゃうトカトカで突き返すと「確認してきます」そして毎回言葉尻はちろっと変わるんだけど、結局いつまでも決まらない罠にはまる。

最後には「Excel シートを渡して、式設定して返して」って言ってそれが帰ってきてから「やっと俺が仕事できる」的な状況。

そんな事って普通ですよね。

んで、「俺が仕事できる」時に「仕事したくないでゴザル」モードだったり、Excelとコピペを繰り返す事に「お刺身にタンポポ(ry」な気がして「俺はこんなことをする為にこの職業を選んだんだっけ…」って思うじゃないですか。

おまけに常時「仕事したくないでゴザル」だったりする奴や、ちっとDB使わせると「SQLとかよくわからなくて」(お前、それ半年前にも言ってなかったか、進歩してないのね…)だって感じで( to be continue…)

あぁ、Excelですよ。

Open XML SDK 2.0 for Microsoft Office

こいつで、Excel ファイルを開くことができて、ブック、シート、セルの順で中身に下りて行って CellFormula プロパティ にたどり着けば、セルに指定された式にたどり着くわけです。

やったね!ってわけで、この CellFormula を NParsec でパースして、式木相当のオブジェクトを構築して、それを元に T4 でコード生成すれば Excel の Cell 相当のプロパティを持って、設定した計算式での計算処理を持ったクラスができちゃうわけですね。

これで「仕事したくないでゴザル」に「特に仕事は無いでゴザル」って言えるじゃん。

顧客との折衝担当が Excel ファイル持ってきたら「やっと決まったのかね」とか上から的にいって「ぽちっとな」すれば良いわけですね。

素晴らしい生産性の向上!これで中々決まらなくても大丈夫だね。

いいか、「まだ決めなくても大丈夫と言ってる間は絶対に決まらないんだ、結果として最後に決まった事に矛盾が出た場合に解決する余裕まですべて食いつぶされるんだ」。

ま、そんなわけで、こんなもんが有ると良いねという題材なので作っております。& NParsec関係は情報少ないっていうか皆無な感じなんで実際の作成時にTDDする方法とか色々絡めて連載しようかねーってことでタイトルの(1)がついております。

.NET CLR2 と CLR4 の StringBuilder のパフォーマンス


以下コードにより計測

    class Program
    {
        static void Main(string[] args)
        {
            Random rnd = new Random();
            Console.WriteLine("Hit enter to start");
            Console.ReadLine();
            var stopwatch = Stopwatch.StartNew();
            for (int i = 0; i < 10000; i++)
            {
                // 64KB のテキストをランダム文字で生成
                StringBuilder sb = new StringBuilder();
                while (sb.Length < 65535)
                {
                    sb.Append('a' + rnd.Next(26));
                }
            }
            stopwatch.Stop();
            Console.WriteLine(stopwatch.ElapsedTicks);
            Console.ReadLine();
        }
    }
まずCLR2 での結果

Hit enter to start

891252599

約90秒程度で完了、その時の特にメモリ系の挙動を PerfMon でとったスクリーンショットの注目点を以下。

CLR2 – #Gen1 Collections

20101207111830

CLR2 – #Gen2 Collections

20101207111945

これが全く同一値という世代別GCにとっては悪夢のシチュエーション。

CLR2 – Large Object Heap Size

20101207113019

LOH の上下からLOHのガベージコレクションが走っている事が解る

CLR2 – % Time in GC

20101207113243

LOH の下がるタイミングで %Time in GC にピンがたっている。

続いてCLR4での結果

Hit enter to start

662106546

数値としては 134%、

パフォーマンスカウンタの差異はこんな感じ

CLR4 – #Gen 1 Collections

20101207114226

CLR4 – #Gen 2 Collections

20101207114336

なんと Gen 2コレクションの発生0。

CLR4 – Large Object Heap Size

20101207114445

LOH もべったり。

CLR4 – % Time in GC

20101207114535

#Gen 1 Collectionsの簡易GCで済むので、コストも低い。

結論、個々のシステムの特性によって感応度に違いはあるかも知れないけど、比較的大きな文字列を編集処理してる場合には CLR2 に対してCLR4の StringBuilder は約3割高速で、主因はStringBuilder内部でのバッファ方式の変更による物。

ReSharper による CLR2 のStringBuilder定義表示

20101207115108

m_StringValue に実体が入ってるので、20KBを超えるとLOH行き、しかもvolatileというCPUに優しくない存在。

同じく ReSharper による CLR4 のStringBuilder 定義表示

20101207115832

m_ChunkPrevious によってチャンク化されていて m_ChunkChars に実体が分割して持たれるという構造、分割されたそれぞれは 8000文字まで、要するに16KBなのでLOHには絶対に行かない。

結論

素晴らしい!アプリケーションをCLR2からCLR4にするだけで数十%のパフォーマンス向上も夢じゃないよね。

夏ぐらいから気付いてたんだけど、正確に分析したくなったので測定してみましたなブログ。

MEFでディレクトリカタログを追いかける(C# Advent Calender jp:2010 12/02)


ってタイトルで用意してたんだけど、結果的には追加はいいけど、削除、更新は駄目だったの巻

MEF いいっすよねー。

ロジック殆ど無しのホストに Extensibility で拡張を仕込んでいければ、システムのメンテナンス単位を小さくできる。停止時間を当然に減らせる。

良い事だらけだ!

って感じなんですが、本当に拡張していけるのか試していってという感じ。

Managed Extensibility Framework の概要

MEF では発見可能性、拡張性、および移植性に重点が置かれているのに対し、MAF では拡張機能の特定とアセンブリの読み込みおよびアンロードに重点が置かれています。

InfoQ: CLRが、何回目かの、「最初の」プラグインモデルを採用。

  • System.Addin は、使用されなくなったAppDomainを自動的に、アンロードし、メモリを再利用できるように、管理している。

 

MAF使わないとアンロードできないのか…

そんな事は無い、アセンブリのシャドウコピーがあるじゃないか!

って試したコードが以下。

using System;
using System.Collections.Generic;
using System.IO;
using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;

namespace MefConsoleApplication
{
    public interface IDoSomething
    {
        void DoSomethig();
    }

    public class Program
    {

#pragma warning disable 649
        [ImportMany(AllowRecomposition = true)]
        private IEnumerable<Lazy<IDoSomething>> _parts;
#pragma warning restore 649

        private CompositionContainer _container;
        private FileSystemWatcher _fsWatcher;
        private DirectoryCatalog _directoryCatalog;

        public void Run()
        {
            AggregateCatalog aggregateCatalog = new AggregateCatalog();
            aggregateCatalog.Catalogs.Add(
                new AssemblyCatalog(typeof(Program).Assembly));
            // ReSharper disable AssignNullToNotNullAttribute
            string extentionPath = Path.Combine(
                Path.GetDirectoryName(typeof(Program).Assembly.Location),
                "..\\..\\Extentions");
            // ReSharper restore AssignNullToNotNullAttribute
            _fsWatcher = new FileSystemWatcher(extentionPath);
            _fsWatcher.Changed += FsWatcherChanged;
            _fsWatcher.EnableRaisingEvents = true;

            _directoryCatalog = new DirectoryCatalog(
                extentionPath);
            _directoryCatalog.Changed += CatalogChanged;
            aggregateCatalog.Catalogs.Add(
                _directoryCatalog);
            aggregateCatalog.Changed += CatalogChanged;
            _container = new CompositionContainer(aggregateCatalog);
            _container.ComposeParts(this);

            foreach (var part in _parts)
            {
                part.Value.DoSomethig();
            }

            while (Console.ReadLine() != "exit")
            {
            }
        }

        void FsWatcherChanged(object sender, FileSystemEventArgs e)
        {
            Console.WriteLine("extentions directory changed");
            _directoryCatalog.Refresh();
        }

        void CatalogChanged(object sender, ComposablePartCatalogChangeEventArgs e)
        {
            Console.WriteLine("catalog changed " + sender.GetType().Name);
            _container.ComposeParts(this);

            foreach (var part in _parts)
            {
                part.Value.DoSomethig();
            }
        }

        static void Main()
        {
            if (!AppDomain.CurrentDomain.ShadowCopyFiles)
            {
                Console.WriteLine("ShadowCopyFilesがオフです");
                AppDomainSetup appDomainSetup = new AppDomainSetup();
                appDomainSetup.ShadowCopyFiles = "true";
                appDomainSetup.ShadowCopyDirectories = Path.GetTempPath();
                appDomainSetup.CachePath = Path.GetTempPath();

                AppDomain execDomain = AppDomain.CreateDomain("execDomain",
                    AppDomain.CurrentDomain.Evidence, appDomainSetup);
                execDomain.ExecuteAssembly(typeof(Program).Assembly.Location);
                return;
            }
            Console.WriteLine("ShadowCopyFilesがオンです");

            Program prog = new Program();
            prog.Run();
        }
    }

    [Export(typeof(IDoSomething))]
    public class ImplSomething1 : IDoSomething
    {
        public void DoSomethig()
        {
            Console.WriteLine("do something 1");
        }
    }
}

 

Main での AppDomainSetup を介して自分を実行し直すって方法で、アプリケーションドメインをシャドウコピー可能にしている。

using System;
using System.ComponentModel.Composition;
using MefConsoleApplication;

namespace ApplicationComponent1
{
    [Export(typeof(IDoSomething))]
    public class Class1 : IDoSomething
    {
        public void DoSomethig()
        {
            Console.WriteLine("extention component1");
        }
    }
}

 

ってアセンブリを放り込むと、ディレクトリの変更を検出して、ちゃんとアセンブリがロードされる。大成功だ。

20101203192208

で、放り込んだもんを削除しようとする。

20101203192308

残念!ってわけで拒否されてしまいました…。

MEF で DirectoryCatalog のChangeを追うには以下が注意事項でございます。

  1. FileSystemWatcherで監視してないと駄目、_directoryCatalog.Refresh()でカタログを更新する必要がある。
  2. [ImportMany(AllowRecomposition = true)] 重要、AllowRecompositionをtrueにしないとRejectされる。
  3. シャドウコピーを有効にしてもアセンブリの削除はできないぜ。

 

って訳で、長い!なMEFでディレクトリカタログを追ってみたけど失敗した!な記事でございます。

C# Advent Calendar jp: 2010 : ATND の 12/2日分って事で次の日の人よろしく。

有用?よくわかんない記事で済まん。

コードは伸びるよどこまでも


寒くなってきましたねー というわけで C#たんの寒さから入る流れを踏襲してみます。

っていうのもフレームワークを使ったアプリケーション開発っていうのが今回の話題。

C#たんと学ぶ/わりと硬派なソフトウェア開発講座 第1回「C#でできること」

の2ページめ、今時のアプリケーション開発に欠かせない「フレームワーク」

 

フレームワークって便利ですよねー、本当に。 LINQ とかで簡単にデータアクセスとかして簡単にアプリケーション書けちゃいますよねー。

 

アプリケーションで顧客データを参照するなら

var blackListedCustomers = from cust in db.Customers where cust.BlackListed select cust;

でブラックリスト入りしてるお客の一覧を取得できると。

んでご会計ページとかではこれを元にブラックリスト入りしてたらなんかするって訳で、こんな事になるでしょう。

var blackListedCustomers = from cust in db.Customers where cust.BlackListed select cust;
if( blackListedCustomers.Any( c=>c.UserId == session[“UserId”] ) )
{
     Mail.Send(
        new MailMessage( config.GetAdminMailAddr(), “Blacklisted customer notice “,
            string.Format( “要注意購入者情報 {0}”, blackListedCustomers.Where( c=>c.UserId==session[“UserId”] ) ) ) );
}

これはアプリケーションコードとしては非常にまっとうなんだが、いかんせん Any と Where の2回クエリが実行されるとか起こる。

まぁね、要注意人物なんてそんなに居ないだろーという訳で最初の blackListedCustomers に ToList()って書いておけば良いよね。

 

良いのよ、前提を解っていて、それに対処するって事は。でも駄目なのよ、前提を知ってるのは、「そんなに居ないだろー」って前提は少なくとも完全に駄目。

「Any とWhereで2回実行される」のが駄目ってのも前提を知ってるって事で言えば駄目、なんでっていうと「フレームワーク」が隠ぺいしている機能についての詳細だから。

むにゃむにゃあって、フレームワークを使っても結局は漏れのある抽象化にしかならないケースが多い。中を気にするのは本来は駄目な話、フレームワークは中の事を気にせずにものごとができる様に作られなきゃいけないはずなんだが、それが実現されてるなんて「夢見てんなよ」だったりする。

夢なら夢で諦めてフレームワークの中の事をちっと気にしてプログラム書いてあげるのはやぶさかではないって事で諦めよう。

 

んな訳で、先に進む。

データを取ってキャッシュしないで済ませられるのは小学生までらしいのでキャッシュする事にしよう。

ASP.NETなんでAPIとしては System.Web.Caching あたりにある物を使おう。

ここでストレージフレームワークと、Webフレームワークの境界線のあたりの不毛地帯を通る必要がある。LINQはクエリ言語であり、その実装が「ストレージアクセスフレームワーク」で隠ぺい範囲は「ストレージアクセス」なんだから「キャッシュなんて知りません」は至極当然まっとうな結果だったりするんだが、本来簡単なはずの話がどこまで複雑になるか見てみるとげっそりする。

var blackListedCustomers = from cust in db.Customers where cust.BlackListed select cust;

の一行をちゃんとキャッシュ意識して書くとこーなるってのが以下の通り。

var cache = HttpContext.Current.Cache;
string cacheKey = “BlacklistedCustomers”;
List<Customer> blackListedCustomers;
blackListedCustomers = cache.Get(cacheKey);
if( blackListedCustomers==null )
{
    blackListedCustomers = (from cust in db.Customers where cust.BlackListed select cust).ToList();
    lock( cache )
    {
        List<Customer> ready = cache.Get(cacheKey);
        if( ready ==null )
        {
            cache.Add( cacheKey,blackListedCustomers ); 
        } 
        else
        {
            blackListedCustomers = ready;
        }
    }
}

check-lock-check (check-execute-lock-check-update) はちゃんとやらずにあちこちでロックしてるとアプリケーションがガチガチとブロッキングするのでキャッシュ制御では必須ね、この辺ちゃーんと複数スレッドでのコード実行を意識できる人でないと忘れがち。

っておい、「アプリケーションプログラマはキャッシュだけでなく並行制御もちゃんと知らなきゃ駄目なのかい?」って事になるし、本来アプリケーションでやりたいと思ってる事よりこっちの方が行数があるってのも問題で、アプリケーションコードなんてのは実際にやりたい事がまばらにあるだけで8割は色々なフレームワークが「自分の範疇じゃないし、そんな事しったこっちゃねーよ」と言った事の掃溜めと言っていいぐらいだったりするのはむしろ普通な事なんだ。(LINQ to SQL 固有の CompiledQuery にするとか、Entity Framework にも固有の事象とかその辺は今回入れてないんだよ)

 

とりあえず、以下のリンクに飛んで、スクロールバーの状態だけでも見てもらえれば、クエリの実行にまつわるどれだけの事が隠ぺいされたかは解る。

.NET アプリケーションのパフォーマンスとスケーラビリティの向上 – 第 12 章 「ADO.NET パフォーマンスの向上」

昔(2005年当時)はデータアクセスにこんだけ注意事項があった、がその辺全部をLINQとクエリプロバイダの中で解決してくれる(はず!)。(実際は LINQ to SQLのクエリ実行ではここに書かれたいくつかのパフォーマンス上のtipsは実行されていない)

しかし、アプリケーション全体ではまだまだ新しい注意事項がどんどん出てきているし、キリガ無い。その上に RIAやRESTだ。

タイトルの通り。

フレームワーク?素晴らしいものかもしれんが、まだまだ夢のような話だよねって感じであります。

QueryProviderの部品いろいろ


Azure Table 用のエンティティデザイナ(書きかけ)


DSL Tools で表題のものを書きましたのでおすそ分け。スクリーンレコーディングを Expression のスクリーンキャプチャで取ったのですが、さすがにFullHDでのスクリーンキャプチャはマシン性能が厳しいですな。SkyDriveへあげるつもりだったんだけど、1ファイル50MBまでって事であがんね(w

 うげ、SkyDrive にあげた物の埋め込みタグが貼り付けられずに切られるんですが、これってアリなんでしょうか、同じ所が提供してるサービスとしてどうよな感じ。

←のバーに出している SkyDrive のパブリック配下に AzureTableEntityDesigner.zip がございますのでご自由にお持ちください。

 Spacesもう駄目か?

.NET Framework CLR2 & CLR4 で例外の性能を見てみた


簡単なコードで例外の発行性能を見てみた。
 
private void ThrowCatch( int count )
{
    var exception = new ApplicationException();
    for( int i=0;i<count;i++ ) {
       try {
            throw exception;
        }
        catch( Exception ) {}
    }
}
 
を count を大きめに指定して Stopwatch (System.Diagnostics名前空間のね、手でカチカチしたわけじゃないよ) で経過時間を count で割ってみるって感じ。
(実際には初回にはリソースの読み込みやGITなど処理が絡むので、count=10 で動かしてから、count=1000で呼んでみた。
 
大雑把な計測だが Core2Duo T9600 2.8GHzで .NET 2.0 は x86/x64 共に 30 exception per ms 程度。 .NET 4.0 だと x86 は若干性能下がって 28 exception per ms、x64 は大幅に性能向上が見られて 60超 exception per ms をたたき出した。
 
この値を見ると例外の発生を嫌うチューニングは、あんまり意味がないように見えるし、普通ならしないでもOKと判断できる。
ただし、この値はもう一段別の事象についてもパフォーマンスベクトルがあって、デバッガがアタッチしてる時の事も考慮に入れる必要がある。
 
Visual Studio 2010 のデバッガがアタッチしている状況においては、最速を誇った .NET 4 CLR x64 でも 0,6 exception per ms しか出なかった。
要するに例外についての詳細を知りたいと思ってるような状況においてパフォーマンスが100分の1に劣化するって事、最悪ケースを想定すれば、普通に動かすと1分で再現する事に2時間近く浪費する可能性があるって事ね。
 
Azure では ADO.NET Data Service Client によるTableストレージのアクセスが提供されるんだけど、このTableアクセスは System.Net.HttpWebRequest を使う、HttpWebRequestはRESTの文脈でのデータねーよっていう極当たり前のシチュエーションである 404 Not Foundに対して WebException 例外を放り投げる。GetResponse に TryGetResponse があればいいんだけど無いものなんで仕方が無い。あれまー、残念なことだねぇって感じ。
 
結果として IgnoreResourceNotFoundException なんてプロパティがあって、これをセットしておけば DataServiceContext が try catch して無視してくれるけど、Visual Studio でのデバッグ中はデバッガのアウトプットに 「~の初回例外が発生しました」がずらずらと並び、パフォーマンスは思いっきりスポイルされる。
自分には 「~の初回例外が発生しました」がずらずらと並びの状態で本来のアプリケーションのデバッグをできる精神力が無いんで TryGetResponse を作りました。性能に関しての視点としてリリースバイナリーでの実行だけを考える&デバックなんてしねーよwの普通の人には無駄だったりしますんでレシピだけ。
 
 
からソースをダウンロードする。
HttpWebRequest のGetResponse を切り出して拡張メソッドにする。
メンバフィールド触ってる所を以下のようにもにょる
  アクセサ static readonly Func<HttpWebRequest,フィールドの型> funcHogeField を定義する。
  static コンストラクタで以下のようにアクセサを作る
  Expression paramReq = Expression.Parameter( typeof( HttpWebRequest ), "req" );
  funcHogeField =
     Expression.Lambda(
        Expression.MakeMemberAccess( paramReq, typeof( HttpWebRequest ).GetField( "フィールド名", BindingFlags.Instance | BindingFlags.NonPublic ) )
        paramReq ).Compile();
 んでフィールドを見てるところを funcHogeField( this ) にする。
メソッドを呼んでる所も似たり寄ったりの方法( Expression.Call を使う)でサロゲートする。
System アセンブリの中の internal class を使ってる所は適当なラッパークラスにアクセサを纏めて作る。
 
以上でアクセサのinline化がされなてないって若干のパフォーマンスペナルティはありますが TryGetResponse を作る事ができまする。
 
注) Azure Table の改善のためにはデバッグのためにはそれを呼んでいる Data Service Client も直さなければいけませんが、自分は全部HTTP RESTコールをする方向に行ってしまったんで Data Service Client の修正についてはソンナコトシランです。

他のQueryProviderに寄生するQueryProviderの作り方


Expression の解釈に介入したい場合とかに利用するパターンなんですが、別のQueryProviderに寄生するQueryProvider を書く事がよくありますんで、コード例おば。
 
public partial class InterceptorQueryProvider : IQueryProvider
{
    IQueryProvider originalProvider;

    public InterceptorQueryProvider( IQueryProvider originalProvider ) { this.originalProvider = originalProvider; }

    public IQueryable CreateQuery( Expression exp )
    {
        return new WeapperQueryableNoType( this,originalProvider.CreateQuery( exp ) );
    }

    public IQueryable<T> CreateQuery<T>( Expression exp )
    {
        return new WrapperQueryable<T>( this,originalProvider.CreateQuery<T>( exp ) );
    }

 
    public object Execute( Expression exp )
    {
        //ここで Expression に対して介入することができる
        return originalProvider.Execute( exp );
    }
    public IEnumerable<T> Execute<T>( Expression exp )
    {
        //ここでExpressionに対して介入することができる
        return originalProvider.Execute<T>( exp );
    }
}
 
public class WrapperQueryable<T> : IQueryable<T>
{
    readonly IQueryable<T> Query;
    readonly InterceptorQueryProvider provider;
    public WrapperQueryable(InterceptorQueryProvider p, IQueryable<T> q )
    {
        this.provider = p;
        this.Query = q;
    }
    public IEnumerable<T> GetEnumerator()
    {
        return this.Query.GetEnumerator<T>();
    }
    public IEnumerable GetEnumerator()
    {
         return this.Query.GetEnumerator();
    }
    public Type ElementType { get { return this.Query.ElementType; } }
    public Expression Expression { get { return this.Query.Expression; } }
    public IQueryProvider Provider { get { return this.provider; } } // Provider だけ置き換える
}
public class WrapperQueryableNoType : IQueryable
{
    readonly IQueryable Query;
    readonly InterceptorQueryProvider provider;
    public WrapperQueryable(InterceptorQueryProvider p, IQueryable q )
    {
        this.provider = p;
        this.Query = q;
    }
    public IEnumerable GetEnumerator()
    {
        return this.Query.GetEnumerator();
    }
    public Type ElementType { get { return this.Query.ElementType; } }
    public Expression Expression { get { return this.Query.Expression; } }
    public IQueryProvider Provider { get { return this.provider; } } // Provider だけ置き換える
}
 
ADO.NET Data Service Client の場合には CreateQuery の戻りが IQueryable なので以下の要領で Intercepter をはさみます
var interceptor = new InterceptorQueryProvider();
return WrapperQueryable<T>( interceptor, dataServiceContext.CreateQuery( entitySetName ) );
 
介入できることのメリットとしては特定の QueryProviderでサポートされない要素をExpressionからそぎ落として抽出をかけるようにするとか、 そぎ落とした操作を Queryable -> Enumerable へ変換する事で、クエリ実行の一部をローカル化する等があります。たとえばAzureではサポートされない集計演算や、order by 操作をクエリ対象で実行するのでなくローカル実行する様に式木を変形してしまえばアプリケーションコードのあちこちを直すのでは無くてミドルとして割り込む形でプラットフォームに無い機能の実装を提供できるでしょう。
 
ちなみに式木をローカル実行する場合には Expression.Lambda で包み、CompileしてDelegateにしてからDynamicInvoke で実行します。リモート実行したい部分についてリモートするメソッドを以下のように用意したとすると
 
public abstract IEnumerable<T> ExecuteRemote( Expression t )
{
}
 
式木の一部をリモート実行する部分については
Expression.Call( Expression.Constant(this), this.GetType().GetMethod(“ExecuteRemote”), Expression.Constant( リモートしたい Expression ) )
 
といった変換が必要になるでしょう。 Queryable.Concat の source1, source2 をリモートでそれぞれ実行して結果をローカルで Concat するとすれば ExpressionVisitor 派生で以下のようにコードします
 
// こういう初期化がされているとして
MethodInfo remoteMethod = typeof(Interceptor).GetMethod(“ExecuteRemote”)
MethodInfo[] enumerableMethods = typeof(Enumerable).GetMethods();
// こう
public override Expression VisitMethodCall( MethodCallExpression node )
{
    if( node.Method.DeclaringType == typeof( Queryable ) && node.Method.Name==”Concat” ) )
    {
         return Expression.Call( enumerableMethods.Where( m=>m.Name==”Concat” ).Single().MakeGenericMethod( elementType ),
                      Expression.Call( Expression.Constant(this), remoteMethod, Expression.Constant( node.Arguments[0] ) ),
                      Expression.Call( Expression.Constant(this), remoteMethod, Expression.Constant( node.Arguments[1] ) ) );
    }
    …
}
 
要するに以下のコードに落ちるわけですね。
 
 Enumerable.Concat<elementType>( this.ExecuteRemote( source1), this.ExecuteRemote(source2) );
 
Queryableには同等のEnumerableメソッドが必ずありますのでローカル実行に入った後の部分は機械的に変換する事ができますが、Generic パラメータが絡んでいるとリフレクションでのGetMethod の結果は激しくあてにならないので( Enumerable.Concat を探そうとして GetMethod( “Concat”, new Type[]{ typeof(IEnumerable<>),typeof( IEnumerable<>) } ) で検索すると出てこないあたり)、ぶっちゃけてマッピング用の辞書は static コンストラクタで構築すること推奨します。(全部列挙しちゃって合うはずのものを Where で抽出して MakeGenericMethod で特化させて使うのが安心かも)。
 
クエリを示す式木のどこがリモート実行可能かとかは下請けに使う QueryProvider 次第だったりしますが Queryable -> Enumerable の変換をしてローカル化してしまえば KVS でjoin できないとかも、ローカルでIEnumerableになってからjoin されるのでリクエストが複数回飛ぶとか無駄要素も拾う可能性とかもろもろの性能面の我慢のしようによっては実行可能という落としどころが得られます。最悪ケースとしてExpressionから必要なIndexを探してみてあればそれを単純に読む、無ければ WorkerRoleにExpressionをうまいこと文字列化して送って「インデックスつくってー」って悲鳴を上げて(Queue書きして) HttpException で 503 Service Unavaiable 投げちゃうのも決して適切とはいえないかも知れませんがとりうる実装のひとつとなります。
(アプリケーション内で使うすべての Expression を取得する方法は .NET Framework内には無かったりしますので、そういうことをする方法をアプリケーションレベルで用意してないと必要なインデックスが準備できてるかは判断できませんのよ!!)
 
Concat とかは単純だったりしますけど、Where句のand項を別々のクエリに振り分ける(例:Queryable.Where( Queryable.SelectMeny( X, Y) , q => q.X.a && q.Y.b )  を Queryable.SelectMeny( Queryable.Where( X , e => e.a ), Queryable.Where( Y, e=>e.b ) ) から Enumerable.SelectMeny( remote1でWhere、remote2でWhere) にしないと各Whereがちゃんと実行できないとか) とかはまじめに実装すると結構大変です。