Skip to content

Commit 60c9cd0

Browse files
benaadamsNateBrady23
authored andcommitted
ASP.NET Core idiomatic dataupdates improvements (#3803)
1 parent e65cce0 commit 60c9cd0

File tree

7 files changed

+138
-32
lines changed

7 files changed

+138
-32
lines changed

frameworks/CSharp/aspnetcore/Benchmarks/Data/BatchUpdateString.cs

+39-6
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,55 @@
33

44
using System.Collections.Generic;
55
using System.Linq;
6+
using System.Runtime.CompilerServices;
67

78
namespace Benchmarks.Data
89
{
910
internal class BatchUpdateString
1011
{
11-
public static IList<BatchUpdateString> Strings { get;} =
12-
Enumerable.Range(0, 500)
12+
private const int MaxBatch = 500;
13+
private static string[] _queries = new string[MaxBatch];
14+
15+
public static IList<BatchUpdateString> Strings { get; } =
16+
Enumerable.Range(0, MaxBatch)
1317
.Select(i => new BatchUpdateString
1418
{
1519
Id = $"Id_{i}",
1620
Random = $"Random_{i}",
17-
UpdateQuery = $"UPDATE world SET randomnumber = @Random_{i} WHERE id = @Id_{i};"
21+
BatchSize = i
1822
}).ToArray();
19-
23+
24+
private int BatchSize { get; set; }
2025
public string Id { get; set; }
2126
public string Random { get; set; }
22-
public string UpdateQuery { get; set; }
27+
public string UpdateQuery => _queries[BatchSize] ?? CreateQuery(BatchSize);
28+
29+
[MethodImpl(MethodImplOptions.NoInlining)]
30+
private string CreateQuery(int batchSize)
31+
{
32+
var sb = StringBuilderCache.Acquire();
33+
foreach (var q in Enumerable.Range(0, batchSize + 1)
34+
.Select(i => $"UPDATE world SET randomnumber = @Random_{i} WHERE id = @Id_{i};"))
35+
{
36+
sb.Append(q);
37+
}
38+
var query = sb.ToString();
39+
_queries[batchSize] = query;
40+
return query;
41+
}
42+
43+
public static void Initalize()
44+
{
45+
Observe(Strings[0].UpdateQuery);
46+
Observe(Strings[4].UpdateQuery);
47+
Observe(Strings[9].UpdateQuery);
48+
Observe(Strings[14].UpdateQuery);
49+
Observe(Strings[19].UpdateQuery);
50+
}
51+
52+
[MethodImpl(MethodImplOptions.NoInlining)]
53+
private static void Observe(string query)
54+
{
55+
}
2356
}
24-
}
57+
}

frameworks/CSharp/aspnetcore/Benchmarks/Data/DapperDb.cs

