Browse Source

EasyRpc full suite (#5949)

* adding single query test

* lower casing model

* updating connection string

* starting fortunes test

* fixing typo

* [ci fw-only csharp/easyrpc csharp/aspcore-mvc-ado-pg]

* adding shared service attribute

* adding fortunes test

* Adding tests
[ci fw-only csharp/easyrpc]

* setting Content-Type to text/html; charset=utf-8

* fixing typo in path

* updating readme
Ian Johnson 5 years ago
parent
commit
513e24feb3

+ 4 - 1
frameworks/CSharp/easyrpc/Benchmarks/Benchmarks.csproj

@@ -5,6 +5,9 @@
   </PropertyGroup>
 
   <ItemGroup>
-    <PackageReference Include="EasyRpc.AspNetCore.Utf8Json" Version="5.0.0-Beta1119" />
+    <PackageReference Include="EasyRpc.AspNetCore" Version="5.0.0-Beta1167" />
+    <PackageReference Include="EasyRpc.AspNetCore.Utf8Json" Version="5.0.0-Beta1167" />
+    <PackageReference Include="EasyRpc.AspNetCore.Views" Version="5.0.0-Beta1167" />
+    <PackageReference Include="Npgsql" Version="5.0.0-alpha1" />
   </ItemGroup>
 </Project>

+ 10 - 0
frameworks/CSharp/easyrpc/Benchmarks/Data/AppSettings.cs

@@ -0,0 +1,10 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+namespace Benchmarks
+{
+    public class AppSettings
+    {
+        public string ConnectionString { get; set; }
+    }
+}

+ 37 - 0
frameworks/CSharp/easyrpc/Benchmarks/Data/BatchUpdateString.cs

@@ -0,0 +1,37 @@
+// Copyright (c) .NET Foundation. All rights reserved. 
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 
+
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.CompilerServices;
+
+namespace Benchmarks.Data
+{
+    internal class BatchUpdateString
+    {
+        private const int MaxBatch = 500;
+
+        internal static readonly string[] Ids = Enumerable.Range(0, MaxBatch).Select(i => $"@Id_{i}").ToArray();
+        internal static readonly string[] Randoms = Enumerable.Range(0, MaxBatch).Select(i => $"@Random_{i}").ToArray();
+
+        private static string[] _queries = new string[MaxBatch + 1];
+
+        public static string Query(int batchSize)
+        {
+            if (_queries[batchSize] != null)
+            {
+                return _queries[batchSize];
+            }
+
+            var lastIndex = batchSize - 1;
+
+            var sb = StringBuilderCache.Acquire();
+
+            sb.Append("UPDATE world SET randomNumber = temp.randomNumber FROM (VALUES ");
+            Enumerable.Range(0, lastIndex).ToList().ForEach(i => sb.Append($"(@Id_{i}, @Random_{i}), "));
+            sb.Append($"(@Id_{lastIndex}, @Random_{lastIndex}) ORDER BY 1) AS temp(id, randomNumber) WHERE temp.id = world.id");
+
+            return _queries[batchSize] = StringBuilderCache.GetStringAndRelease(sb);
+        }
+    }
+}

+ 17 - 0
frameworks/CSharp/easyrpc/Benchmarks/Data/CachedWorld.cs

@@ -0,0 +1,17 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Runtime.InteropServices;
+
+namespace Benchmarks.Data
+{
+    public class CachedWorld
+    {
+        public int id { get; set; }
+
+        public int randomNumber { get; set; }
+
+        public static implicit operator World(CachedWorld world) => new World { id = world.id, randomNumber = world.randomNumber };
+        public static implicit operator CachedWorld(World world) => new CachedWorld { id = world.id, randomNumber = world.randomNumber };
+    }
+}

+ 25 - 0
frameworks/CSharp/easyrpc/Benchmarks/Data/Fortune.cs

@@ -0,0 +1,25 @@
+// Copyright (c) .NET Foundation. All rights reserved. 
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 
+
+using System;
+
+namespace Benchmarks.Data
+{
+    public readonly struct Fortune : IComparable<Fortune>, IComparable
+    {
+        public Fortune(int id, string message)
+        {
+            Id = id;
+            Message = message;
+        }
+
+        public int Id { get; }
+
+        public string Message { get; }
+
+        public int CompareTo(object obj) => throw new InvalidOperationException("The non-generic CompareTo should not be used");
+
+        // Performance critical, using culture insensitive comparison
+        public int CompareTo(Fortune other) => string.CompareOrdinal(Message, other.Message);
+    }
+}

+ 35 - 0
frameworks/CSharp/easyrpc/Benchmarks/Data/Random.cs

@@ -0,0 +1,35 @@
+// Copyright (c) .NET Foundation. All rights reserved. 
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 
+
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Runtime.CompilerServices;
+using System.Threading;
+
+namespace Benchmarks.Data
+{
+    public class ConcurrentRandom
+    {
+        private static int nextSeed = 0;
+
+        // Random isn't thread safe
+        [ThreadStatic]
+        private static Random _random;
+
+        private static Random Random => _random ?? CreateRandom();
+
+        [MethodImpl(MethodImplOptions.NoInlining)]
+        private static Random CreateRandom()
+        {
+            _random = new Random(Interlocked.Increment(ref nextSeed));
+            return _random;
+        }
+
+        public int Next(int minValue, int maxValue)
+        {
+            return Random.Next(minValue, maxValue);
+        }
+    }
+}

+ 262 - 0
frameworks/CSharp/easyrpc/Benchmarks/Data/RawDb.cs

@@ -0,0 +1,262 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Caching.Memory;
+using Npgsql;
+
+namespace Benchmarks.Data
+{
+    public interface IRawDb    
+    {
+        Task<World> LoadSingleQueryRow();
+
+        Task<World[]> LoadMultipleQueriesRows(int count);
+
+        Task<World[]> LoadMultipleUpdatesRows(int count);
+
+        Task<World[]> LoadCachedQueries(int count);
+
+        Task<List<Fortune>> LoadFortunesRows();        
+    }
+
+
+    public class RawDb : IRawDb
+    {
+        private readonly string _connectionString;
+        private readonly MemoryCache _cache;
+        
+        public RawDb(AppSettings appSettings)
+        {
+            _connectionString = appSettings.ConnectionString;
+
+            _cache = new MemoryCache(
+                new MemoryCacheOptions()
+                {
+                    ExpirationScanFrequency = TimeSpan.FromMinutes(60)
+                });
+        }
+
+        public async Task<World> LoadSingleQueryRow()
+        {
+            using (var db = new NpgsqlConnection(_connectionString))
+            {
+                await db.OpenAsync();
+                
+                var (cmd, _) = CreateReadCommand(db, new ConcurrentRandom());
+                
+                using (cmd)
+                {
+                    return await ReadSingleRow(cmd);
+                }
+            }
+        }
+
+        public async Task<List<Fortune>> LoadFortunesRows()
+        {
+            var result = new List<Fortune>();
+
+            using (var db = new NpgsqlConnection(_connectionString))            
+            using (var cmd = db.CreateCommand())
+            {
+                cmd.CommandText = "SELECT id, message FROM fortune";
+
+                await db.OpenAsync();
+
+                using (var rdr = await cmd.ExecuteReaderAsync(CommandBehavior.CloseConnection))
+                {
+                    while (await rdr.ReadAsync())
+                    {
+                        result.Add(new Fortune(rdr.GetInt32(0), rdr.GetString(1)));
+                    }
+                }
+            }
+
+            result.Add(new Fortune (0, "Additional fortune added at request time." ));
+            result.Sort();
+
+            return result;
+        }
+
+        public async Task<World[]> LoadMultipleQueriesRows(int count)
+        {
+            var random = new ConcurrentRandom();
+            var result = new World[count];
+
+            using (var db = new NpgsqlConnection(_connectionString))
+            {
+                await db.OpenAsync();
+
+                var (cmd, parameter) = CreateReadCommand(db, random);
+
+                using (cmd)
+                {
+                    for (int i = 0; i < count; i++)
+                    {
+                        result[i] = await ReadSingleRow(cmd);
+
+                        parameter.Value = random.Next(1, 10001);
+                    }
+                }
+            }
+
+            return result;
+        }
+
+        public async Task<World[]> LoadMultipleUpdatesRows(int count)
+        {
+            var random = new ConcurrentRandom();
+            var results = new World[count];
+
+            using (var db = new NpgsqlConnection(_connectionString))
+            {
+                await db.OpenAsync();
+
+                var (queryCmd, queryParameter) = CreateReadCommand(db, random);
+
+                using (queryCmd)
+                {
+                    for (int i = 0; i < results.Length; i++)
+                    {
+                        results[i] = await ReadSingleRow(queryCmd);
+                        queryParameter.TypedValue = random.Next(1, 10001);
+                    }
+                }
+
+                using (var updateCmd = new NpgsqlCommand(BatchUpdateString.Query(count), db))
+                {
+                    var ids = BatchUpdateString.Ids;
+                    var randoms = BatchUpdateString.Randoms;
+
+                    for (int i = 0; i < results.Length; i++)
+                    {
+                        var randomNumber = random.Next(1, 10001);
+
+                        updateCmd.Parameters.Add(new NpgsqlParameter<int>(parameterName: ids[i], value: results[i].id));
+                        updateCmd.Parameters.Add(new NpgsqlParameter<int>(parameterName: randoms[i], value: randomNumber));
+
+                        results[i].randomNumber = randomNumber;
+                    }
+
+                    await updateCmd.ExecuteNonQueryAsync();
+                }
+            }
+
+            return results;
+        }
+
+        public Task<World[]> LoadCachedQueries(int count)
+        {
+            var result = new World[count];
+            var cacheKeys = _cacheKeys;
+            var cache = _cache;
+            var random = new ConcurrentRandom();
+
+            for (var i = 0; i < result.Length; i++)
+            {
+                var id = random.Next(1, 10001);
+                var key = cacheKeys[id];
+                var data = cache.Get<CachedWorld>(key);
+
+                if (data != null)
+                {
+                    result[i] = data;
+                }
+                else
+                {
+                    return LoadUncachedQueries(random, id, i, count, this, result);
+                }
+            }
+
+            return Task.FromResult(result);
+
+            static async Task<World[]> LoadUncachedQueries(ConcurrentRandom random, int id, int i, int count, RawDb rawdb, World[] result)
+            {
+                using (var db = new NpgsqlConnection(rawdb._connectionString))
+                {
+                    await db.OpenAsync();
+
+                    var (cmd, idParameter) = rawdb.CreateReadCommand(db,random);
+                    
+                    using (cmd)
+                    {
+                        Func<ICacheEntry, Task<CachedWorld>> create = async (entry) => 
+                        {
+                            return await rawdb.ReadSingleRow(cmd);
+                        };
+
+                        var cacheKeys = _cacheKeys;
+                        var key = cacheKeys[id];
+
+                        idParameter.TypedValue = id;
+
+                        for (; i < result.Length; i++)
+                        {
+                            var data = await rawdb._cache.GetOrCreateAsync<CachedWorld>(key, create);
+                            result[i] = data;
+
+                            id = random.Next(1, 10001);
+                            idParameter.TypedValue = id;
+                            key = cacheKeys[id];
+                        }
+                    }
+                }
+
+                return result;
+            }
+        }
+
+        private (NpgsqlCommand readCmd, NpgsqlParameter<int> idParameter) CreateReadCommand(NpgsqlConnection connection, ConcurrentRandom random)
+        {
+            var cmd = new NpgsqlCommand("SELECT id, randomnumber FROM world WHERE id = @Id", connection);
+
+            var parameter = new NpgsqlParameter<int>(parameterName: "@Id", value: random.Next(1, 10001));
+
+            cmd.Parameters.Add(parameter);
+
+            return (cmd, parameter);
+        }
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        private async Task<World> ReadSingleRow(NpgsqlCommand cmd)
+        {
+            using (var rdr = await cmd.ExecuteReaderAsync(System.Data.CommandBehavior.SingleRow))
+            {
+                await rdr.ReadAsync();
+
+                return new World
+                {
+                    id = rdr.GetInt32(0),
+                    randomNumber = rdr.GetInt32(1)
+                };
+            }
+        }
+
+        private static readonly object[] _cacheKeys = Enumerable.Range(0, 10001).Select((i) => new CacheKey(i)).ToArray();
+
+        public sealed class CacheKey : IEquatable<CacheKey>
+        {
+            private readonly int _value;
+
+            public CacheKey(int value)
+                => _value = value;
+
+            public bool Equals(CacheKey key)
+                => key._value == _value;
+
+            public override bool Equals(object obj) 
+                => ReferenceEquals(obj, this);
+
+            public override int GetHashCode()
+                => _value;
+
+            public override string ToString()
+                => _value.ToString();
+        }
+    }
+}

+ 59 - 0
frameworks/CSharp/easyrpc/Benchmarks/Data/StringBuilderCache.cs

@@ -0,0 +1,59 @@
+// Copyright (c) .NET Foundation. All rights reserved. 
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 
+
+using System;
+using System.Text;
+
+namespace Benchmarks.Data
+{
+    internal static class StringBuilderCache
+    {
+        private const int DefaultCapacity = 1386;
+        private const int MaxBuilderSize = DefaultCapacity * 3;
+
+        [ThreadStatic]
+        private static StringBuilder t_cachedInstance;
+
+        /// <summary>Get a StringBuilder for the specified capacity.</summary>
+        /// <remarks>If a StringBuilder of an appropriate size is cached, it will be returned and the cache emptied.</remarks>
+        public static StringBuilder Acquire(int capacity = DefaultCapacity)
+        {
+            if (capacity <= MaxBuilderSize)
+            {
+                StringBuilder sb = t_cachedInstance;
+                if (capacity < DefaultCapacity)
+                {
+                    capacity = DefaultCapacity;
+                }
+
+                if (sb != null)
+                {
+                    // Avoid stringbuilder block fragmentation by getting a new StringBuilder
+                    // when the requested size is larger than the current capacity
+                    if (capacity <= sb.Capacity)
+                    {
+                        t_cachedInstance = null;
+                        sb.Clear();
+                        return sb;
+                    }
+                }
+            }
+            return new StringBuilder(capacity);
+        }
+
+        public static void Release(StringBuilder sb)
+        {
+            if (sb.Capacity <= MaxBuilderSize)
+            {
+                t_cachedInstance = sb;
+            }
+        }
+
+        public static string GetStringAndRelease(StringBuilder sb)
+        {
+            string result = sb.ToString();
+            Release(sb);
+            return result;
+        }
+    }
+}

+ 15 - 0
frameworks/CSharp/easyrpc/Benchmarks/Data/World.cs

@@ -0,0 +1,15 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Runtime.InteropServices;
+
+namespace Benchmarks.Data
+{
+    [StructLayout(LayoutKind.Sequential, Size = 8)]
+    public struct World
+    {
+        public int id { get; set; }
+
+        public int randomNumber { get; set; }
+    }
+}

+ 1 - 0
frameworks/CSharp/easyrpc/Benchmarks/Program.cs

@@ -10,6 +10,7 @@ namespace Benchmarks
         public static async Task Main(string[] args)
         {
             var config = new ConfigurationBuilder()
+                .AddJsonFile("appsettings.json")
                 .AddEnvironmentVariables(prefix: "ASPNETCORE_")
                 .AddCommandLine(args)
                 .Build();

+ 24 - 0
frameworks/CSharp/easyrpc/Benchmarks/Services/FortuneService.cs

@@ -0,0 +1,24 @@
+using EasyRpc.Abstractions.Path;
+using EasyRpc.AspNetCore.Views;
+using Benchmarks.Data;
+using System.Threading.Tasks;
+using System.Collections.Generic;
+using EasyRpc.Abstractions.Services;
+
+namespace Benchmarks
+{
+    [SharedService]
+    public class FortuneService
+    {
+        private IRawDb _rawDb;
+
+        public FortuneService(IRawDb rawDb) => _rawDb = rawDb;
+
+        [GetMethod("/Fortunes/Fortunes")]
+        [ReturnView(ContentType = "text/html; charset=utf-8")]
+        public Task<List<Fortune>> Fortunes()
+        {
+            return _rawDb.LoadFortunesRows();
+        }
+    }
+}

+ 72 - 0
frameworks/CSharp/easyrpc/Benchmarks/Services/QueryService.cs

@@ -0,0 +1,72 @@
+using System.Threading.Tasks;
+using Benchmarks.Data;
+using EasyRpc.Abstractions.Path;
+using EasyRpc.Abstractions.Services;
+
+namespace Benchmarks.Services
+{
+    [SharedService]
+    public class QueryService
+    {
+        private IRawDb _rawDb;
+
+        public QueryService(IRawDb rawDb)
+        {
+            _rawDb = rawDb;
+        }
+
+        [GetMethod("/db")]
+        public Task<World> Single()
+        {
+            return _rawDb.LoadSingleQueryRow();
+        }
+
+        [GetMethod("/queries/{count}")]
+        public Task<World[]> Multiple(int count = 1)
+        {
+            if(count < 1 )
+            {
+                count = 1;
+            }
+
+            if(count > 500)
+            {
+                count = 500;
+            }
+
+            return _rawDb.LoadMultipleQueriesRows(count);
+        }
+
+        [GetMethod("/updates/{count}")]
+        public Task<World[]> Updates(int count = 1)
+        {
+            if(count < 1 )
+            {
+                count = 1;
+            }
+
+            if(count > 500)
+            {
+                count = 500;
+            }
+            
+            return _rawDb.LoadMultipleUpdatesRows(count);
+        }
+
+        [GetMethod("/cached-worlds/{count}")]
+        public Task<World[]> CachedWorlds(int count = 1)
+        {
+            if(count < 1 )
+            {
+                count = 1;
+            }
+
+            if(count > 500)
+            {
+                count = 500;
+            }
+            
+            return _rawDb.LoadCachedQueries(count);
+        }
+    }
+}

+ 31 - 0
frameworks/CSharp/easyrpc/Benchmarks/Startup.cs

@@ -1,18 +1,46 @@
 using EasyRpc.AspNetCore.Serializers;
 using EasyRpc.AspNetCore.Utf8Json;
+using Microsoft.Extensions.Configuration;
 
 namespace Benchmarks
 {
+    using System.Text.Encodings.Web;
+    using System.Text.Unicode;
+    using Benchmarks.Data;
+    using Benchmarks.Services;
     using EasyRpc.AspNetCore;
     using Microsoft.AspNetCore.Builder;
     using Microsoft.Extensions.DependencyInjection;
 
     public class Startup
     {
+        private IConfiguration _configuration;
+
+        public Startup(IConfiguration configuration)
+        {
+            _configuration = configuration;
+        }       
+
         public void ConfigureServices(IServiceCollection services)
         {
             services.AddRpcServices(registerJsonSerializer: false);
             services.AddSingleton<IContentSerializer, Utf8JsonContentSerializer>();
+            services.AddSingleton<IRawDb, RawDb>();
+
+            var appSettings = new AppSettings();
+
+            _configuration.Bind(appSettings);
+
+            services.AddSingleton(appSettings);   
+
+            // for views
+            services.AddControllersWithViews();                
+            var settings = new TextEncoderSettings(UnicodeRanges.BasicLatin, 
+                            UnicodeRanges.Katakana,
+                            UnicodeRanges.Hiragana);
+
+            settings.AllowCharacter('\u2014');  // allow EM DASH through
+            services.AddWebEncoders((options) =>  options.TextEncoderSettings = settings);     
         }
 
         public void Configure(IApplicationBuilder app)
@@ -22,6 +50,9 @@ namespace Benchmarks
                 api.GetMethod("/plaintext", () => "Hello, World!").Raw("text/plain");
 
                 api.GetMethod("/json", () => new { message = "Hello, World!" });
+
+                api.Expose<QueryService>();
+                api.Expose<FortuneService>();
             });
         }
     }

