再戦 ASP.NET Core MVC vs. WCF

はじめに

以前 ASP.NET Core MVCWCFベンチマークを比較したことがあって、 そのときは WCF の方がかなり速いなぁ、という結果だった。 ASP.NET Core MVC が思いのほか遅いのが気になったけど、それに関しては当時調べず。

tnakamura.hatenablog.com

このときのベンチマークASP.NET Core MVC が遅かった原因が、コンソールにログを出力していたせいだと判明。試しに Information 以下のログを出力しないように構成したら、だいぶ結果が変わったので、ベンチマークを比較し直すことにした。

ベンチマーク結果

ただやり直すだけでは面白くないので、ASP.NET Core MVC の方はシリアライザに ProtocolBuffers を使った場合のベンチマークも追加してみた。

f:id:griefworker:20190423094647p:plain

ProtocolBuffers を使っても、NetTcpBinding を使った WCF には敵わなかったのが残念。

ベンチマークに使ったコード

ASP.NET Core MVC

WebApiContrib.Core.Formatter.Protobuf を導入することで、シリアライザに ProtocolBuffers が使えるようになる。

using System;
using System.Linq;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using ProtoBuf;
using WebApiContrib.Core.Formatter.Protobuf;

namespace AspNetCoreVsWcf.AspNetCore
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateWebHostBuilder(args).Build().Run();
        }

        public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseStartup<Startup>()
                .ConfigureLogging(logging =>
                {
                    logging.SetMinimumLevel(LogLevel.Warning);
                })
                .UseUrls("http://localhost:8000");
    }

    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc()
                .SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
                .AddProtobufFormatters();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseMvc();
        }
    }

    [ProtoContract]
    public class Book
    {
        [ProtoMember(1)]
        public string Id { get; set; }

        [ProtoMember(2)]
        public string Title { get; set; }

        [ProtoMember(3)]
        public string Description { get; set; }

        [ProtoMember(4)]
        public string Author { get; set; }

        [ProtoMember(5)]
        public int Price { get; set; }

        [ProtoMember(6)]
        public string PublishedAt { get; set; }
    }

    [Route("api/[controller]")]
    [ApiController]
    public class BooksController : ControllerBase
    {
        static readonly Book[] books;

        static BooksController()
        {
            books = Enumerable.Range(0, 50)
                .Select(i => new Book()
                {
                    Id = Guid.NewGuid().ToString(),
                    Title = $"Book{i}",
                    Author = $"Author{i}",
                    Description = $"Description{i}",
                    PublishedAt = DateTime.Today.ToString(),
                    Price = 2000,
                })
                .ToArray();
        }

        [HttpGet]
        public ActionResult<Book[]> Get()
        {
            return books;
        }
    }
}
WCF
using System;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;

namespace AspNetCoreVsWcf.Wcf
{
    class Program
    {
        static void Main(string[] args)
        {
            var host = new ServiceHost(typeof(BookService));
            host.AddServiceEndpoint(
                typeof(IBookService),
                new NetTcpBinding(),
                "net.tcp://localhost:8001/BookService");
            host.Open();

            Console.WriteLine("Enter で終了");
            Console.ReadLine();

            host.Close();
        }
    }

    [DataContract]
    public class Book
    {
        [DataMember]
        public string Id { get; set; }

        [DataMember]
        public string Title { get; set; }

        [DataMember]
        public string Description { get; set; }

        [DataMember]
        public string Author { get; set; }

        [DataMember]
        public int Price { get; set; }

        [DataMember]
        public string PublishedAt { get; set; }
    }

    [ServiceContract]
    public interface IBookService
    {
        [OperationContract]
        Book[] GetBooks();
    }

    public class BookService : IBookService
    {
        static readonly Book[] books;

        static BookService()
        {
            books = Enumerable.Range(0, 50)
                .Select(i => new Book()
                {
                    Id = Guid.NewGuid().ToString(),
                    Title = $"Book{i}",
                    Author = $"Author{i}",
                    Description = $"Description{i}",
                    PublishedAt = DateTime.Today.ToString(),
                    Price = 2000,
                })
                .ToArray();
        }

        public Book[] GetBooks()
        {
            return books;
        }
    }
}
ベンチマーク
using System;
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using Newtonsoft.Json;
using ProtoBuf;

namespace AspNetCoreVsWcf.Benchmark
{
    class Program
    {
        static void Main(string[] args)
        {
            BenchmarkRunner.Run<AspNetCoreVsWcfBenchmark>();

            Console.ReadLine();
        }
    }