+9-9
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
using System.Collections.Generic;
66
using System.Data.Common;
77
using System.Dynamic;
8-
using System.Text;
98
using System.Threading.Tasks;
109
using Benchmarks.Configuration;
1110
using Dapper;
@@ -15,6 +14,8 @@ namespace Benchmarks.Data
1514
{
1615
public class DapperDb : IDb
1716
{
17+
private static readonly Comparison<World> WorldSortComparison = (a, b) => a.Id.CompareTo(b.Id);
18+
1819
private readonly IRandom _random;
1920
private readonly DbProviderFactory _dbProviderFactory;
2021
private readonly string _connectionString;
@@ -63,37 +64,36 @@ public async Task<World[]> LoadMultipleQueriesRows(int count)
6364

6465
public async Task<World[]> LoadMultipleUpdatesRows(int count)
6566
{
66-
var results = new World[count];
6767
IDictionary<string, object> parameters = new ExpandoObject();
68-
var updateCommand = new StringBuilder(count);
6968

7069
using (var db = _dbProviderFactory.CreateConnection())
7170
{
7271
db.ConnectionString = _connectionString;
7372
await db.OpenAsync();
7473

74+
var results = new World[count];
7575
for (int i = 0; i < count; i++)
7676
{
7777
results[i] = await ReadSingleRow(db);
7878
}
7979

8080
// postgres has problems with deadlocks when these aren't sorted
81-
Array.Sort<World>(results, (a, b) => a.Id.CompareTo(b.Id));
81+
Array.Sort<World>(results, WorldSortComparison);
8282

8383
for (int i = 0; i < count; i++)
8484
{
85+
var strings = BatchUpdateString.Strings[i];
8586
var randomNumber = _random.Next(1, 10001);
86-
parameters[BatchUpdateString.Strings[i].Random] = randomNumber;
87-
parameters[BatchUpdateString.Strings[i].Id] = results[i].Id;
87+
parameters[strings.Random] = randomNumber;
88+
parameters[strings.Id] = results[i].Id;
8889

8990
results[i].RandomNumber = randomNumber;
90-
updateCommand.Append(BatchUpdateString.Strings[i].UpdateQuery);
9191
}
9292

93-
await db.ExecuteAsync(updateCommand.ToString(), parameters);
93+
await db.ExecuteAsync(BatchUpdateString.Strings[results.Length - 1].UpdateQuery, parameters);
94+
return results;
9495
}
9596

96-
return results;
9797
}
9898

9999
public async Task<IEnumerable<Fortune>> LoadFortunesRows()

frameworks/CSharp/aspnetcore/Benchmarks/Data/Random.cs

+14-2
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,31 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5+
using System.Runtime.CompilerServices;
56
using System.Threading;
67

78
namespace Benchmarks.Data
89
{
910
public class DefaultRandom : IRandom
1011
{
1112
private static int nextSeed = 0;
13+
1214
// Random isn't thread safe
13-
private static readonly ThreadLocal<Random> _random = new ThreadLocal<Random>(() => new Random(Interlocked.Increment(ref nextSeed)));
15+
[ThreadStatic]
16+
private static Random _random;
17+
18+
private static Random Random => _random ?? CreateRandom();
19+
20+
[MethodImpl(MethodImplOptions.NoInlining)]
21+
private static Random CreateRandom()
22+
{
23+
_random = new Random(Interlocked.Increment(ref nextSeed));
24+
return _random;
25+
}
1426

1527
public int Next(int minValue, int maxValue)
1628
{
17-
return _random.Value.Next(minValue, maxValue);
29+
return Random.Next(minValue, maxValue);
1830
}
1931
}
2032
}

frameworks/CSharp/aspnetcore/Benchmarks/Data/RawDb.cs

+10-13
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
using System.Collections.Generic;
66
using System.Data;
77
using System.Data.Common;
8-
using System.Text;
98
using System.Threading.Tasks;
109
using Benchmarks.Configuration;
1110
using Microsoft.Extensions.Options;
@@ -14,6 +13,8 @@ namespace Benchmarks.Data
1413
{
1514
public class RawDb : IDb
1615
{
16+
private static readonly Comparison<World> WorldSortComparison = (a, b) => a.Id.CompareTo(b.Id);
17+
1718
private readonly IRandom _random;
1819
private readonly DbProviderFactory _dbProviderFactory;
1920
private readonly string _connectionString;
@@ -89,10 +90,6 @@ public async Task<World[]> LoadMultipleQueriesRows(int count)
8990

9091
public async Task<World[]> LoadMultipleUpdatesRows(int count)
9192
{
92-
var results = new World[count];
93-
94-
var updateCommand = new StringBuilder(count);
95-
9693
using (var db = _dbProviderFactory.CreateConnection())
9794
{
9895
db.ConnectionString = _connectionString;
@@ -101,41 +98,41 @@ public async Task<World[]> LoadMultipleUpdatesRows(int count)
10198
using (var updateCmd = db.CreateCommand())
10299
using (var queryCmd = CreateReadCommand(db))
103100
{
101+
var results = new World[count];
104102
for (int i = 0; i < count; i++)
105103
{
106104
results[i] = await ReadSingleRow(db, queryCmd);
107105
queryCmd.Parameters["@Id"].Value = _random.Next(1, 10001);
108106
}
109107

110108
// Postgres has problems with deadlocks when these aren't sorted
111-
Array.Sort<World>(results, (a, b) => a.Id.CompareTo(b.Id));
109+
Array.Sort<World>(results, WorldSortComparison);
112110

113111
for(int i = 0; i < count; i++)
114112
{
113+
var strings = BatchUpdateString.Strings[i];
115114
var id = updateCmd.CreateParameter();
116-
id.ParameterName = BatchUpdateString.Strings[i].Id;
115+
id.ParameterName = strings.Id;
117116
id.DbType = DbType.Int32;
118117
updateCmd.Parameters.Add(id);
119118

120119
var random = updateCmd.CreateParameter();
121-
random.ParameterName = BatchUpdateString.Strings[i].Random;
120+
random.ParameterName = strings.Random;
122121
random.DbType = DbType.Int32;
123122
updateCmd.Parameters.Add(random);
124123

125124
var randomNumber = _random.Next(1, 10001);
126125
id.Value = results[i].Id;
127126
random.Value = randomNumber;
128127
results[i].RandomNumber = randomNumber;
129-
130-
updateCommand.Append(BatchUpdateString.Strings[i].UpdateQuery);
131128
}
132129

133-
updateCmd.CommandText = updateCommand.ToString();
130+
updateCmd.CommandText = BatchUpdateString.Strings[results.Length - 1].UpdateQuery;
131+
134132
await updateCmd.ExecuteNonQueryAsync();
133+
return results;
135134
}
136135
}
137-
138-
return results;
139136
}
140137

141138
public async Task<IEnumerable<Fortune>> LoadFortunesRows()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Text;
6+
7+
namespace Benchmarks.Data
8+
{
9+
internal static class StringBuilderCache
10+
{
11+
private const int DefaultCapacity = 1386;
12+
private const int MaxBuilderSize = DefaultCapacity * 3;
13+
14+
[ThreadStatic]
15+
private static StringBuilder t_cachedInstance;
16+
17+
/// <summary>Get a StringBuilder for the specified capacity.</summary>
18+
/// <remarks>If a StringBuilder of an appropriate size is cached, it will be returned and the cache emptied.</remarks>
19+
public static StringBuilder Acquire(int capacity = DefaultCapacity)
20+
{
21+
if (capacity <= MaxBuilderSize)
22+
{
23+
StringBuilder sb = t_cachedInstance;
24+
if (capacity < DefaultCapacity)
25+
{
26+
capacity = DefaultCapacity;
27+
}
28+
29+
if (sb != null)
30+
{
31+
// Avoid stringbuilder block fragmentation by getting a new StringBuilder
32+
// when the requested size is larger than the current capacity
33+
if (capacity <= sb.Capacity)
34+
{
35+
t_cachedInstance = null;
36+
sb.Clear();
37+
return sb;
38+
}
39+
}
40+
}
41+
return new StringBuilder(capacity);
42+
}
43+
44+
public static void Release(StringBuilder sb)
45+
{
46+
if (sb.Capacity <= MaxBuilderSize)
47+
{
48+
t_cachedInstance = sb;
49+
}
50+
}
51+
52+
public static string GetStringAndRelease(StringBuilder sb)
53+
{
54+
string result = sb.ToString();
55+
Release(sb);
56+
return result;
57+
}
58+
}
59+
}

frameworks/CSharp/aspnetcore/Benchmarks/Middleware/MiddlewareHelpers.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public static async Task RenderFortunesHtml(IEnumerable<Fortune> model, HttpCont
3535
httpContext.Response.StatusCode = StatusCodes.Status200OK;
3636
httpContext.Response.ContentType = "text/html; charset=UTF-8";
3737

38-
var sb = new StringBuilder();
38+
var sb = StringBuilderCache.Acquire();
3939
sb.Append("<!DOCTYPE html><html><head><title>Fortunes</title></head><body><table><tr><th>id</th><th>message</th></tr>");
4040
foreach (var item in model)
4141
{
@@ -47,7 +47,7 @@ public static async Task RenderFortunesHtml(IEnumerable<Fortune> model, HttpCont
4747
}
4848

4949
sb.Append("</table></body></html>");
50-
var response = sb.ToString();
50+
var response = StringBuilderCache.GetStringAndRelease(sb);
5151
// fortunes includes multibyte characters so response.Length is incorrect
5252
httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(response);
5353
await httpContext.Response.WriteAsync(response);

frameworks/CSharp/aspnetcore/Benchmarks/Startup.cs

+5
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,11 @@ public void ConfigureServices(IServiceCollection services)
9191
services.AddScoped<DapperDb>();
9292
}
9393

94+
if (Scenarios.Any("Update"))
95+
{
96+
BatchUpdateString.Initalize();
97+
}
98+
9499
if (Scenarios.Any("Fortunes"))
95100
{
96101
var settings = new TextEncoderSettings(UnicodeRanges.BasicLatin, UnicodeRanges.Katakana, UnicodeRanges.Hiragana);

0 commit comments

Comments
 (0)