+ 12 - 0
frameworks/CSharp/easyrpc/Benchmarks/Views/Fortunes/Fortunes.cshtml

@@ -0,0 +1,12 @@
+@using Benchmarks.Data
+@model List<Fortune>
+<!DOCTYPE html>
+<html>
+<head><title>Fortunes</title></head>
+<body><table><tr><th>id</th><th>message</th></tr>
+    @foreach (var item in Model)
+    {
+        <tr><td>@item.Id</td><td>@item.Message</td></tr>
+    }
+</table></body>
+</html>

+ 3 - 0
frameworks/CSharp/easyrpc/Benchmarks/appsettings.json

@@ -0,0 +1,3 @@
+{
+  "ConnectionString": "Server=tfb-database;Database=hello_world;User Id=benchmarkdbuser;Password=benchmarkdbpass;Maximum Pool Size=256;NoResetOnClose=true;Enlist=false;Max Auto Prepare=3"  
+}

+ 5 - 0
frameworks/CSharp/easyrpc/README.md

@@ -24,3 +24,8 @@ This includes tests for plaintext and json serialization.
 
 * [Plaintext](Benchmarks/Startup.cs): "/plaintext"
 * [JSON Serialization](Benchmarks/Startup.cs): "/json"
+* [Single query](Benchmarks/Services/QueryService.cs): "/db"
+* [Multiple query](Benchmarks/Services/QueryService.cs): "/queries"
+* [Update query](Benchmarks/Services/QueryService.cs): "/updates"
+* [Caching query](Benchmarks/Services/QueryService.cs): "/cached-worlds"
+* [Fortune](Benchmarks/Services/FortuneService.cs): "/fortunes/fortunes"

+ 5 - 0
frameworks/CSharp/easyrpc/benchmark_config.json

@@ -5,6 +5,11 @@
       "default": {
         "json_url": "/json",
         "plaintext_url": "/plaintext",
+        "db_url": "/db",
+        "query_url": "/queries/",
+        "fortune_url": "/fortunes/fortunes",
+        "update_url": "/updates/",
+        "cached_query_url": "/cached-worlds/",        
         "port": 8080,
         "approach": "Realistic",
         "classification": "Fullstack",