Skip to content

Commit 068454d

Browse files
rojifranz1981
authored andcommitted
Updates to aspnetcore PG database access (TechEmpower#8005)
* Some code cleanup * Updates to aspnetcore PG database access * Use batching with Sync error barriers in Updates * Use NpgsqlDataSource * Use typed NpgsqlParameter<T> everywhere * Use positional parameter placeholders everywhere ($1, $2) * Stop UTF8 decoding/reencoding in Fortunes platform * Update Fortunes to use Razor templating * Turn of SQL parsing/rewriting for Fortunes
1 parent 7558579 commit 068454d

23 files changed

+573
-231
lines changed

frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkApplication.Caching.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ namespace PlatformBenchmarks;
1010

1111
public partial class BenchmarkApplication
1212
{
13-
private async Task Caching(PipeWriter pipeWriter, int count)
13+
private static async Task Caching(PipeWriter pipeWriter, int count)
1414
{
1515
OutputMultipleQueries(pipeWriter, await Db.LoadCachedQueries(count), SerializerContext.CachedWorldArray);
1616
}

frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkApplication.Fortunes.cs

Lines changed: 41 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,53 +3,63 @@
33

44
#if DATABASE
55

6-
using System.Collections.Generic;
76
using System.IO.Pipelines;
8-
using System.Text.Encodings.Web;
9-
using System.Threading.Tasks;
7+
using System.Runtime.CompilerServices;
8+
using RazorSlices;
109

1110
namespace PlatformBenchmarks
1211
{
1312
public partial class BenchmarkApplication
1413
{
15-
private static ReadOnlySpan<byte> _fortunesPreamble =>
16-
"HTTP/1.1 200 OK\r\n"u8 +
17-
"Server: K\r\n"u8 +
18-
"Content-Type: text/html; charset=UTF-8\r\n"u8 +
19-
"Content-Length: "u8;
20-
2114
private async Task Fortunes(PipeWriter pipeWriter)
2215
{
23-
OutputFortunes(pipeWriter, await Db.LoadFortunesRows());
16+
await OutputFortunes(pipeWriter, await Db.LoadFortunesRows(), FortunesTemplateFactory);
2417
}
2518

26-
private void OutputFortunes(PipeWriter pipeWriter, List<Fortune> model)
19+
private ValueTask OutputFortunes<TModel>(PipeWriter pipeWriter, TModel model, SliceFactory<TModel> templateFactory)
2720
{
28-
var writer = GetWriter(pipeWriter, sizeHint: 1600); // in reality it's 1361
29-
30-
writer.Write(_fortunesPreamble);
31-
32-
var lengthWriter = writer;
33-
writer.Write(_contentLengthGap);
21+
// Render headers
22+
var preamble = """
23+
HTTP/1.1 200 OK
24+
Server: K
25+
Content-Type: text/html; charset=utf-8
26+
Transfer-Encoding: chunked
27+
"""u8;
28+
var headersLength = preamble.Length + DateHeader.HeaderBytes.Length;
29+
var headersSpan = pipeWriter.GetSpan(headersLength);
30+
preamble.CopyTo(headersSpan);
31+
DateHeader.HeaderBytes.CopyTo(headersSpan[preamble.Length..]);
32+
pipeWriter.Advance(headersLength);
3433

35-
// Date header
36-
writer.Write(DateHeader.HeaderBytes);
34+
// Render body
35+
var template = templateFactory(model);
36+
// Kestrel PipeWriter span size is 4K, headers above already written to first span & template output is ~1350 bytes,
37+
// so 2K chunk size should result in only a single span and chunk being used.
38+
var chunkedWriter = GetChunkedWriter(pipeWriter, chunkSizeHint: 2048);
39+
var renderTask = template.RenderAsync(chunkedWriter, null, HtmlEncoder);
3740

38-
var bodyStart = writer.Buffered;
39-
// Body
40-
writer.Write(_fortunesTableStart);
41-
foreach (var item in model)
41+
if (renderTask.IsCompletedSuccessfully)
4242
{
43-
writer.Write(_fortunesRowStart);
44-
writer.WriteNumeric((uint)item.Id);
45-
writer.Write(_fortunesColumn);
46-
writer.WriteUtf8String(HtmlEncoder.Encode(item.Message));
47-
writer.Write(_fortunesRowEnd);
43+
renderTask.GetAwaiter().GetResult();
44+
EndTemplateRendering(chunkedWriter, template);
45+
return ValueTask.CompletedTask;
4846
}
49-
writer.Write(_fortunesTableEnd);
50-
lengthWriter.WriteNumeric((uint)(writer.Buffered - bodyStart));
5147

52-
writer.Commit();
48+
return AwaitTemplateRenderTask(renderTask, chunkedWriter, template);
49+
}
50+
51+
private static async ValueTask AwaitTemplateRenderTask(ValueTask renderTask, ChunkedBufferWriter<WriterAdapter> chunkedWriter, RazorSlice template)
52+
{
53+
await renderTask;
54+
EndTemplateRendering(chunkedWriter, template);
55+
}
56+
57+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
58+
private static void EndTemplateRendering(ChunkedBufferWriter<WriterAdapter> chunkedWriter, RazorSlice template)
59+
{
60+
chunkedWriter.End();
61+
ReturnChunkedWriter(chunkedWriter);
62+
template.Dispose();
5363
}
5464
}
5565
}

frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkApplication.HttpConnection.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,18 @@ private enum State
251251

252252
[MethodImpl(MethodImplOptions.AggressiveInlining)]
253253
private static BufferWriter<WriterAdapter> GetWriter(PipeWriter pipeWriter, int sizeHint)
254-
=> new(new WriterAdapter(pipeWriter), sizeHint);
254+
=> new(new(pipeWriter), sizeHint);
255+
256+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
257+
private static ChunkedBufferWriter<WriterAdapter> GetChunkedWriter(PipeWriter pipeWriter, int chunkSizeHint)
258+
{
259+
var writer = ChunkedWriterPool.Get();
260+
writer.SetOutput(new WriterAdapter(pipeWriter), chunkSizeHint);
261+
return writer;
262+
}
263+
264+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
265+
private static void ReturnChunkedWriter(ChunkedBufferWriter<WriterAdapter> writer) => ChunkedWriterPool.Return(writer);
255266

256267
private struct WriterAdapter : IBufferWriter<byte>
257268
{

frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkApplication.Json.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ private static void Json(ref BufferWriter<WriterAdapter> writer, IBufferWriter<b
2525

2626
writer.Commit();
2727

28-
Utf8JsonWriter utf8JsonWriter = t_writer ??= new Utf8JsonWriter(bodyWriter, new JsonWriterOptions { SkipValidation = true });
28+
var utf8JsonWriter = t_writer ??= new Utf8JsonWriter(bodyWriter, new JsonWriterOptions { SkipValidation = true });
2929
utf8JsonWriter.Reset(bodyWriter);
3030

3131
// Body

frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkApplication.MultipleQueries.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ namespace PlatformBenchmarks
1212
{
1313
public partial class BenchmarkApplication
1414
{
15-
private async Task MultipleQueries(PipeWriter pipeWriter, int count)
15+
private static async Task MultipleQueries(PipeWriter pipeWriter, int count)
1616
{
1717
OutputMultipleQueries(pipeWriter, await Db.LoadMultipleQueriesRows(count), SerializerContext.WorldArray);
1818
}
@@ -31,11 +31,11 @@ private static void OutputMultipleQueries<TWorld>(PipeWriter pipeWriter, TWorld[
3131

3232
writer.Commit();
3333

34-
Utf8JsonWriter utf8JsonWriter = t_writer ??= new Utf8JsonWriter(pipeWriter, new JsonWriterOptions { SkipValidation = true });
34+
var utf8JsonWriter = t_writer ??= new Utf8JsonWriter(pipeWriter, new JsonWriterOptions { SkipValidation = true });
3535
utf8JsonWriter.Reset(pipeWriter);
3636

3737
// Body
38-
JsonSerializer.Serialize<TWorld[]>(utf8JsonWriter, rows, jsonTypeInfo);
38+
JsonSerializer.Serialize(utf8JsonWriter, rows, jsonTypeInfo);
3939

4040
// Content-Length
4141
lengthWriter.WriteNumeric((uint)utf8JsonWriter.BytesCommitted);

frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkApplication.SingleQuery.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ namespace PlatformBenchmarks
1111
{
1212
public partial class BenchmarkApplication
1313
{
14-
private async Task SingleQuery(PipeWriter pipeWriter)
14+
private static async Task SingleQuery(PipeWriter pipeWriter)
1515
{
1616
OutputSingleQuery(pipeWriter, await Db.LoadSingleQueryRow());
1717
}
@@ -30,7 +30,7 @@ private static void OutputSingleQuery(PipeWriter pipeWriter, World row)
3030

3131
writer.Commit();
3232

33-
Utf8JsonWriter utf8JsonWriter = t_writer ??= new Utf8JsonWriter(pipeWriter, new JsonWriterOptions { SkipValidation = true });
33+
var utf8JsonWriter = t_writer ??= new Utf8JsonWriter(pipeWriter, new JsonWriterOptions { SkipValidation = true });
3434
utf8JsonWriter.Reset(pipeWriter);
3535

3636
// Body

frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkApplication.Updates.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ namespace PlatformBenchmarks
1111
{
1212
public partial class BenchmarkApplication
1313
{
14-
private async Task Updates(PipeWriter pipeWriter, int count)
14+
private static async Task Updates(PipeWriter pipeWriter, int count)
1515
{
1616
OutputUpdates(pipeWriter, await Db.LoadMultipleUpdatesRows(count));
1717
}
@@ -30,11 +30,11 @@ private static void OutputUpdates(PipeWriter pipeWriter, World[] rows)
3030

3131
writer.Commit();
3232

33-
Utf8JsonWriter utf8JsonWriter = t_writer ??= new Utf8JsonWriter(pipeWriter, new JsonWriterOptions { SkipValidation = true });
33+
var utf8JsonWriter = t_writer ??= new Utf8JsonWriter(pipeWriter, new JsonWriterOptions { SkipValidation = true });
3434
utf8JsonWriter.Reset(pipeWriter);
3535

3636
// Body
37-
JsonSerializer.Serialize( utf8JsonWriter, rows, SerializerContext.WorldArray);
37+
JsonSerializer.Serialize(utf8JsonWriter, rows, SerializerContext.WorldArray);
3838

3939
// Content-Length
4040
lengthWriter.WriteNumeric((uint)utf8JsonWriter.BytesCommitted);

frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkApplication.cs

Lines changed: 62 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
using System.Threading.Tasks;
1010

1111
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
12+
using Microsoft.Extensions.ObjectPool;
13+
using RazorSlices;
1214

1315
namespace PlatformBenchmarks;
1416

@@ -34,31 +36,49 @@ public sealed partial class BenchmarkApplication
3436
"Content-Length: "u8;
3537

3638
private static ReadOnlySpan<byte> _plainTextBody => "Hello, World!"u8;
39+
private static ReadOnlySpan<byte> _contentLengthGap => " "u8;
3740

38-
private static readonly JsonContext SerializerContext = JsonContext.Default;
41+
#if DATABASE
42+
public static RawDb Db { get; set; }
43+
#endif
3944

40-
[JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Serialization, PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
41-
[JsonSerializable(typeof(JsonMessage))]
42-
[JsonSerializable(typeof(CachedWorld[]))]
43-
[JsonSerializable(typeof(World[]))]
44-
private sealed partial class JsonContext : JsonSerializerContext
45+
private static readonly DefaultObjectPool<ChunkedBufferWriter<WriterAdapter>> ChunkedWriterPool
46+
= new(new ChunkedWriterObjectPolicy());
47+
48+
private sealed class ChunkedWriterObjectPolicy : IPooledObjectPolicy<ChunkedBufferWriter<WriterAdapter>>
4549
{
46-
}
50+
public ChunkedBufferWriter<WriterAdapter> Create() => new();
4751

48-
private static ReadOnlySpan<byte> _fortunesTableStart => "<!DOCTYPE html><html><head><title>Fortunes</title></head><body><table><tr><th>id</th><th>message</th></tr>"u8;
49-
private static ReadOnlySpan<byte> _fortunesRowStart => "<tr><td>"u8;
50-
private static ReadOnlySpan<byte> _fortunesColumn => "</td><td>"u8;
51-
private static ReadOnlySpan<byte> _fortunesRowEnd => "</td></tr>"u8;
52-
private static ReadOnlySpan<byte> _fortunesTableEnd => "</table></body></html>"u8;
53-
private static ReadOnlySpan<byte> _contentLengthGap => " "u8;
52+
public bool Return(ChunkedBufferWriter<WriterAdapter> writer)
53+
{
54+
writer.Reset();
55+
return true;
56+
}
57+
}
5458

5559
#if DATABASE
56-
public static RawDb Db { get; set; }
60+
#if NPGSQL
61+
private readonly static SliceFactory<List<FortuneUtf8>> FortunesTemplateFactory = RazorSlice.ResolveSliceFactory<List<FortuneUtf8>>("/Templates/FortunesUtf8.cshtml");
62+
#elif MYSQLCONNECTOR
63+
private readonly static SliceFactory<List<FortuneUtf16>> FortunesTemplateFactory = RazorSlice.ResolveSliceFactory<List<FortuneUtf16>>("/Templates/FortunesUtf16.cshtml");
64+
#else
65+
#error "DATABASE defined by neither NPGSQL nor MYSQLCONNECTOR are defined"
66+
#endif
5767
#endif
5868

5969
[ThreadStatic]
6070
private static Utf8JsonWriter t_writer;
6171

72+
private static readonly JsonContext SerializerContext = JsonContext.Default;
73+
74+
[JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Serialization, PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
75+
[JsonSerializable(typeof(JsonMessage))]
76+
[JsonSerializable(typeof(CachedWorld[]))]
77+
[JsonSerializable(typeof(World[]))]
78+
private sealed partial class JsonContext : JsonSerializerContext
79+
{
80+
}
81+
6282
public static class Paths
6383
{
6484
public static ReadOnlySpan<byte> Json => "/json"u8;
@@ -78,41 +98,41 @@ public void OnStartLine(HttpVersionAndMethod versionAndMethod, TargetOffsetPathL
7898
_requestType = versionAndMethod.Method == Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpMethod.Get ? GetRequestType(startLine.Slice(targetPath.Offset, targetPath.Length), ref _queries) : RequestType.NotRecognized;
7999
}
80100

81-
private RequestType GetRequestType(ReadOnlySpan<byte> path, ref int queries)
101+
private static RequestType GetRequestType(ReadOnlySpan<byte> path, ref int queries)
82102
{
83103
#if !DATABASE
84104
if (path.Length == 10 && path.SequenceEqual(Paths.Plaintext))
85105
{
86106
return RequestType.PlainText;
87107
}
88-
else if (path.Length == 5 && path.SequenceEqual(Paths.Json))
108+
if (path.Length == 5 && path.SequenceEqual(Paths.Json))
89109
{
90110
return RequestType.Json;
91111
}
92112
#else
93-
if (path.Length == 3 && path[0] == '/' && path[1] == 'd' && path[2] == 'b')
94-
{
95-
return RequestType.SingleQuery;
96-
}
97-
else if (path.Length == 9 && path[1] == 'f' && path.SequenceEqual(Paths.Fortunes))
98-
{
99-
return RequestType.Fortunes;
100-
}
101-
else if (path.Length >= 15 && path[1] == 'c' && path.StartsWith(Paths.Caching))
102-
{
103-
queries = ParseQueries(path.Slice(15));
104-
return RequestType.Caching;
105-
}
106-
else if (path.Length >= 9 && path[1] == 'u' && path.StartsWith(Paths.Updates))
107-
{
108-
queries = ParseQueries(path.Slice(9));
109-
return RequestType.Updates;
110-
}
111-
else if (path.Length >= 9 && path[1] == 'q' && path.StartsWith(Paths.MultipleQueries))
112-
{
113-
queries = ParseQueries(path.Slice(9));
114-
return RequestType.MultipleQueries;
115-
}
113+
if (path.Length == 3 && path[0] == '/' && path[1] == 'd' && path[2] == 'b')
114+
{
115+
return RequestType.SingleQuery;
116+
}
117+
if (path.Length == 9 && path[1] == 'f' && path.SequenceEqual(Paths.Fortunes))
118+
{
119+
return RequestType.Fortunes;
120+
}
121+
if (path.Length >= 15 && path[1] == 'c' && path.StartsWith(Paths.Caching))
122+
{
123+
queries = ParseQueries(path.Slice(15));
124+
return RequestType.Caching;
125+
}
126+
if (path.Length >= 9 && path[1] == 'u' && path.StartsWith(Paths.Updates))
127+
{
128+
queries = ParseQueries(path.Slice(9));
129+
return RequestType.Updates;
130+
}
131+
if (path.Length >= 9 && path[1] == 'q' && path.StartsWith(Paths.MultipleQueries))
132+
{
133+
queries = ParseQueries(path.Slice(9));
134+
return RequestType.MultipleQueries;
135+
}
116136
#endif
117137
return RequestType.NotRecognized;
118138
}
@@ -138,13 +158,13 @@ private void ProcessRequest(ref BufferWriter<WriterAdapter> writer)
138158

139159
private static int ParseQueries(ReadOnlySpan<byte> parameter)
140160
{
141-
if (!Utf8Parser.TryParse(parameter, out int queries, out _) || queries < 1)
161+
if (!Utf8Parser.TryParse(parameter, out int queries, out _))
142162
{
143163
queries = 1;
144164
}
145-
else if (queries > 500)
165+
else
146166
{
147-
queries = 500;
167+
queries = Math.Clamp(queries, 1, 500);
148168
}
149169

150170
return queries;

frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkConfigurationHelpers.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ public static IWebHostBuilder UseBenchmarksConfiguration(this IWebHostBuilder bu
1414

1515
builder.UseSockets(options =>
1616
{
17-
if (int.TryParse(builder.GetSetting("threadCount"), out int threadCount))
17+
if (int.TryParse(builder.GetSetting("threadCount"), out var threadCount))
1818
{
1919
options.IOQueueCount = threadCount;
2020
}

frameworks/CSharp/aspnetcore/PlatformBenchmarks/BufferWriter.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public void Advance(int count)
4444
}
4545

4646
[MethodImpl(MethodImplOptions.AggressiveInlining)]
47-
public void Write(ReadOnlySpan<byte> source)
47+
public void Write(scoped ReadOnlySpan<byte> source)
4848
{
4949
if (_span.Length >= source.Length)
5050
{
@@ -77,7 +77,7 @@ private void EnsureMore(int count = 0)
7777
_span = _output.GetSpan(count);
7878
}
7979

80-
private void WriteMultiBuffer(ReadOnlySpan<byte> source)
80+
private void WriteMultiBuffer(scoped ReadOnlySpan<byte> source)
8181
{
8282
while (source.Length > 0)
8383
{

0 commit comments

Comments
 (0)