2010年12月29日水曜日

Razor Templating Engine

Razor Templating Engine

CodePlexで新しいのが出てたよ~。面白いね~。テンプレートエンジンっていろいろ用途があるけど需要はどーなんだろね。まぁ、いいや。Razor記法でWeb以外にも使えるって楽しいっすよね。だっていろいろ覚えなくて済むし。ちなみにASP.NETの<%...%>とか<%$…%>とか<%#…%>なんかの記法も用途特化したテンプレートエンジンが解釈してクラスファイルを生成してますよね。

んでね、Razorは純粋にテンプレートエンジンって言ってるくらいだからWebのコンテキストや実行Hostに依存しないわけですね。つまりConsoleアプリでも使えるってことです。もちろんHtmlHelperとかWeb関連のアセンブリに依存するものはあるとしても、それ自体はRazorの機能じゃなくてHelperの機能。TemplateBaseクラスが生成されるクラスの親クラスになるって言う仕組みなので、親クラスにいろいろWeb関連の機能を保持するっていう、Razorとは直接関係なっしんぐ。

サンプル見てみるとこんな感じですね。

rte1

string template = "Hello @Model.Name! Welcome to Razor!";
string result = Razor.Parse(template, new { Name = "World" });

Console.WriteLine(result);
Console.ReadLine();

簡単ですね~。お気楽です。

同じくサンプルのHtmlHelperを使うパターン。

rte2

Razor.SetTemplateBaseType(typeof(HtmlTemplateBase<>));
string template =
@"<html>
    <head>
    <title>Hello @Model.Name</title>
    </head>
    <body>
    Email: @Html.TextBoxFor(m => m.Email)
    </body>
</html>";

var model = new PageModel { Name = "World", Email = "someone@somewhere.com" };
string result = Razor.Parse(template, model);

Console.WriteLine(result);
Console.ReadLine();

はい、これも簡単ですね。イメージ通りです。

PageModelは自分で作ってね。あと、RazorEngine.Coreだけじゃ足りなくて、RazorEngine.Templatesアセンブリも必要なので、ソースのダウンロードはここからどーぞ。

