Bläddra i källkod

ASP.NET Core idiomatic dataupdates improvements (#3803)

Ben Adams 7 år sedan
förälder
incheckning
60c9cd0ef7

+ 39 - 6
frameworks/CSharp/aspnetcore/Benchmarks/Data/BatchUpdateString.cs

@@ -3,22 +3,55 @@
 
 
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Linq;
 using System.Linq;
+using System.Runtime.CompilerServices;
 
 
 namespace Benchmarks.Data
 namespace Benchmarks.Data
 {
 {
     internal class BatchUpdateString
     internal class BatchUpdateString
     {
     {
-        public static IList<BatchUpdateString> Strings { get;} = 
-            Enumerable.Range(0, 500)
+        private const int MaxBatch = 500;
+        private static string[] _queries = new string[MaxBatch];
+
+        public static IList<BatchUpdateString> Strings { get; } =
+            Enumerable.Range(0, MaxBatch)
                       .Select(i => new BatchUpdateString
                       .Select(i => new BatchUpdateString
                       {
                       {
                           Id = $"Id_{i}",
                           Id = $"Id_{i}",
                           Random = $"Random_{i}",
                           Random = $"Random_{i}",
-                          UpdateQuery = $"UPDATE world SET randomnumber = @Random_{i} WHERE id = @Id_{i};"
+                          BatchSize = i
                       }).ToArray();
                       }).ToArray();
-                        
+
+        private int BatchSize { get; set; }
         public string Id { get; set; }
         public string Id { get; set; }
         public string Random { get; set; }
         public string Random { get; set; }
-        public string UpdateQuery { get; set; }
+        public string UpdateQuery => _queries[BatchSize] ?? CreateQuery(BatchSize);
+
+        [MethodImpl(MethodImplOptions.NoInlining)]
+        private string CreateQuery(int batchSize)
+        {
+            var sb = StringBuilderCache.Acquire();
+            foreach (var q in Enumerable.Range(0, batchSize + 1)
+                .Select(i => $"UPDATE world SET randomnumber = @Random_{i} WHERE id = @Id_{i};"))
+            {
+                sb.Append(q);
+            }
+            var query = sb.ToString();
+            _queries[batchSize] = query;
+            return query;
+        }
+
+        public static void Initalize()
+        {
+            Observe(Strings[0].UpdateQuery);
+            Observe(Strings[4].UpdateQuery);
+            Observe(Strings[9].UpdateQuery);
+            Observe(Strings[14].UpdateQuery);
+            Observe(Strings[19].UpdateQuery);
+        }
+
+        [MethodImpl(MethodImplOptions.NoInlining)]
+        private static void Observe(string query)
+        {
+        }
     }
     }
-}
+}

+ 9 - 9
frameworks/CSharp/aspnetcore/Benchmarks/Data/DapperDb.cs

@@ -5,7 +5,6 @@ using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Data.Common;
 using System.Data.Common;
 using System.Dynamic;
 using System.Dynamic;
-using System.Text;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Benchmarks.Configuration;
 using Benchmarks.Configuration;
 using Dapper;
 using Dapper;
@@ -15,6 +14,8 @@ namespace Benchmarks.Data
 {
 {
     public class DapperDb : IDb
     public class DapperDb : IDb
     {
     {
+        private static readonly Comparison<World> WorldSortComparison = (a, b) => a.Id.CompareTo(b.Id);
+
         private readonly IRandom _random;
         private readonly IRandom _random;
         private readonly DbProviderFactory _dbProviderFactory;
         private readonly DbProviderFactory _dbProviderFactory;
         private readonly string _connectionString;
         private readonly string _connectionString;
@@ -63,37 +64,36 @@ namespace Benchmarks.Data
 
 
         public async Task<World[]> LoadMultipleUpdatesRows(int count)
         public async Task<World[]> LoadMultipleUpdatesRows(int count)
         {
         {
-            var results = new World[count];
             IDictionary<string, object> parameters = new ExpandoObject();
             IDictionary<string, object> parameters = new ExpandoObject();
-            var updateCommand = new StringBuilder(count);
 
 
             using (var db = _dbProviderFactory.CreateConnection())
             using (var db = _dbProviderFactory.CreateConnection())
             {
             {
                 db.ConnectionString = _connectionString;
                 db.ConnectionString = _connectionString;
                 await db.OpenAsync();
                 await db.OpenAsync();
 
 
+                var results = new World[count];
                 for (int i = 0; i < count; i++)
                 for (int i = 0; i < count; i++)
                 {
                 {
                     results[i] = await ReadSingleRow(db);
                     results[i] = await ReadSingleRow(db);
                 }
                 }
 
 
                 // postgres has problems with deadlocks when these aren't sorted
                 // postgres has problems with deadlocks when these aren't sorted
-                Array.Sort<World>(results, (a, b) => a.Id.CompareTo(b.Id));
+                Array.Sort<World>(results, WorldSortComparison);
 
 
                 for (int i = 0; i < count; i++)
                 for (int i = 0; i < count; i++)
                 {
                 {
+                    var strings = BatchUpdateString.Strings[i];
                     var randomNumber = _random.Next(1, 10001);
                     var randomNumber = _random.Next(1, 10001);
-                    parameters[BatchUpdateString.Strings[i].Random] = randomNumber;
-                    parameters[BatchUpdateString.Strings[i].Id] = results[i].Id;
+                    parameters[strings.Random] = randomNumber;
+                    parameters[strings.Id] = results[i].Id;
 
 
                     results[i].RandomNumber = randomNumber;
                     results[i].RandomNumber = randomNumber;
-                    updateCommand.Append(BatchUpdateString.Strings[i].UpdateQuery);
                 }
                 }
 
 
-                await db.ExecuteAsync(updateCommand.ToString(), parameters);
+                await db.ExecuteAsync(BatchUpdateString.Strings[results.Length - 1].UpdateQuery, parameters);
+                return results;
             }
             }
 
 
-            return results;
         }
         }
 
 
         public async Task<IEnumerable<Fortune>> LoadFortunesRows()
         public async Task<IEnumerable<Fortune>> LoadFortunesRows()

+ 14 - 2
frameworks/CSharp/aspnetcore/Benchmarks/Data/Random.cs

@@ -2,6 +2,7 @@
 // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 
 // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 
 
 
 using System;
 using System;
+using System.Runtime.CompilerServices;
 using System.Threading;
 using System.Threading;
 
 
 namespace Benchmarks.Data
 namespace Benchmarks.Data
@@ -9,12 +10,23 @@ namespace Benchmarks.Data
     public class DefaultRandom : IRandom
     public class DefaultRandom : IRandom
     {
     {
         private static int nextSeed = 0;
         private static int nextSeed = 0;
+
         // Random isn't thread safe
         // Random isn't thread safe
-        private static readonly ThreadLocal<Random> _random = new ThreadLocal<Random>(() => new Random(Interlocked.Increment(ref nextSeed)));
+        [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)
         public int Next(int minValue, int maxValue)
         {
         {
-            return _random.Value.Next(minValue, maxValue);
+            return Random.Next(minValue, maxValue);
         }
         }
     }
     }
 }
 }

+ 10 - 13
frameworks/CSharp/aspnetcore/Benchmarks/Data/RawDb.cs

@@ -5,7 +5,6 @@ using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Data;
 using System.Data;
 using System.Data.Common;
 using System.Data.Common;
-using System.Text;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Benchmarks.Configuration;
 using Benchmarks.Configuration;
 using Microsoft.Extensions.Options;
 using Microsoft.Extensions.Options;
@@ -14,6 +13,8 @@ namespace Benchmarks.Data
 {
 {
     public class RawDb : IDb
     public class RawDb : IDb
     {
     {
+        private static readonly Comparison<World> WorldSortComparison = (a, b) => a.Id.CompareTo(b.Id);
+
         private readonly IRandom _random;
         private readonly IRandom _random;
         private readonly DbProviderFactory _dbProviderFactory;
         private readonly DbProviderFactory _dbProviderFactory;
         private readonly string _connectionString;
         private readonly string _connectionString;
@@ -89,10 +90,6 @@ namespace Benchmarks.Data
 
 
         public async Task<World[]> LoadMultipleUpdatesRows(int count)
         public async Task<World[]> LoadMultipleUpdatesRows(int count)
         {
         {
-            var results = new World[count];
-
-            var updateCommand = new StringBuilder(count);
-
             using (var db = _dbProviderFactory.CreateConnection())
             using (var db = _dbProviderFactory.CreateConnection())
             {
             {
                 db.ConnectionString = _connectionString;
                 db.ConnectionString = _connectionString;
@@ -101,6 +98,7 @@ namespace Benchmarks.Data
                 using (var updateCmd = db.CreateCommand())
                 using (var updateCmd = db.CreateCommand())
                 using (var queryCmd = CreateReadCommand(db))
                 using (var queryCmd = CreateReadCommand(db))
                 {
                 {
+                    var results = new World[count];
                     for (int i = 0; i < count; i++)
                     for (int i = 0; i < count; i++)
                     {
                     {
                         results[i] = await ReadSingleRow(db, queryCmd);
                         results[i] = await ReadSingleRow(db, queryCmd);
@@ -108,17 +106,18 @@ namespace Benchmarks.Data
                     }
                     }
 
 
                     // Postgres has problems with deadlocks when these aren't sorted
                     // Postgres has problems with deadlocks when these aren't sorted
-                    Array.Sort<World>(results, (a, b) => a.Id.CompareTo(b.Id));
+                    Array.Sort<World>(results, WorldSortComparison);
 
 
                     for(int i = 0; i < count; i++)
                     for(int i = 0; i < count; i++)
                     {
                     {
+                        var strings = BatchUpdateString.Strings[i];
                         var id = updateCmd.CreateParameter();
                         var id = updateCmd.CreateParameter();
-                        id.ParameterName = BatchUpdateString.Strings[i].Id;
+                        id.ParameterName = strings.Id;
                         id.DbType = DbType.Int32;
                         id.DbType = DbType.Int32;
                         updateCmd.Parameters.Add(id);
                         updateCmd.Parameters.Add(id);
 
 
                         var random = updateCmd.CreateParameter();
                         var random = updateCmd.CreateParameter();
-                        random.ParameterName = BatchUpdateString.Strings[i].Random;
+                        random.ParameterName = strings.Random;
                         random.DbType = DbType.Int32;
                         random.DbType = DbType.Int32;
                         updateCmd.Parameters.Add(random);
                         updateCmd.Parameters.Add(random);
 
 
@@ -126,16 +125,14 @@ namespace Benchmarks.Data
                         id.Value = results[i].Id;
                         id.Value = results[i].Id;
                         random.Value = randomNumber;
                         random.Value = randomNumber;
                         results[i].RandomNumber = randomNumber;
                         results[i].RandomNumber = randomNumber;
-
-                        updateCommand.Append(BatchUpdateString.Strings[i].UpdateQuery);
                     }
                     }
 
 
-                    updateCmd.CommandText = updateCommand.ToString();
+                    updateCmd.CommandText = BatchUpdateString.Strings[results.Length - 1].UpdateQuery;
+
                     await updateCmd.ExecuteNonQueryAsync();
                     await updateCmd.ExecuteNonQueryAsync();
+                    return results;
                 }
                 }
             }
             }
-
-            return results;
         }
         }
 
 
         public async Task<IEnumerable<Fortune>> LoadFortunesRows()
         public async Task<IEnumerable<Fortune>> LoadFortunesRows()

+ 59 - 0
frameworks/CSharp/aspnetcore/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;
+        }
+    }
+}

+ 2 - 2
frameworks/CSharp/aspnetcore/Benchmarks/Middleware/MiddlewareHelpers.cs

@@ -35,7 +35,7 @@ namespace Benchmarks.Middleware
             httpContext.Response.StatusCode = StatusCodes.Status200OK;
             httpContext.Response.StatusCode = StatusCodes.Status200OK;
             httpContext.Response.ContentType = "text/html; charset=UTF-8";
             httpContext.Response.ContentType = "text/html; charset=UTF-8";
 
 
-            var sb = new StringBuilder();
+            var sb = StringBuilderCache.Acquire();
             sb.Append("<!DOCTYPE html><html><head><title>Fortunes</title></head><body><table><tr><th>id</th><th>message</th></tr>");
             sb.Append("<!DOCTYPE html><html><head><title>Fortunes</title></head><body><table><tr><th>id</th><th>message</th></tr>");
             foreach (var item in model)
             foreach (var item in model)
             {
             {
@@ -47,7 +47,7 @@ namespace Benchmarks.Middleware
             }
             }
 
 
             sb.Append("</table></body></html>");
             sb.Append("</table></body></html>");
-            var response = sb.ToString();
+            var response = StringBuilderCache.GetStringAndRelease(sb);
             // fortunes includes multibyte characters so response.Length is incorrect
             // fortunes includes multibyte characters so response.Length is incorrect
             httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(response);
             httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(response);
             await httpContext.Response.WriteAsync(response);
             await httpContext.Response.WriteAsync(response);

+ 5 - 0
frameworks/CSharp/aspnetcore/Benchmarks/Startup.cs

@@ -91,6 +91,11 @@ namespace Benchmarks
                 services.AddScoped<DapperDb>();
                 services.AddScoped<DapperDb>();
             }
             }
 
 
+            if (Scenarios.Any("Update"))
+            {
+                BatchUpdateString.Initalize();
+            }
+
             if (Scenarios.Any("Fortunes"))
             if (Scenarios.Any("Fortunes"))
             {
             {
                 var settings = new TextEncoderSettings(UnicodeRanges.BasicLatin, UnicodeRanges.Katakana, UnicodeRanges.Hiragana);
                 var settings = new TextEncoderSettings(UnicodeRanges.BasicLatin, UnicodeRanges.Katakana, UnicodeRanges.Hiragana);