Browse Source

Add ASP.NET Core RAW MySQL test (#6566)

* Generalize as little as possible, to support more databases.

* Add support for MySQL via MySqlConnector.

* Prepare SELECT statements (async).

* Seal database specific implementations.

* Revert "Seal database specific implementations."

This reverts commit 6b048be7

* Use preprocessor directives for perf reasons, instead of an interface.

* Simplify project file.

* Use appsettings from the correct subdirectory.
Clarify exception message.

* Simplify selection of database specific RawDb class.

* Retrigger CI.
Laurents Meyer 4 years ago
parent
commit
8708e6e94d

+ 4 - 0
frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkApplication.Caching.cs

@@ -1,6 +1,8 @@
 // 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.
 
+#if DATABASE
+
 using System.IO.Pipelines;
 using System.Threading.Tasks;
 
@@ -14,3 +16,5 @@ namespace PlatformBenchmarks
         }
     }
 }
+
+#endif

+ 4 - 0
frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkApplication.Fortunes.cs

@@ -1,6 +1,8 @@
 // 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.
 
+#if DATABASE
+
 using System.Collections.Generic;
 using System.IO.Pipelines;
 using System.Text.Encodings.Web;
@@ -51,3 +53,5 @@ namespace PlatformBenchmarks
         }
     }
 }
+
+#endif

+ 4 - 0
frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkApplication.MultipleQueries.cs

@@ -1,6 +1,8 @@
 // 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.
 
+#if DATABASE
+
 using System.IO.Pipelines;
 using System.Text.Json;
 using System.Threading.Tasks;
@@ -39,3 +41,5 @@ namespace PlatformBenchmarks
         }
     }
 }
+
+#endif

+ 4 - 0
frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkApplication.SingleQuery.cs

@@ -1,6 +1,8 @@
 // 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.
 
+#if DATABASE
+
 using System.IO.Pipelines;
 using System.Text.Json;
 using System.Threading.Tasks;
@@ -39,3 +41,5 @@ namespace PlatformBenchmarks
         }
     }
 }
+
+#endif

+ 4 - 0
frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkApplication.Updates.cs

@@ -1,6 +1,8 @@
 // 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.
 
+#if DATABASE
+
 using System.IO.Pipelines;
 using System.Text.Json;
 using System.Threading.Tasks;
@@ -39,3 +41,5 @@ namespace PlatformBenchmarks
         }
     }
 }
+
+#endif

+ 2 - 0
frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkApplication.cs

@@ -44,7 +44,9 @@ namespace PlatformBenchmarks
         private readonly static AsciiString _fortunesTableEnd = "</table></body></html>";
         private readonly static AsciiString _contentLengthGap = new string(' ', 4);
 
+#if DATABASE
         public static RawDb Db { get; set; }
+#endif
 
         [ThreadStatic]
         private static Utf8JsonWriter t_writer;

+ 275 - 0
frameworks/CSharp/aspnetcore/PlatformBenchmarks/Data/Providers/RawDbMySqlConnector.cs