    [ProtoContract]
    [DataContract]
    public class Book
    {
        [DataMember]
        [ProtoMember(1)]
        public string Id { get; set; }

        [DataMember]
        [ProtoMember(2)]
        public string Title { get; set; }

        [DataMember]
        [ProtoMember(3)]
        public string Description { get; set; }

        [DataMember]
        [ProtoMember(4)]
        public string Author { get; set; }

        [DataMember]
        [ProtoMember(5)]
        public int Price { get; set; }

        [DataMember]
        [ProtoMember(6)]
        public string PublishedAt { get; set; }
    }

    [ServiceContract]
    public interface IBookService
    {
        [OperationContract]
        Book[] GetBooks();
    }

    public class AspNetCoreVsWcfBenchmark
    {
        IBookService tcpChannel;
        HttpClient httpClient;
        JsonSerializer jsonSerializer;

        [GlobalSetup]
        public void GlobalSetup()
        {
            tcpChannel = ChannelFactory<IBookService>.CreateChannel(
                new NetTcpBinding(),
                new EndpointAddress("net.tcp://localhost:8001/BookService"));
            httpClient = new HttpClient();
            jsonSerializer = JsonSerializer.CreateDefault();
        }

        [GlobalCleanup]
        public void GlobalCleanup()
        {
            ((IClientChannel)tcpChannel).Close();
            httpClient.Dispose();
        }

        [Benchmark]
        public Book[] WcfTcp()
        {
            return tcpChannel.GetBooks();
        }

        [Benchmark]
        public async Task<Book[]> AspNetCoreJson()
        {
            var request = new HttpRequestMessage(
                HttpMethod.Get,
                "http://localhost:8000/api/books");
            request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
            var response = await httpClient.SendAsync(request);
            var stream = await response.Content.ReadAsStreamAsync();
            using (var streamReader = new StreamReader(stream))
            using (var jsonReader = new JsonTextReader(streamReader))
            {
                return jsonSerializer.Deserialize<Book[]>(jsonReader);
            }
        }

        [Benchmark]
        public async Task<Book[]> AspNetCoreProtobuf()
        {
            var request = new HttpRequestMessage(
                HttpMethod.Get,
                "http://localhost:8000/api/books");
            request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/x-protobuf"));
            var response = await httpClient.SendAsync(request);
            var stream = await response.Content.ReadAsStreamAsync();
            return Serializer.Deserialize<Book[]>(stream);
        }
    }
}

服を着るならこんなふうに(7)

さすがに7巻ともなるとネタも無くなりつつあるのか、服の買い時・雨具・洗濯・古着・髪型・服の捨て時と、ファッションの周辺知識が詰められていた。服の捨て時では、最初に買った黒のスキニーとお別れかという展開で、いよいよ最終回かと思ったが、まだ続きそう。

五等分の花嫁(9)

一花・ニ乃・三玖による三つ巴の戦いは、今のところ二乃が優勢か。風太郎にしっかり意識させることに成功しているし、やはり面と向かって告白したアドバンテージは大きい。暴走列車状態なのも強み。

三玖は告白するための自信をつけるために修行中といったところか。風太郎に唯一見分けてもらえたんだから、もっと自信持っていいと思うんだけどねぇ。

一花はちょっといただけないな。嘘はあかんよ嘘は。ダークサイドに落ちつつある。個人的に一花推しだったんだが、このままダークサイドに落ちてしまうようなら、五つ子の間で遺恨残りまくるから、風太郎争奪戦からは脱落するのかもしれないな。

五等分の花嫁(9) (週刊少年マガジンコミックス)

五等分の花嫁(9) (週刊少年マガジンコミックス)

波よ聞いてくれ(6)

カルト教団に軟禁されたミナレたちを奪還すべく男衆が活躍するわけなんだけど、いやなんというか、これラジオを題材にしたマンガなんだよな。最近の展開が展開なだけに忘れそうになる。教団は放送に関する実験とかやっていて、かろうじてラジオと繋がっている…いや無理やりすぎでしょ。

それにしても中原は良い漢だな。中身が。マキエといるときは硬派で頼り甲斐ある姿ばかりで、惚れられてしまうのも仕方ない。ミナレといるときは、彼女に振り回されて情けない姿を見ることが多いのに。ミナレの生き方が無免許だから、それもまた仕方ない。生き方が無免許、ミナレのセリフだけど名言だと思ったね。

長崎ペンギン水族館