これらは全て実行時にクラスファイル(特に指定が無い限りc#)をオンメモリで生成してコンパイルします。そのタイミングでAppDomain内からはクラスのインスタンスを生成できるようになるって言う流れはASP.NETのパイプラインそのもの。

試しに最初のサンプル(Hello World!のほう)をステップ実行していくと、どういうクラス名で生成されるのかというのを追いかけていくと...。

rte3

雑ですね。Guid.NewGuid()から数字とハイフンを除外したのがクラス名。それにNamespaceをくっつけてます。で確認。

rte4

ちゃんといます。で、AppDomainの仕様を見てみるとこんなことがかかれてます。

既定のアプリケーション ドメインにロードされたアセンブリは、プロセスの実行中にメモリからアンロードすることはできません。 しかし、アプリケーション ドメインをもう 1 つ開いてアセンブリをロードおよび実行すると、そのアプリケーション ドメインがアンロードされたときに、アセンブリもアンロードされます。 このテクニックを使用すると、大きな DLL を使用する場合のある、長時間実行されるプロセスのワーキング セットを最小限に抑えることができます。

AppDomain クラス (System)

ん~、どーだろ。どうなるんだろね。

rte6

rte5

めちゃめちゃ分かりにくいですね。2個目のサンプル(HtmlHelper使うほう)を使ってRazor.Parseを繰り返してる途中です。

Razor.SetTemplateBaseType(typeof(HtmlTemplateBase<>));
var model = new PageModel { Name = "World", Email = "someone@somewhere.com" };

for (var i = 0; i < (singleExec ? 1 : Repeat); i++)
{
    var sw = new Stopwatch();
    sw.Start();
    var result = Razor.Parse(Template, model);
    sw.Stop();

    Console.WriteLine(
        "{0} - {1} {2} ms {3:0,###} bytes",
        result.GetHashCode(),
        i,
        sw.ElapsedMilliseconds,
        Process.GetCurrentProcess().PrivateMemorySize64);
}

少しずつメモリ使用量と動作時間が長くなっていきます。最初は200msとかで処理。すごく単純で1種類だけの生成なのにね。ちなみに一度コンパイルしたものをその後何度も使用する場合は超早い。

rte8

rte7

10000回実行してもすぐ終了。

これってつまり、ASP.NETでも同じでaspxを何度も書き換えてると処理時間はどんどん増加。いつか破綻?でもリサイクルも走るし、そもそも運用環境でそんなことしないから問題にはならないか。

AppDomainをアンロードしない限りアセンブリもアンロードされないってことはつまり常時動いてるようなシステムで頻繁にコンパイルを繰り返すのはよろしくないね!ってなりましょう。それをひっくり返すのがAppDomainを別に作ってそっちで動かす方法。

リファレンスにもちゃんとかかれてますね。ボスもタイムリーに(MEFだけど)、そんなことをブログに書いてました。

MEFでディレクトリカタログを追いかける(C# Advent Calender jp:2010 12/02) « kazuk は null に触れてしまった

なのでAppDomainを作って実行するように試してみた。

rte9

けど無理!遅すぎ!

var sw = new Stopwatch();
sw.Start();

AppDomainSetup ads = new AppDomainSetup();
ads.ApplicationBase = Environment.CurrentDirectory;
ads.DisallowBindingRedirects = false;
ads.DisallowCodeDownload = true;
ads.ConfigurationFile = AppDomain.CurrentDomain.SetupInformation.ConfigurationFile;

AppDomain executeAppDomain = AppDomain.CreateDomain("AD#" + i, null, ads);
var razor = executeAppDomain.CreateInstanceAndUnwrap(
    Assembly.GetEntryAssembly().FullName,
    typeof(RazorTemplateTest).FullName
) as RazorTemplateTest;

razor.ExecuteParse(i);

AppDomain.Unload(executeAppDomain);

sw.Stop();

Console.WriteLine(" {0} : {1} ms",
    AppDomain.CurrentDomain.FriendlyName,
    sw.ElapsedMilliseconds);

でも、まぁ、思ったような結果が出なかった残念な調査でした。ホントはもっとメモリ開放されないことを想定してたんだけどな~。

オチがない...。

using System;
using System.Diagnostics;
using System.Reflection;
using RazorEngine;
using RazorEngine.Templating;

namespace ConsoleApplication1
{
    public class PageModel
    {
        public string Name { get; set; }
        public string Email { get; set; }
    }

    public class RazorTemplateTest : MarshalByRefObject
    {
        private static int Repeat = 10000;
        static string Template =
@"<html>
    <head>
        <title>Hello @Model.Name</title>
    </head>
    <body>
        Email: @Html.TextBoxFor(m => m.Email)
    </body>
</html>";

        public void ExecuteParse()
        {
            ExecuteParse(-1);
        }

        public void ExecuteParse(int counter)
        {
            Razor.SetTemplateBaseType(typeof(HtmlTemplateBase<>));
            var model = new PageModel { Name = "World", Email = "someone@somewhere.com" };

            for (var i = 0; i < (counter!=-1 ? 1 : Repeat); i++)
            {
                var sw = new Stopwatch();
                sw.Start();
                var result = Razor.Parse(Template, model);
                sw.Stop();

                Console.WriteLine(
                    "{0} - {1} {2} ms {3:0,###} bytes",
                    AppDomain.CurrentDomain.FriendlyName,
                    counter != -1 ? counter : i,
                    sw.ElapsedMilliseconds,
                    Process.GetCurrentProcess().PrivateMemorySize64);
            }
        }

        public void ExecuteCompile()
        {
            Razor.SetTemplateBaseType(typeof(HtmlTemplateBase<>));
            var model = new PageModel { Name = "World", Email = "someone@somewhere.com" };

            Razor.Compile(Template, typeof(PageModel), "test");

            for (var i = 0; i < Repeat; i++)
            {
                var sw = new Stopwatch();
                sw.Start();

                var result = Razor.Run(model, "test");

                sw.Stop();

                Console.WriteLine(
                    "{0} - {1} {2} ms {3:0,###} bytes",
                    result.GetHashCode(),
                    i,
                    sw.ElapsedMilliseconds,
                    Process.GetCurrentProcess().PrivateMemorySize64);
            }
        }

        public void ExecuteAnotherAppDomain()
        {
            for (var i = 0; i < Repeat; i++)
            {
                var sw = new Stopwatch();
                sw.Start();

                AppDomainSetup ads = new AppDomainSetup();
                ads.ApplicationBase = Environment.CurrentDirectory;
                ads.DisallowBindingRedirects = false;
                ads.DisallowCodeDownload = true;
                ads.ConfigurationFile = AppDomain.CurrentDomain.SetupInformation.ConfigurationFile;

                AppDomain executeAppDomain = AppDomain.CreateDomain("AD#" + i, null, ads);
                var razor = executeAppDomain.CreateInstanceAndUnwrap(
                    Assembly.GetEntryAssembly().FullName,
                    typeof(RazorTemplateTest).FullName
                ) as RazorTemplateTest;

                razor.ExecuteParse(i);
                
                AppDomain.Unload(executeAppDomain);

                sw.Stop();

                Console.WriteLine(" {0} : {1} ms",
                    AppDomain.CurrentDomain.FriendlyName,
                    sw.ElapsedMilliseconds);
            }
        }
    }

    class Program
    {

        static void Main(string[] args)
        {
            //new RazorTemplateTest().ExecuteParse();
            //new RazorTemplateTest().ExecuteCompile();
            new RazorTemplateTest().ExecuteAnotherAppDomain();

            Console.ReadLine();
        }
    }
}

こんなコードで~す。

dotnetConf2015 Japan

https://github.com/takepara/MvcVpl ↑こちらにいろいろ置いときました。 参加してくださった方々の温かい対応に感謝感謝です。