@@ -0,0 +1,275 @@
+// 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.
+
+#if MYSQLCONNECTOR
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Caching.Memory;
+using MySqlConnector;
+
+namespace PlatformBenchmarks
+{
+    // Is semantically identical to RawDbNpgsql.cs.
+    // If you are changing RawDbMySqlConnector.cs, also consider changing RawDbNpgsql.cs.
+    public class RawDb
+    {
+        private readonly ConcurrentRandom _random;
+        private readonly string _connectionString;
+        private readonly MemoryCache _cache = new MemoryCache(
+            new MemoryCacheOptions()
+            {
+                ExpirationScanFrequency = TimeSpan.FromMinutes(60)
+            });
+
+        public RawDb(ConcurrentRandom random, AppSettings appSettings)
+        {
+            _random = random;
+            _connectionString = appSettings.ConnectionString;
+        }
+
+        public async Task<World> LoadSingleQueryRow()
+        {
+            using (var db = new MySqlConnection(_connectionString))
+            {
+                await db.OpenAsync();
+
+                var (cmd, _) = await CreateReadCommandAsync(db);
+                using (cmd)
+                {
+                    return await ReadSingleRow(cmd);
+                }
+            }
+        }
+
+        public async Task<World[]> LoadMultipleQueriesRows(int count)
+        {
+            var result = new World[count];
+
+            using (var db = new MySqlConnection(_connectionString))
+            {
+                await db.OpenAsync();
+
+                var (cmd, idParameter) = await CreateReadCommandAsync(db);
+                using (cmd)
+                {
+                    for (int i = 0; i < result.Length; i++)
+                    {
+                        result[i] = await ReadSingleRow(cmd);
+                        idParameter.Value = _random.Next(1, 10001);
+                    }
+                }
+            }
+
+            return result;
+        }
+
+        public Task<World[]> LoadCachedQueries(int count)
+        {
+            var result = new World[count];
+            var cacheKeys = _cacheKeys;
+            var cache = _cache;
+            var random = _random;
+            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(id, i, count, this, result);
+                }
+            }
+
+            return Task.FromResult(result);
+
+            static async Task<World[]> LoadUncachedQueries(int id, int i, int count, RawDb rawdb, World[] result)
+            {
+                using (var db = new MySqlConnection(rawdb._connectionString))
+                {
+                    await db.OpenAsync();
+
+                    var (cmd, idParameter) = await rawdb.CreateReadCommandAsync(db);
+                    using (cmd)
+                    {
+                        Func<ICacheEntry, Task<CachedWorld>> create = async (entry) =>
+                        {
+                            return await rawdb.ReadSingleRow(cmd);
+                        };
+
+                        var cacheKeys = _cacheKeys;
+                        var key = cacheKeys[id];
+
+                        idParameter.Value = id;
+
+                        for (; i < result.Length; i++)
+                        {
+                            var data = await rawdb._cache.GetOrCreateAsync<CachedWorld>(key, create);
+                            result[i] = data;
+
+                            id = rawdb._random.Next(1, 10001);
+                            idParameter.Value = id;
+                            key = cacheKeys[id];
+                        }
+                    }
+                }
+
+                return result;
+            }
+        }
+
+        public async Task PopulateCache()
+        {
+            using (var db = new MySqlConnection(_connectionString))
+            {
+                await db.OpenAsync();
+
+                var (cmd, idParameter) = await CreateReadCommandAsync(db);
+                using (cmd)
+                {
+                    var cacheKeys = _cacheKeys;
+                    var cache = _cache;
+                    for (var i = 1; i < 10001; i++)
+                    {
+                        idParameter.Value = i;
+                        cache.Set<CachedWorld>(cacheKeys[i], await ReadSingleRow(cmd));
+                    }
+                }
+            }
+
+            Console.WriteLine("Caching Populated");
+        }
+
+        public async Task<World[]> LoadMultipleUpdatesRows(int count)
+        {
+            var results = new World[count];
+
+            using (var db = new MySqlConnection(_connectionString))
+            {
+                await db.OpenAsync();
+
+                var (queryCmd, queryParameter) = await CreateReadCommandAsync(db);
+                using (queryCmd)
+                {
+                    for (int i = 0; i < results.Length; i++)
+                    {
+                        results[i] = await ReadSingleRow(queryCmd);
+                        queryParameter.Value = _random.Next(1, 10001);
+                    }
+                }
+
+                using (var updateCmd = new MySqlCommand(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 MySqlParameter(ids[i], results[i].Id));
+                        updateCmd.Parameters.Add(new MySqlParameter(randoms[i], randomNumber));
+
+                        results[i].RandomNumber = randomNumber;
+                    }
+
+                    await updateCmd.ExecuteNonQueryAsync();
+                }
+            }
+
+            return results;
+        }
+
+        public async Task<List<Fortune>> LoadFortunesRows()
+        {
+            var result = new List<Fortune>();
+
+            using (var db = new MySqlConnection(_connectionString))
+            {
+                await db.OpenAsync();
+
+                using (var cmd = new MySqlCommand("SELECT id, message FROM fortune", db))
+                {
+                    await cmd.PrepareAsync();
+                    
+                    using (var rdr = await cmd.ExecuteReaderAsync())
+                    {
+                        while (await rdr.ReadAsync())
+                        {
+                            result.Add(
+                                new Fortune
+                                (
+                                    id: rdr.GetInt32(0),
+                                    message: rdr.GetString(1)
+                                ));
+                        }
+                    }
+                }
+            }
+
+            result.Add(new Fortune(id: 0, message: "Additional fortune added at request time." ));
+            result.Sort();
+
+            return result;
+        }
+
+        private async Task<(MySqlCommand readCmd, MySqlParameter idParameter)> CreateReadCommandAsync(MySqlConnection connection)
+        {
+            var cmd = new MySqlCommand("SELECT id, randomnumber FROM world WHERE id = @Id", connection);
+            var parameter = new MySqlParameter("@Id", _random.Next(1, 10001));
+
+            cmd.Parameters.Add(parameter);
+
+            await cmd.PrepareAsync();
+            
+            return (cmd, parameter);
+        }
+        
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        private async Task<World> ReadSingleRow(MySqlCommand 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();
+        }
+    }
+}
+
+#endif