ハウステンボスに行ったついでというわけではないが、娘が一時期行きたいと言ってた、 長崎市にある長崎ペンギン水族館にも行ってきた。

入り口でマスコットキャラクターのアバちゃんがお出迎え。

ペンギンを間近で見ることができて、子供はもちろんのこと、大人でも楽しめる。 透明な板を挟んだ向こう側、手を少し伸ばせば触れるくらいの距離。 もちろん触っちゃだめだけど。

ペンギン水族館だけど、魚もちゃんと展示されている。 ちょうど餌やりの時間で、いつもは物陰にいるウツボが餌を求めて泳ぐ姿を初めて見た。

キングペンギンは全く動かなくて、置物かと思ったら、ちゃんと動いて本物であることを確認。

ペンギンの散歩や餌やりを見れたり、ペンギンに触ったりできて、娘はかなり満足したみたいだ。 餌やりの体験もできたんだが、こちらは整理券が速攻で無くなってできなかった。残念。

30分くらいで順路をひと回りできちゃうくらいコンパクトな水族館だったけど満足度は高かった。 大きな水族館だと周るのが大変だったりするので、子供が小さい間はこれくらいがちょうどいいのかも。

penguin-aqua.jp

ハウステンボス

娘が4月から幼稚園に入ったら、夏休みや冬休みといった長期休み以外での遠出は難しくなるので、 ずっと前から連れて行きたいと思っていたハウステンボスのチューリップ祭に行ってきた。

1DAYパスポートを購入して入国。天気が心配だったけど晴れてきてよかった。

テディベアミュージアムで娘のつかみはオッケー。 巨大なジャイアントベアを怖がらないか心配だったけど、無用だった。 幼稚園にも結構大きなクマの人形があるし、耐性がついているんだろう。

チューリップロードには風車と、一面のチューリップ。 ただ、心なしか以前来たときよりチューリップ感が少ないような・・・。 写真の風車の左側なんて、本物のチューリップじゃないし。 まぁ、こいつらは夜に活躍するんだけど。

これまでドムトールンには昇ったことがなかったので、 今回は昇ることに決めていた。 ただ、その前にしまじろうのイベントがちょうど開催されていたので、 そっちから攻略。

しまじろうのイベントでかなり歩かされたので、ひとまず腹ごしらえ。 行きたかった八天堂のカフェは残念ながら閉店していたので、 ドムトールン下のフードコートで佐世保バーガーを食べた。 思いのほか美味しかったのは収穫。

ドムトールンに初めて昇った感想としては、思っていた以上に狭くてびっくり。 福岡タワーの展望台くらいはあると思っていたんだけどなぁ。 この日は平日だったからよかったものの、もし休日だったらぎゅうぎゅう詰めになるんじゃないか? さすがに見晴らしはよかった。

アトラクションタウンには、映像や VR を駆使したアトラクションが多かったが、 どれも子供にはまだ早くて入れなかった。 せっかくの 1DAY パスポートがぁ。 実物大のイングラムが展示されていたのは驚いたね。 娘は怖がって近づかなかったけど。

当初は時間遅いし見るの難しいかもなぁと思っていた噴水ショーも、 なんとか19時まで粘って見ることができた。 お土産にクリームチーズとターフルソースのセットを購入して帰宅。

前回来たチューリップ祭よりもスケールが落ちてる気がした。 無料ゾーンも無くなってたし。 娘はアドベンチャーパークのふわふわランドが一番楽しめたみたいで、 ここだけで2時間ぐらい遊んでいたんじゃないだろうか。 そんなアドベンチャーパークも、大半のアトラクションは娘にはまだ早いものばかり。 全体を通して、ハウステンボス自体がまだ来るの早かったかなぁ、という感想。

www.huistenbosch.co.jp

ぼくたちは勉強ができない(11)

文乃と父親の問題がついに解決。父親に思いの丈をぶつけた文乃の姿は立派で、メインヒロインの格が見えた。別に嫌い合っていた訳ではなく、どちらが素直になれればもっと早くに解決したかもしれないけど、そのために必要な勇気が今まで足りなかったんだろうな。成幸が背中を押されたことで、勇気をもらえて、父親と向かい合うことができた、と。いい話や。

成幸争奪レースはやはり本命・文乃、対抗・うるか、大穴・先生なのか。その成幸は、前に進んでいく文乃やうるか、理珠の影響を受けて、重大な決断をした模様。ヒロインたちの夢には向き合ってきたけど、成幸の夢に関しては触れてこなかったからなぁ。ここから先は成幸の夢が話の中心になっていくんだろう。