+ 6 - 0
frameworks/CSharp/aspnetcore/PlatformBenchmarks/Data/RawDb.cs → frameworks/CSharp/aspnetcore/PlatformBenchmarks/Data/Providers/RawDbNpgsql.cs

@@ -1,6 +1,8 @@
 // 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.
 
+#if NPGSQL
+
 using System;
 using System.Collections.Generic;
 using System.Linq;
@@ -11,6 +13,8 @@ using Npgsql;
 
 namespace PlatformBenchmarks
 {
+    // Is semantically identical to RawDbMySqlConnector.cs.
+    // If you are changing RawDbNpgsql.cs, also consider changing RawDbMySqlConnector.cs.
     public class RawDb
     {
         private readonly ConcurrentRandom _random;
@@ -260,3 +264,5 @@ namespace PlatformBenchmarks
         }
     }
 }
+
+#endif

+ 7 - 3
frameworks/CSharp/aspnetcore/PlatformBenchmarks/PlatformBenchmarks.csproj

@@ -7,14 +7,18 @@
   </PropertyGroup>
   
   <PropertyGroup>
-    <DefineConstants Condition=" '$(IsDatabase)' == 'true' ">$(DefineConstants);DATABASE</DefineConstants>
+    <DefineConstants Condition=" '$(DatabaseProvider)' != '' ">$(DefineConstants);DATABASE</DefineConstants>
+    <DefineConstants Condition=" '$(DatabaseProvider)' == 'Npgsql' ">$(DefineConstants);NPGSQL</DefineConstants>
+    <DefineConstants Condition=" '$(DatabaseProvider)' == 'MySqlConnector' ">$(DefineConstants);MYSQLCONNECTOR</DefineConstants>
   </PropertyGroup>
   
   <ItemGroup>
-      <None Include="appsettings.json" CopyToOutputDirectory="PreserveNewest" />
+    <PackageReference Condition=" '$(DatabaseProvider)' == 'Npgsql' " Include="Npgsql" Version="5.0.4" />
+    <PackageReference Condition=" '$(DatabaseProvider)' == 'MySqlConnector' " Include="MySqlConnector" Version="1.3.8" />
   </ItemGroup>
 
   <ItemGroup>
-    <PackageReference Include="Npgsql" Version="5.0.4" />
+      <None Include="appsettings.json" CopyToOutputDirectory="PreserveNewest" />
   </ItemGroup>
+
 </Project>

+ 2 - 5
frameworks/CSharp/aspnetcore/PlatformBenchmarks/Program.cs

@@ -2,13 +2,9 @@
 // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 
 using System;
-using System.Net;
 using System.Threading.Tasks;
 using Microsoft.AspNetCore.Hosting;
 using Microsoft.Extensions.Configuration;
-#if DATABASE
-using Npgsql;
-#endif
 
 namespace PlatformBenchmarks
 {
@@ -55,7 +51,8 @@ namespace PlatformBenchmarks
             Console.WriteLine($"Database: {appSettings.Database}");
             Console.WriteLine($"ConnectionString: {appSettings.ConnectionString}");
 
-            if (appSettings.Database == DatabaseServer.PostgreSql)
+            if (appSettings.Database is DatabaseServer.PostgreSql
+                                     or DatabaseServer.MySql)
             {
                 BenchmarkApplication.Db = new RawDb(new ConcurrentRandom(), appSettings);
             }

+ 0 - 1
frameworks/CSharp/aspnetcore/PlatformBenchmarks/Startup.cs

@@ -10,7 +10,6 @@ namespace PlatformBenchmarks
     {
         public void Configure(IApplicationBuilder app)
         {
-            
         }
     }
 }

+ 4 - 0
frameworks/CSharp/aspnetcore/PlatformBenchmarks/appsettings.mysql.json

@@ -0,0 +1,4 @@
+{
+  "ConnectionString": "Server=tfb-database;Database=hello_world;User Id=benchmarkdbuser;Password=benchmarkdbpass;Maximum Pool Size=1024;SslMode=None;ConnectionReset=false;ConnectionIdlePingTime=900;ConnectionIdleTimeout=0;AutoEnlist=false;DefaultCommandTimeout=0;ConnectionTimeout=0;IgnorePrepare=false;",
+  "Database": "mysql"
+}

+ 2 - 2
frameworks/CSharp/aspnetcore/aspcore-ado-my.dockerfile

@@ -1,13 +1,13 @@
 FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build
 WORKDIR /app
 COPY PlatformBenchmarks .
-RUN dotnet publish -c Release -o out /p:IsDatabase=true
+RUN dotnet publish -c Release -o out /p:DatabaseProvider=MySqlConnector
 
 FROM mcr.microsoft.com/dotnet/aspnet:5.0 AS runtime
 ENV ASPNETCORE_URLS http://+:8080
 WORKDIR /app
 COPY --from=build /app/out ./
-COPY Benchmarks/appsettings.mysql.json ./appsettings.json
+COPY PlatformBenchmarks/appsettings.mysql.json ./appsettings.json
 
 EXPOSE 8080
 

+ 1 - 1
frameworks/CSharp/aspnetcore/aspcore-ado-pg-up.dockerfile

@@ -1,7 +1,7 @@
 FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build
 WORKDIR /app
 COPY PlatformBenchmarks .
-RUN dotnet publish -c Release -o out /p:IsDatabase=true
+RUN dotnet publish -c Release -o out /p:DatabaseProvider=Npgsql
 
 FROM mcr.microsoft.com/dotnet/aspnet:5.0 AS runtime
 ENV ASPNETCORE_URLS http://+:8080

+ 1 - 1
frameworks/CSharp/aspnetcore/aspcore-ado-pg.dockerfile

@@ -1,7 +1,7 @@
 FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build
 WORKDIR /app
 COPY PlatformBenchmarks .
-RUN dotnet publish -c Release -o out /p:IsDatabase=true
+RUN dotnet publish -c Release -o out /p:DatabaseProvider=Npgsql
 
 FROM mcr.microsoft.com/dotnet/aspnet:5.0 AS runtime
 ENV ASPNETCORE_URLS http://+:8080

+ 22 - 0
frameworks/CSharp/aspnetcore/benchmark_config.json

@@ -389,6 +389,28 @@
       "display_name": "ASP.NET Core [MVC, My, Dapper]",
       "notes": "",
       "versus": "aspcore-ado-my"
+    },
+    "ado-my": {
+      "fortune_url": "/fortunes",
+      "db_url": "/db",
+      "query_url": "/queries/",
+      "update_url": "/updates/",
+      "cached_query_url": "/cached-worlds/",
+      "port": 8080,
+      "approach": "Realistic",
+      "classification": "Platform",
+      "database": "MySQL",
+      "framework": "ASP.NET Core",
+      "language": "C#",
+      "orm": "Raw",
+      "platform": ".NET",
+      "flavor": "CoreCLR",
+      "webserver": "Kestrel",
+      "os": "Linux",
+      "database_os": "Linux",
+      "display_name": "ASP.NET Core [Platform, My]",
+      "notes": "",
+      "versus": "aspcore-ado-my"
     }
   }]
 }

+ 16 - 0
frameworks/CSharp/aspnetcore/config.toml

@@ -16,6 +16,22 @@ platform = ".NET"
 webserver = "Kestrel"
 versus = "aspcore-ado-pg"
 
+[ado-my]
+urls.db = "/db"
+urls.query = "/queries/"
+urls.update = "/updates/"
+urls.fortune = "/fortunes"
+urls.cached_query = "/cached-worlds/"
+approach = "Realistic"
+classification = "Platform"
+database = "MySQL"
+database_os = "Linux"
+os = "Linux"
+orm = "Raw"
+platform = ".NET"
+webserver = "Kestrel"
+versus = "aspcore-ado-my"
+
 [mw-ef-pg]
 urls.db = "/db/ef"
 urls.query = "/queries/ef?queries="