From 0e7509d45cbd6fbaca105bf45a728614fb56cab7 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Thu, 9 Mar 2023 16:55:03 +0100 Subject: [PATCH 1/5] Some code cleanup --- .../Data/Providers/RawDbNpgsql.cs | 186 ++++++++---------- 1 file changed, 80 insertions(+), 106 deletions(-) diff --git a/frameworks/CSharp/aspnetcore/PlatformBenchmarks/Data/Providers/RawDbNpgsql.cs b/frameworks/CSharp/aspnetcore/PlatformBenchmarks/Data/Providers/RawDbNpgsql.cs index 72257928094..e37d07b5920 100644 --- a/frameworks/CSharp/aspnetcore/PlatformBenchmarks/Data/Providers/RawDbNpgsql.cs +++ b/frameworks/CSharp/aspnetcore/PlatformBenchmarks/Data/Providers/RawDbNpgsql.cs @@ -3,11 +3,7 @@ #if NPGSQL -using System; -using System.Collections.Generic; -using System.Linq; using System.Runtime.CompilerServices; -using System.Threading.Tasks; using Microsoft.Extensions.Caching.Memory; using Npgsql; @@ -33,35 +29,27 @@ public RawDb(ConcurrentRandom random, AppSettings appSettings) public async Task LoadSingleQueryRow() { - using (var db = new NpgsqlConnection(_connectionString)) - { - await db.OpenAsync(); + using var db = new NpgsqlConnection(_connectionString); + await db.OpenAsync(); - var (cmd, _) = CreateReadCommand(db); - using (cmd) - { - return await ReadSingleRow(cmd); - } - } + var (cmd, _) = CreateReadCommand(db); + using var command = cmd; + return await ReadSingleRow(cmd); } public async Task LoadMultipleQueriesRows(int count) { var result = new World[count]; - using (var db = new NpgsqlConnection(_connectionString)) - { - await db.OpenAsync(); + using var db = new NpgsqlConnection(_connectionString); + await db.OpenAsync(); - var (cmd, idParameter) = CreateReadCommand(db); - using (cmd) - { - for (int i = 0; i < result.Length; i++) - { - result[i] = await ReadSingleRow(cmd); - idParameter.TypedValue = _random.Next(1, 10001); - } - } + var (cmd, idParameter) = CreateReadCommand(db); + using var command = cmd; + for (int i = 0; i < result.Length; i++) + { + result[i] = await ReadSingleRow(cmd); + idParameter.TypedValue = _random.Next(1, 10001); } return result; @@ -91,32 +79,28 @@ public Task LoadCachedQueries(int count) static async Task LoadUncachedQueries(int id, int i, int count, RawDb rawdb, CachedWorld[] result) { - using (var db = new NpgsqlConnection(rawdb._connectionString)) + using var db = new NpgsqlConnection(rawdb._connectionString); + await db.OpenAsync(); + + var (cmd, idParameter) = rawdb.CreateReadCommand(db); + using var command = cmd; + Func> create = async _ => { - await db.OpenAsync(); - - var (cmd, idParameter) = rawdb.CreateReadCommand(db); - using (cmd) - { - Func> create = async _ => - { - return await rawdb.ReadSingleRow(cmd); - }; - - var cacheKeys = _cacheKeys; - var key = cacheKeys[id]; - - idParameter.TypedValue = id; - - for (; i < result.Length; i++) - { - result[i] = await rawdb._cache.GetOrCreateAsync(key, create); - - id = rawdb._random.Next(1, 10001); - idParameter.TypedValue = id; - key = cacheKeys[id]; - } - } + return await rawdb.ReadSingleRow(cmd); + }; + + var cacheKeys = _cacheKeys; + var key = cacheKeys[id]; + + idParameter.TypedValue = id; + + for (; i < result.Length; i++) + { + result[i] = await rawdb._cache.GetOrCreateAsync(key, create); + + id = rawdb._random.Next(1, 10001); + idParameter.TypedValue = id; + key = cacheKeys[id]; } return result; @@ -125,21 +109,17 @@ static async Task LoadUncachedQueries(int id, int i, int count, R public async Task PopulateCache() { - using (var db = new NpgsqlConnection(_connectionString)) - { - await db.OpenAsync(); + using var db = new NpgsqlConnection(_connectionString); + await db.OpenAsync(); - var (cmd, idParameter) = CreateReadCommand(db); - using (cmd) - { - var cacheKeys = _cacheKeys; - var cache = _cache; - for (var i = 1; i < 10001; i++) - { - idParameter.TypedValue = i; - cache.Set(cacheKeys[i], await ReadSingleRow(cmd)); - } - } + var (cmd, idParameter) = CreateReadCommand(db); + using var command = cmd; + var cacheKeys = _cacheKeys; + var cache = _cache; + for (var i = 1; i < 10001; i++) + { + idParameter.TypedValue = i; + cache.Set(cacheKeys[i], await ReadSingleRow(cmd)); } Console.WriteLine("Caching Populated"); @@ -149,37 +129,35 @@ public async Task LoadMultipleUpdatesRows(int count) { var results = new World[count]; - using (var db = new NpgsqlConnection(_connectionString)) - { - await db.OpenAsync(); + using var db = new NpgsqlConnection(_connectionString); + await db.OpenAsync(); - var (queryCmd, queryParameter) = CreateReadCommand(db); - using (queryCmd) + var (queryCmd, queryParameter) = CreateReadCommand(db); + using (queryCmd) + { + for (int i = 0; i < results.Length; i++) { - for (int i = 0; i < results.Length; i++) - { - results[i] = await ReadSingleRow(queryCmd); - queryParameter.TypedValue = _random.Next(1, 10001); - } + 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); + using (var updateCmd = new NpgsqlCommand(BatchUpdateString.Query(count), db)) + { + var ids = BatchUpdateString.Ids; + var randoms = BatchUpdateString.Randoms; - updateCmd.Parameters.Add(new NpgsqlParameter(parameterName: ids[i], value: results[i].Id)); - updateCmd.Parameters.Add(new NpgsqlParameter(parameterName: randoms[i], value: randomNumber)); + for (int i = 0; i < results.Length; i++) + { + var randomNumber = _random.Next(1, 10001); - results[i].RandomNumber = randomNumber; - } + updateCmd.Parameters.Add(new NpgsqlParameter(parameterName: ids[i], value: results[i].Id)); + updateCmd.Parameters.Add(new NpgsqlParameter(parameterName: randoms[i], value: randomNumber)); - await updateCmd.ExecuteNonQueryAsync(); + results[i].RandomNumber = randomNumber; } + + await updateCmd.ExecuteNonQueryAsync(); } return results; @@ -193,17 +171,15 @@ public async Task> LoadFortunesRows() { await db.OpenAsync(); - using (var cmd = new NpgsqlCommand("SELECT id, message FROM fortune", db)) - using (var rdr = await cmd.ExecuteReaderAsync()) + using var cmd = new NpgsqlCommand("SELECT id, message FROM fortune", db); + using var rdr = await cmd.ExecuteReaderAsync(); + while (await rdr.ReadAsync()) { - while (await rdr.ReadAsync()) - { - result.Add(new Fortune - ( - id:rdr.GetInt32(0), - message: rdr.GetString(1) - )); - } + result.Add(new Fortune + ( + id:rdr.GetInt32(0), + message: rdr.GetString(1) + )); } } @@ -226,16 +202,14 @@ public async Task> LoadFortunesRows() [MethodImpl(MethodImplOptions.AggressiveInlining)] private async Task ReadSingleRow(NpgsqlCommand cmd) { - using (var rdr = await cmd.ExecuteReaderAsync(System.Data.CommandBehavior.SingleRow)) - { - await rdr.ReadAsync(); + using var rdr = await cmd.ExecuteReaderAsync(System.Data.CommandBehavior.SingleRow); + await rdr.ReadAsync(); - return new World - { - Id = rdr.GetInt32(0), - RandomNumber = rdr.GetInt32(1) - }; - } + 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(); From 90741107d376f11778fc315048e07c067ad8b349 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Thu, 9 Mar 2023 18:13:44 +0100 Subject: [PATCH 2/5] Updates to aspnetcore PG database access * Use batching with Sync error barriers in Updates * Use NpgsqlDataSource * Use typed NpgsqlParameter everywhere * Use positional parameter placeholders everywhere ($1, $2) --- .../Data/BatchUpdateString.cs | 22 +++-- .../Data/Providers/RawDbNpgsql.cs | 87 +++++++++++++------ .../PlatformBenchmarks.csproj | 2 +- 3 files changed, 73 insertions(+), 38 deletions(-) diff --git a/frameworks/CSharp/aspnetcore/PlatformBenchmarks/Data/BatchUpdateString.cs b/frameworks/CSharp/aspnetcore/PlatformBenchmarks/Data/BatchUpdateString.cs index 39777801b5d..72ff323850d 100644 --- a/frameworks/CSharp/aspnetcore/PlatformBenchmarks/Data/BatchUpdateString.cs +++ b/frameworks/CSharp/aspnetcore/PlatformBenchmarks/Data/BatchUpdateString.cs @@ -15,21 +15,25 @@ internal sealed class BatchUpdateString private static readonly string[] _queries = new string[MaxBatch + 1]; public static string Query(int batchSize) - { - if (_queries[batchSize] != null) - { - return _queries[batchSize]; - } - - var lastIndex = batchSize - 1; + => _queries[batchSize] is null + ? CreateBatch(batchSize) + : _queries[batchSize]; + private static string CreateBatch(int batchSize) + { var sb = StringBuilderCache.Acquire(); if (DatabaseServer == DatabaseServer.PostgreSql) { 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"); + var c = 1; + for (var i = 0; i < batchSize; i++) + { + if (i > 0) + sb.Append(", "); + sb.Append($"(${c++}, ${c++})"); + } + sb.Append(" ORDER BY 1) AS temp(id, randomNumber) WHERE temp.id = world.id"); } else { diff --git a/frameworks/CSharp/aspnetcore/PlatformBenchmarks/Data/Providers/RawDbNpgsql.cs b/frameworks/CSharp/aspnetcore/PlatformBenchmarks/Data/Providers/RawDbNpgsql.cs index e37d07b5920..80c3aa1fe7e 100644 --- a/frameworks/CSharp/aspnetcore/PlatformBenchmarks/Data/Providers/RawDbNpgsql.cs +++ b/frameworks/CSharp/aspnetcore/PlatformBenchmarks/Data/Providers/RawDbNpgsql.cs @@ -14,22 +14,23 @@ namespace PlatformBenchmarks public sealed class RawDb { private readonly ConcurrentRandom _random; - private readonly string _connectionString; private readonly MemoryCache _cache = new( new MemoryCacheOptions { ExpirationScanFrequency = TimeSpan.FromMinutes(60) }); + private readonly NpgsqlDataSource _dataSource; + public RawDb(ConcurrentRandom random, AppSettings appSettings) { _random = random; - _connectionString = appSettings.ConnectionString; + _dataSource = NpgsqlDataSource.Create(appSettings.ConnectionString); } public async Task LoadSingleQueryRow() { - using var db = new NpgsqlConnection(_connectionString); + using var db = _dataSource.CreateConnection(); await db.OpenAsync(); var (cmd, _) = CreateReadCommand(db); @@ -39,20 +40,37 @@ public async Task LoadSingleQueryRow() public async Task LoadMultipleQueriesRows(int count) { - var result = new World[count]; + var results = new World[count]; - using var db = new NpgsqlConnection(_connectionString); - await db.OpenAsync(); + using var connection = await _dataSource.OpenConnectionAsync(); - var (cmd, idParameter) = CreateReadCommand(db); - using var command = cmd; - for (int i = 0; i < result.Length; i++) + using var batch = new NpgsqlBatch(connection) + { + // Inserts a PG Sync message between each statement in the batch, required for compliance with + // TechEmpower general test requirement 7 + // https://github.com/TechEmpower/FrameworkBenchmarks/wiki/Project-Information-Framework-Tests-Overview + EnableErrorBarriers = true + }; + + for (var i = 0; i < count; i++) { - result[i] = await ReadSingleRow(cmd); - idParameter.TypedValue = _random.Next(1, 10001); + batch.BatchCommands.Add(new() + { + CommandText = "SELECT id, randomnumber FROM world WHERE id = $1", + Parameters = { new NpgsqlParameter { TypedValue = _random.Next(1, 10001) } } + }); } - return result; + using var reader = await batch.ExecuteReaderAsync(); + + for (var i = 0; i < count; i++) + { + await reader.ReadAsync(); + results[i] = new World { Id = reader.GetInt32(0), RandomNumber = reader.GetInt32(1) }; + await reader.NextResultAsync(); + } + + return results; } public Task LoadCachedQueries(int count) @@ -79,7 +97,7 @@ public Task LoadCachedQueries(int count) static async Task LoadUncachedQueries(int id, int i, int count, RawDb rawdb, CachedWorld[] result) { - using var db = new NpgsqlConnection(rawdb._connectionString); + using var db = rawdb._dataSource.CreateConnection(); await db.OpenAsync(); var (cmd, idParameter) = rawdb.CreateReadCommand(db); @@ -109,7 +127,7 @@ static async Task LoadUncachedQueries(int id, int i, int count, R public async Task PopulateCache() { - using var db = new NpgsqlConnection(_connectionString); + using var db = _dataSource.CreateConnection(); await db.OpenAsync(); var (cmd, idParameter) = CreateReadCommand(db); @@ -129,30 +147,43 @@ public async Task LoadMultipleUpdatesRows(int count) { var results = new World[count]; - using var db = new NpgsqlConnection(_connectionString); - await db.OpenAsync(); + using var connection = _dataSource.CreateConnection(); + await connection.OpenAsync(); - var (queryCmd, queryParameter) = CreateReadCommand(db); - using (queryCmd) + using (var batch = new NpgsqlBatch(connection)) { - for (int i = 0; i < results.Length; i++) + // Inserts a PG Sync message between each statement in the batch, required for compliance with + // TechEmpower general test requirement 7 + // https://github.com/TechEmpower/FrameworkBenchmarks/wiki/Project-Information-Framework-Tests-Overview + batch.EnableErrorBarriers = true; + + for (var i = 0; i < count; i++) { - results[i] = await ReadSingleRow(queryCmd); - queryParameter.TypedValue = _random.Next(1, 10001); + batch.BatchCommands.Add(new() + { + CommandText = "SELECT id, randomnumber FROM world WHERE id = $1", + Parameters = { new NpgsqlParameter { TypedValue = _random.Next(1, 10001) } } + }); + } + + using var reader = await batch.ExecuteReaderAsync(); + + for (var i = 0; i < count; i++) + { + await reader.ReadAsync(); + results[i] = new World { Id = reader.GetInt32(0), RandomNumber = reader.GetInt32(1) }; + await reader.NextResultAsync(); } } - using (var updateCmd = new NpgsqlCommand(BatchUpdateString.Query(count), db)) + using (var updateCmd = new NpgsqlCommand(BatchUpdateString.Query(count), connection)) { - 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(parameterName: ids[i], value: results[i].Id)); - updateCmd.Parameters.Add(new NpgsqlParameter(parameterName: randoms[i], value: randomNumber)); + updateCmd.Parameters.Add(new NpgsqlParameter { TypedValue = results[i].Id }); + updateCmd.Parameters.Add(new NpgsqlParameter { TypedValue = randomNumber }); results[i].RandomNumber = randomNumber; } @@ -167,7 +198,7 @@ public async Task> LoadFortunesRows() { var result = new List(); - using (var db = new NpgsqlConnection(_connectionString)) + using (var db = _dataSource.CreateConnection()) { await db.OpenAsync(); diff --git a/frameworks/CSharp/aspnetcore/PlatformBenchmarks/PlatformBenchmarks.csproj b/frameworks/CSharp/aspnetcore/PlatformBenchmarks/PlatformBenchmarks.csproj index 74ab1839180..69afd47eff1 100644 --- a/frameworks/CSharp/aspnetcore/PlatformBenchmarks/PlatformBenchmarks.csproj +++ b/frameworks/CSharp/aspnetcore/PlatformBenchmarks/PlatformBenchmarks.csproj @@ -22,7 +22,7 @@ - + From 7fafda4030f10d520a1f263e32f8f647900728e2 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Fri, 10 Mar 2023 18:52:37 +0100 Subject: [PATCH 3/5] Stop UTF8 decoding/reencoding in Fortunes platform --- .../BenchmarkApplication.Fortunes.cs | 34 ++++++++++++++++++- .../PlatformBenchmarks/BufferWriter.cs | 4 +-- .../Data/{Fortune.cs => FortuneUtf16.cs} | 6 ++-- .../PlatformBenchmarks/Data/FortuneUtf8.cs | 22 ++++++++++++ .../Data/Providers/RawDbMySqlConnector.cs | 8 ++--- .../Data/Providers/RawDbNpgsql.cs | 12 ++++--- 6 files changed, 71 insertions(+), 15 deletions(-) rename frameworks/CSharp/aspnetcore/PlatformBenchmarks/Data/{Fortune.cs => FortuneUtf16.cs} (70%) create mode 100644 frameworks/CSharp/aspnetcore/PlatformBenchmarks/Data/FortuneUtf8.cs diff --git a/frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkApplication.Fortunes.cs b/frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkApplication.Fortunes.cs index 5d951303e0b..dc83c5a14e3 100644 --- a/frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkApplication.Fortunes.cs +++ b/frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkApplication.Fortunes.cs @@ -4,6 +4,7 @@ #if DATABASE using System.Collections.Generic; +using System.Diagnostics; using System.IO.Pipelines; using System.Text.Encodings.Web; using System.Threading.Tasks; @@ -23,7 +24,38 @@ private async Task Fortunes(PipeWriter pipeWriter) OutputFortunes(pipeWriter, await Db.LoadFortunesRows()); } - private void OutputFortunes(PipeWriter pipeWriter, List model) + private void OutputFortunes(PipeWriter pipeWriter, List model) + { + var writer = GetWriter(pipeWriter, sizeHint: 1600); // in reality it's 1361 + + writer.Write(_fortunesPreamble); + + var lengthWriter = writer; + writer.Write(_contentLengthGap); + + // Date header + writer.Write(DateHeader.HeaderBytes); + + var bodyStart = writer.Buffered; + // Body + writer.Write(_fortunesTableStart); + foreach (var item in model) + { + writer.Write(_fortunesRowStart); + writer.WriteNumeric((uint)item.Id); + writer.Write(_fortunesColumn); + HtmlEncoder.EncodeUtf8(item.Message.AsSpan(), writer.Span, out var bytesConsumed, out var bytesWritten, isFinalBlock: true); + Debug.Assert(bytesConsumed == item.Message.Length, "Not enough remaining space in the buffer"); + writer.Advance(bytesWritten); + writer.Write(_fortunesRowEnd); + } + writer.Write(_fortunesTableEnd); + lengthWriter.WriteNumeric((uint)(writer.Buffered - bodyStart)); + + writer.Commit(); + } + + private void OutputFortunes(PipeWriter pipeWriter, List model) { var writer = GetWriter(pipeWriter, sizeHint: 1600); // in reality it's 1361 diff --git a/frameworks/CSharp/aspnetcore/PlatformBenchmarks/BufferWriter.cs b/frameworks/CSharp/aspnetcore/PlatformBenchmarks/BufferWriter.cs index 95035307826..e46b3303b77 100644 --- a/frameworks/CSharp/aspnetcore/PlatformBenchmarks/BufferWriter.cs +++ b/frameworks/CSharp/aspnetcore/PlatformBenchmarks/BufferWriter.cs @@ -44,7 +44,7 @@ public void Advance(int count) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Write(ReadOnlySpan source) + public void Write(scoped ReadOnlySpan source) { if (_span.Length >= source.Length) { @@ -77,7 +77,7 @@ private void EnsureMore(int count = 0) _span = _output.GetSpan(count); } - private void WriteMultiBuffer(ReadOnlySpan source) + private void WriteMultiBuffer(scoped ReadOnlySpan source) { while (source.Length > 0) { diff --git a/frameworks/CSharp/aspnetcore/PlatformBenchmarks/Data/Fortune.cs b/frameworks/CSharp/aspnetcore/PlatformBenchmarks/Data/FortuneUtf16.cs similarity index 70% rename from frameworks/CSharp/aspnetcore/PlatformBenchmarks/Data/Fortune.cs rename to frameworks/CSharp/aspnetcore/PlatformBenchmarks/Data/FortuneUtf16.cs index ac39ac82363..74602be355f 100644 --- a/frameworks/CSharp/aspnetcore/PlatformBenchmarks/Data/Fortune.cs +++ b/frameworks/CSharp/aspnetcore/PlatformBenchmarks/Data/FortuneUtf16.cs @@ -3,9 +3,9 @@ namespace PlatformBenchmarks; -public readonly struct Fortune : IComparable, IComparable +public readonly struct FortuneUtf16 : IComparable, IComparable { - public Fortune(int id, string message) + public FortuneUtf16(int id, string message) { Id = id; Message = message; @@ -18,5 +18,5 @@ public Fortune(int id, string message) 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); + public int CompareTo(FortuneUtf16 other) => string.CompareOrdinal(Message, other.Message); } diff --git a/frameworks/CSharp/aspnetcore/PlatformBenchmarks/Data/FortuneUtf8.cs b/frameworks/CSharp/aspnetcore/PlatformBenchmarks/Data/FortuneUtf8.cs new file mode 100644 index 00000000000..a914f30539d --- /dev/null +++ b/frameworks/CSharp/aspnetcore/PlatformBenchmarks/Data/FortuneUtf8.cs @@ -0,0 +1,22 @@ +// 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 PlatformBenchmarks; + +public readonly struct FortuneUtf8 : IComparable, IComparable +{ + public FortuneUtf8(int id, byte[] message) + { + Id = id; + Message = message; + } + + public int Id { get; } + + public byte[] 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(FortuneUtf8 other) => Message.AsSpan().SequenceCompareTo(other.Message.AsSpan()); +} diff --git a/frameworks/CSharp/aspnetcore/PlatformBenchmarks/Data/Providers/RawDbMySqlConnector.cs b/frameworks/CSharp/aspnetcore/PlatformBenchmarks/Data/Providers/RawDbMySqlConnector.cs index 45cbc27e64c..5b23c3b4c1f 100644 --- a/frameworks/CSharp/aspnetcore/PlatformBenchmarks/Data/Providers/RawDbMySqlConnector.cs +++ b/frameworks/CSharp/aspnetcore/PlatformBenchmarks/Data/Providers/RawDbMySqlConnector.cs @@ -185,9 +185,9 @@ public async Task LoadMultipleUpdatesRows(int count) return results; } - public async Task> LoadFortunesRows() + public async Task> LoadFortunesRows() { - var result = new List(); + var result = new List(); using (var db = new MySqlConnection(_connectionString)) { @@ -202,7 +202,7 @@ public async Task> LoadFortunesRows() while (await rdr.ReadAsync()) { result.Add( - new Fortune + new FortuneUtf16 ( id: rdr.GetInt32(0), message: rdr.GetString(1) @@ -212,7 +212,7 @@ public async Task> LoadFortunesRows() } } - result.Add(new Fortune(id: 0, message: "Additional fortune added at request time." )); + result.Add(new FortuneUtf16(id: 0, message: "Additional fortune added at request time." )); result.Sort(); return result; diff --git a/frameworks/CSharp/aspnetcore/PlatformBenchmarks/Data/Providers/RawDbNpgsql.cs b/frameworks/CSharp/aspnetcore/PlatformBenchmarks/Data/Providers/RawDbNpgsql.cs index 80c3aa1fe7e..931040c65c4 100644 --- a/frameworks/CSharp/aspnetcore/PlatformBenchmarks/Data/Providers/RawDbNpgsql.cs +++ b/frameworks/CSharp/aspnetcore/PlatformBenchmarks/Data/Providers/RawDbNpgsql.cs @@ -194,9 +194,9 @@ public async Task LoadMultipleUpdatesRows(int count) return results; } - public async Task> LoadFortunesRows() + public async Task> LoadFortunesRows() { - var result = new List(); + var result = new List(); using (var db = _dataSource.CreateConnection()) { @@ -206,20 +206,22 @@ public async Task> LoadFortunesRows() using var rdr = await cmd.ExecuteReaderAsync(); while (await rdr.ReadAsync()) { - result.Add(new Fortune + result.Add(new FortuneUtf8 ( id:rdr.GetInt32(0), - message: rdr.GetString(1) + message: rdr.GetFieldValue(1) )); } } - result.Add(new Fortune(id: 0, message: "Additional fortune added at request time." )); + result.Add(new FortuneUtf8(id: 0, AdditionalFortune)); result.Sort(); return result; } + private readonly byte[] AdditionalFortune = "Additional fortune added at request time."u8.ToArray(); + private (NpgsqlCommand readCmd, NpgsqlParameter idParameter) CreateReadCommand(NpgsqlConnection connection) { var cmd = new NpgsqlCommand("SELECT id, randomnumber FROM world WHERE id = $1", connection); From 812a13786d60c58b4f9565a6457a7dc278bf6989 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Thu, 16 Mar 2023 13:07:49 +0100 Subject: [PATCH 4/5] Update Fortunes to use Razor templating --- .../BenchmarkApplication.Caching.cs | 2 +- .../BenchmarkApplication.Fortunes.cs | 100 +++----- .../BenchmarkApplication.HttpConnection.cs | 13 +- .../BenchmarkApplication.Json.cs | 2 +- .../BenchmarkApplication.MultipleQueries.cs | 6 +- .../BenchmarkApplication.SingleQuery.cs | 4 +- .../BenchmarkApplication.Updates.cs | 6 +- .../BenchmarkApplication.cs | 104 +++++--- .../BenchmarkConfigurationHelpers.cs | 2 +- .../PlatformBenchmarks/ChunkedBufferWriter.cs | 241 ++++++++++++++++++ .../Data/Providers/RawDbNpgsql.cs | 9 +- .../PlatformBenchmarks/DateHeader.cs | 5 +- .../PlatformBenchmarks.csproj | 1 + .../aspnetcore/PlatformBenchmarks/Program.cs | 40 +-- .../Templates/FortunesUtf16.cshtml | 2 + .../Templates/FortunesUtf8.cshtml | 2 + .../Templates/_ViewImports.cshtml | 9 + .../PlatformBenchmarks/appsettings.json | 3 +- 18 files changed, 410 insertions(+), 141 deletions(-) create mode 100644 frameworks/CSharp/aspnetcore/PlatformBenchmarks/ChunkedBufferWriter.cs create mode 100644 frameworks/CSharp/aspnetcore/PlatformBenchmarks/Templates/FortunesUtf16.cshtml create mode 100644 frameworks/CSharp/aspnetcore/PlatformBenchmarks/Templates/FortunesUtf8.cshtml create mode 100644 frameworks/CSharp/aspnetcore/PlatformBenchmarks/Templates/_ViewImports.cshtml diff --git a/frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkApplication.Caching.cs b/frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkApplication.Caching.cs index 96bec49e72b..2b8f1eb9243 100644 --- a/frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkApplication.Caching.cs +++ b/frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkApplication.Caching.cs @@ -10,7 +10,7 @@ namespace PlatformBenchmarks; public partial class BenchmarkApplication { - private async Task Caching(PipeWriter pipeWriter, int count) + private static async Task Caching(PipeWriter pipeWriter, int count) { OutputMultipleQueries(pipeWriter, await Db.LoadCachedQueries(count), SerializerContext.CachedWorldArray); } diff --git a/frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkApplication.Fortunes.cs b/frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkApplication.Fortunes.cs index dc83c5a14e3..793d20af48b 100644 --- a/frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkApplication.Fortunes.cs +++ b/frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkApplication.Fortunes.cs @@ -3,85 +3,63 @@ #if DATABASE -using System.Collections.Generic; -using System.Diagnostics; using System.IO.Pipelines; -using System.Text.Encodings.Web; -using System.Threading.Tasks; +using System.Runtime.CompilerServices; +using RazorSlices; namespace PlatformBenchmarks { public partial class BenchmarkApplication { - private static ReadOnlySpan _fortunesPreamble => - "HTTP/1.1 200 OK\r\n"u8 + - "Server: K\r\n"u8 + - "Content-Type: text/html; charset=UTF-8\r\n"u8 + - "Content-Length: "u8; - private async Task Fortunes(PipeWriter pipeWriter) { - OutputFortunes(pipeWriter, await Db.LoadFortunesRows()); + await OutputFortunes(pipeWriter, await Db.LoadFortunesRows(), FortunesTemplateFactory); } - private void OutputFortunes(PipeWriter pipeWriter, List model) + private ValueTask OutputFortunes(PipeWriter pipeWriter, TModel model, SliceFactory templateFactory) { - var writer = GetWriter(pipeWriter, sizeHint: 1600); // in reality it's 1361 - - writer.Write(_fortunesPreamble); - - var lengthWriter = writer; - writer.Write(_contentLengthGap); - - // Date header - writer.Write(DateHeader.HeaderBytes); - - var bodyStart = writer.Buffered; - // Body - writer.Write(_fortunesTableStart); - foreach (var item in model) + // Render headers + var preamble = """ + HTTP/1.1 200 OK + Server: K + Content-Type: text/html; charset=utf-8 + Transfer-Encoding: chunked + """u8; + var headersLength = preamble.Length + DateHeader.HeaderBytes.Length; + var headersSpan = pipeWriter.GetSpan(headersLength); + preamble.CopyTo(headersSpan); + DateHeader.HeaderBytes.CopyTo(headersSpan[preamble.Length..]); + pipeWriter.Advance(headersLength); + + // Render body + var template = templateFactory(model); + // Kestrel PipeWriter span size is 4K, headers above already written to first span & template output is ~1350 bytes, + // so 2K chunk size should result in only a single span and chunk being used. + var chunkedWriter = GetChunkedWriter(pipeWriter, chunkSizeHint: 2048); + var renderTask = template.RenderAsync(chunkedWriter, null, HtmlEncoder); + + if (renderTask.IsCompletedSuccessfully) { - writer.Write(_fortunesRowStart); - writer.WriteNumeric((uint)item.Id); - writer.Write(_fortunesColumn); - HtmlEncoder.EncodeUtf8(item.Message.AsSpan(), writer.Span, out var bytesConsumed, out var bytesWritten, isFinalBlock: true); - Debug.Assert(bytesConsumed == item.Message.Length, "Not enough remaining space in the buffer"); - writer.Advance(bytesWritten); - writer.Write(_fortunesRowEnd); + renderTask.GetAwaiter().GetResult(); + EndTemplateRendering(chunkedWriter, template); + return ValueTask.CompletedTask; } - writer.Write(_fortunesTableEnd); - lengthWriter.WriteNumeric((uint)(writer.Buffered - bodyStart)); - writer.Commit(); + return AwaitTemplateRenderTask(renderTask, chunkedWriter, template); } - private void OutputFortunes(PipeWriter pipeWriter, List model) + private static async ValueTask AwaitTemplateRenderTask(ValueTask renderTask, ChunkedBufferWriter chunkedWriter, RazorSlice template) { - var writer = GetWriter(pipeWriter, sizeHint: 1600); // in reality it's 1361 - - writer.Write(_fortunesPreamble); - - var lengthWriter = writer; - writer.Write(_contentLengthGap); - - // Date header - writer.Write(DateHeader.HeaderBytes); - - var bodyStart = writer.Buffered; - // Body - writer.Write(_fortunesTableStart); - foreach (var item in model) - { - writer.Write(_fortunesRowStart); - writer.WriteNumeric((uint)item.Id); - writer.Write(_fortunesColumn); - writer.WriteUtf8String(HtmlEncoder.Encode(item.Message)); - writer.Write(_fortunesRowEnd); - } - writer.Write(_fortunesTableEnd); - lengthWriter.WriteNumeric((uint)(writer.Buffered - bodyStart)); + await renderTask; + EndTemplateRendering(chunkedWriter, template); + } - writer.Commit(); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void EndTemplateRendering(ChunkedBufferWriter chunkedWriter, RazorSlice template) + { + chunkedWriter.End(); + ReturnChunkedWriter(chunkedWriter); + template.Dispose(); } } } diff --git a/frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkApplication.HttpConnection.cs b/frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkApplication.HttpConnection.cs index 1d3aa29c2e3..1c31523cdfb 100644 --- a/frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkApplication.HttpConnection.cs +++ b/frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkApplication.HttpConnection.cs @@ -251,7 +251,18 @@ private enum State [MethodImpl(MethodImplOptions.AggressiveInlining)] private static BufferWriter GetWriter(PipeWriter pipeWriter, int sizeHint) - => new(new WriterAdapter(pipeWriter), sizeHint); + => new(new(pipeWriter), sizeHint); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ChunkedBufferWriter GetChunkedWriter(PipeWriter pipeWriter, int chunkSizeHint) + { + var writer = ChunkedWriterPool.Get(); + writer.SetOutput(new WriterAdapter(pipeWriter), chunkSizeHint); + return writer; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void ReturnChunkedWriter(ChunkedBufferWriter writer) => ChunkedWriterPool.Return(writer); private struct WriterAdapter : IBufferWriter { diff --git a/frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkApplication.Json.cs b/frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkApplication.Json.cs index ad213dfabe7..186eefc8105 100644 --- a/frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkApplication.Json.cs +++ b/frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkApplication.Json.cs @@ -25,7 +25,7 @@ private static void Json(ref BufferWriter writer, IBufferWriter(PipeWriter pipeWriter, TWorld[ writer.Commit(); - Utf8JsonWriter utf8JsonWriter = t_writer ??= new Utf8JsonWriter(pipeWriter, new JsonWriterOptions { SkipValidation = true }); + var utf8JsonWriter = t_writer ??= new Utf8JsonWriter(pipeWriter, new JsonWriterOptions { SkipValidation = true }); utf8JsonWriter.Reset(pipeWriter); // Body - JsonSerializer.Serialize(utf8JsonWriter, rows, jsonTypeInfo); + JsonSerializer.Serialize(utf8JsonWriter, rows, jsonTypeInfo); // Content-Length lengthWriter.WriteNumeric((uint)utf8JsonWriter.BytesCommitted); diff --git a/frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkApplication.SingleQuery.cs b/frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkApplication.SingleQuery.cs index 102cafd907c..31c12060179 100644 --- a/frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkApplication.SingleQuery.cs +++ b/frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkApplication.SingleQuery.cs @@ -11,7 +11,7 @@ namespace PlatformBenchmarks { public partial class BenchmarkApplication { - private async Task SingleQuery(PipeWriter pipeWriter) + private static async Task SingleQuery(PipeWriter pipeWriter) { OutputSingleQuery(pipeWriter, await Db.LoadSingleQueryRow()); } @@ -30,7 +30,7 @@ private static void OutputSingleQuery(PipeWriter pipeWriter, World row) writer.Commit(); - Utf8JsonWriter utf8JsonWriter = t_writer ??= new Utf8JsonWriter(pipeWriter, new JsonWriterOptions { SkipValidation = true }); + var utf8JsonWriter = t_writer ??= new Utf8JsonWriter(pipeWriter, new JsonWriterOptions { SkipValidation = true }); utf8JsonWriter.Reset(pipeWriter); // Body diff --git a/frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkApplication.Updates.cs b/frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkApplication.Updates.cs index 2cdc4083c4f..91a164ae6a2 100644 --- a/frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkApplication.Updates.cs +++ b/frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkApplication.Updates.cs @@ -11,7 +11,7 @@ namespace PlatformBenchmarks { public partial class BenchmarkApplication { - private async Task Updates(PipeWriter pipeWriter, int count) + private static async Task Updates(PipeWriter pipeWriter, int count) { OutputUpdates(pipeWriter, await Db.LoadMultipleUpdatesRows(count)); } @@ -30,11 +30,11 @@ private static void OutputUpdates(PipeWriter pipeWriter, World[] rows) writer.Commit(); - Utf8JsonWriter utf8JsonWriter = t_writer ??= new Utf8JsonWriter(pipeWriter, new JsonWriterOptions { SkipValidation = true }); + var utf8JsonWriter = t_writer ??= new Utf8JsonWriter(pipeWriter, new JsonWriterOptions { SkipValidation = true }); utf8JsonWriter.Reset(pipeWriter); // Body - JsonSerializer.Serialize( utf8JsonWriter, rows, SerializerContext.WorldArray); + JsonSerializer.Serialize(utf8JsonWriter, rows, SerializerContext.WorldArray); // Content-Length lengthWriter.WriteNumeric((uint)utf8JsonWriter.BytesCommitted); diff --git a/frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkApplication.cs b/frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkApplication.cs index 627faed5dca..022c128fafd 100644 --- a/frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkApplication.cs +++ b/frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkApplication.cs @@ -9,6 +9,8 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; +using Microsoft.Extensions.ObjectPool; +using RazorSlices; namespace PlatformBenchmarks; @@ -34,31 +36,49 @@ public sealed partial class BenchmarkApplication "Content-Length: "u8; private static ReadOnlySpan _plainTextBody => "Hello, World!"u8; + private static ReadOnlySpan _contentLengthGap => " "u8; - private static readonly JsonContext SerializerContext = JsonContext.Default; +#if DATABASE + public static RawDb Db { get; set; } +#endif - [JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Serialization, PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] - [JsonSerializable(typeof(JsonMessage))] - [JsonSerializable(typeof(CachedWorld[]))] - [JsonSerializable(typeof(World[]))] - private sealed partial class JsonContext : JsonSerializerContext + private static readonly DefaultObjectPool> ChunkedWriterPool + = new(new ChunkedWriterObjectPolicy()); + + private sealed class ChunkedWriterObjectPolicy : IPooledObjectPolicy> { - } + public ChunkedBufferWriter Create() => new(); - private static ReadOnlySpan _fortunesTableStart => "Fortunes"u8; - private static ReadOnlySpan _fortunesRowStart => ""u8; - private static ReadOnlySpan _fortunesTableEnd => "
idmessage
"u8; - private static ReadOnlySpan _fortunesColumn => ""u8; - private static ReadOnlySpan _fortunesRowEnd => "
"u8; - private static ReadOnlySpan _contentLengthGap => " "u8; + public bool Return(ChunkedBufferWriter writer) + { + writer.Reset(); + return true; + } + } #if DATABASE - public static RawDb Db { get; set; } +#if NPGSQL + private readonly static SliceFactory> FortunesTemplateFactory = RazorSlice.ResolveSliceFactory>("/Templates/FortunesUtf8.cshtml"); +#elif MYSQLCONNECTOR + private readonly static SliceFactory> FortunesTemplateFactory = RazorSlice.ResolveSliceFactory>("/Templates/FortunesUtf16.cshtml"); +#else +#error "DATABASE defined by neither NPGSQL nor MYSQLCONNECTOR are defined" +#endif #endif [ThreadStatic] private static Utf8JsonWriter t_writer; + private static readonly JsonContext SerializerContext = JsonContext.Default; + + [JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Serialization, PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] + [JsonSerializable(typeof(JsonMessage))] + [JsonSerializable(typeof(CachedWorld[]))] + [JsonSerializable(typeof(World[]))] + private sealed partial class JsonContext : JsonSerializerContext + { + } + public static class Paths { public static ReadOnlySpan Json => "/json"u8; @@ -78,41 +98,41 @@ public void OnStartLine(HttpVersionAndMethod versionAndMethod, TargetOffsetPathL _requestType = versionAndMethod.Method == Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpMethod.Get ? GetRequestType(startLine.Slice(targetPath.Offset, targetPath.Length), ref _queries) : RequestType.NotRecognized; } - private RequestType GetRequestType(ReadOnlySpan path, ref int queries) + private static RequestType GetRequestType(ReadOnlySpan path, ref int queries) { #if !DATABASE if (path.Length == 10 && path.SequenceEqual(Paths.Plaintext)) { return RequestType.PlainText; } - else if (path.Length == 5 && path.SequenceEqual(Paths.Json)) + if (path.Length == 5 && path.SequenceEqual(Paths.Json)) { return RequestType.Json; } #else - if (path.Length == 3 && path[0] == '/' && path[1] == 'd' && path[2] == 'b') - { - return RequestType.SingleQuery; - } - else if (path.Length == 9 && path[1] == 'f' && path.SequenceEqual(Paths.Fortunes)) - { - return RequestType.Fortunes; - } - else if (path.Length >= 15 && path[1] == 'c' && path.StartsWith(Paths.Caching)) - { - queries = ParseQueries(path.Slice(15)); - return RequestType.Caching; - } - else if (path.Length >= 9 && path[1] == 'u' && path.StartsWith(Paths.Updates)) - { - queries = ParseQueries(path.Slice(9)); - return RequestType.Updates; - } - else if (path.Length >= 9 && path[1] == 'q' && path.StartsWith(Paths.MultipleQueries)) - { - queries = ParseQueries(path.Slice(9)); - return RequestType.MultipleQueries; - } + if (path.Length == 3 && path[0] == '/' && path[1] == 'd' && path[2] == 'b') + { + return RequestType.SingleQuery; + } + if (path.Length == 9 && path[1] == 'f' && path.SequenceEqual(Paths.Fortunes)) + { + return RequestType.Fortunes; + } + if (path.Length >= 15 && path[1] == 'c' && path.StartsWith(Paths.Caching)) + { + queries = ParseQueries(path.Slice(15)); + return RequestType.Caching; + } + if (path.Length >= 9 && path[1] == 'u' && path.StartsWith(Paths.Updates)) + { + queries = ParseQueries(path.Slice(9)); + return RequestType.Updates; + } + if (path.Length >= 9 && path[1] == 'q' && path.StartsWith(Paths.MultipleQueries)) + { + queries = ParseQueries(path.Slice(9)); + return RequestType.MultipleQueries; + } #endif return RequestType.NotRecognized; } @@ -138,13 +158,13 @@ private void ProcessRequest(ref BufferWriter writer) private static int ParseQueries(ReadOnlySpan parameter) { - if (!Utf8Parser.TryParse(parameter, out int queries, out _) || queries < 1) + if (!Utf8Parser.TryParse(parameter, out int queries, out _)) { queries = 1; } - else if (queries > 500) + else { - queries = 500; + queries = Math.Clamp(queries, 1, 500); } return queries; diff --git a/frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkConfigurationHelpers.cs b/frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkConfigurationHelpers.cs index 2341f0b0e12..87317b4fd97 100644 --- a/frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkConfigurationHelpers.cs +++ b/frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkConfigurationHelpers.cs @@ -14,7 +14,7 @@ public static IWebHostBuilder UseBenchmarksConfiguration(this IWebHostBuilder bu builder.UseSockets(options => { - if (int.TryParse(builder.GetSetting("threadCount"), out int threadCount)) + if (int.TryParse(builder.GetSetting("threadCount"), out var threadCount)) { options.IOQueueCount = threadCount; } diff --git a/frameworks/CSharp/aspnetcore/PlatformBenchmarks/ChunkedBufferWriter.cs b/frameworks/CSharp/aspnetcore/PlatformBenchmarks/ChunkedBufferWriter.cs new file mode 100644 index 00000000000..b63f0775332 --- /dev/null +++ b/frameworks/CSharp/aspnetcore/PlatformBenchmarks/ChunkedBufferWriter.cs @@ -0,0 +1,241 @@ +// 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.Buffers; +using System.Buffers.Text; +using System.Diagnostics; +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace PlatformBenchmarks; + +internal sealed class ChunkedBufferWriter : IBufferWriter where TWriter : IBufferWriter +{ + private const int DefaultChunkSizeHint = 2048; + private static readonly StandardFormat DefaultHexFormat = GetHexFormat(DefaultChunkSizeHint); + private static ReadOnlySpan ChunkTerminator => "\r\n"u8; + + private TWriter _output; + private int _chunkSizeHint; + private StandardFormat _hexFormat = DefaultHexFormat; + private Memory _currentFullChunk; + private Memory _currentChunk; + private int _buffered; + private bool _ended = false; + + public Memory Memory => _currentChunk; + + public TWriter Output => _output; + + public int Buffered => _buffered; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetOutput(TWriter output, int chunkSizeHint = DefaultChunkSizeHint) + { + _buffered = 0; + _chunkSizeHint = chunkSizeHint; + _output = output; + + StartNewChunk(chunkSizeHint, isFirst: true); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Reset() + { + _buffered = 0; + _output = default; + _ended = false; + _hexFormat = DefaultHexFormat; + _currentFullChunk = default; + _currentChunk = default; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Advance(int count) + { + ThrowIfEnded(); + + _buffered += count; + _currentChunk = _currentChunk[count..]; + } + + public Memory GetMemory(int sizeHint = 0) + { + ThrowIfEnded(); + + if (_currentChunk.Length <= sizeHint) + { + EnsureMore(sizeHint); + } + return _currentChunk; + } + + public Span GetSpan(int sizeHint = 0) => GetMemory(sizeHint).Span; + + public void End() + { + ThrowIfEnded(); + + CommitCurrentChunk(isFinal: true); + + _ended = true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static StandardFormat GetHexFormat(int maxValue) + { + var hexDigitCount = CountHexDigits(maxValue); + + return new StandardFormat('X', (byte)hexDigitCount); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int CountHexDigits(int n) => n <= 16 ? 1 : (BitOperations.Log2((uint)n) >> 2) + 1; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void StartNewChunk(int sizeHint, bool isFirst = false) + { + ThrowIfEnded(); + + // Header is like: + // 520\r\n + + var oldFullChunkHexLength = -1; + if (!isFirst) + { + oldFullChunkHexLength = CountHexDigits(_currentFullChunk.Length); + } + _currentFullChunk = _output.GetMemory(Math.Max(_chunkSizeHint, sizeHint)); + var newFullChunkHexLength = CountHexDigits(_currentFullChunk.Length); + + var currentFullChunkSpan = _currentFullChunk.Span; + + // Write space for HEX digits + currentFullChunkSpan[..newFullChunkHexLength].Fill(48); // 48 == '0' + + // Write header terminator + var terminator = "\r\n"u8; + terminator.CopyTo(currentFullChunkSpan[newFullChunkHexLength..]); + var chunkHeaderLength = newFullChunkHexLength + terminator.Length; + _currentChunk = _currentFullChunk[chunkHeaderLength..]; + + if ((!isFirst && oldFullChunkHexLength != newFullChunkHexLength) || (isFirst && DefaultChunkSizeHint != _chunkSizeHint)) + { + // Update HEX format if changed + _hexFormat = GetHexFormat(_currentFullChunk.Length); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void CommitCurrentChunk(bool isFinal = false, int sizeHint = 0) + { + ThrowIfEnded(); + + var contentLength = _buffered; + + if (contentLength > 0) + { + // Update the chunk header + var chunkLengthHexDigitsLength = CountHexDigits(contentLength); + var span = _currentFullChunk.Span; + if (!Utf8Formatter.TryFormat(contentLength, span, out var bytesWritten, _hexFormat)) + { + throw new NotSupportedException("Chunk size too large"); + } + Debug.Assert(chunkLengthHexDigitsLength == bytesWritten, "HEX formatting math problem."); + var headerLength = chunkLengthHexDigitsLength + 2; + + // Total chunk length: content length as HEX string + \r\n + content + \r\n + var spanOffset = headerLength + contentLength; + var chunkTotalLength = spanOffset + ChunkTerminator.Length; + + Debug.Assert(span.Length >= chunkTotalLength, "Bad chunk size calculation."); + + // Write out the chunk terminator + ChunkTerminator.CopyTo(span[spanOffset..]); + spanOffset = chunkTotalLength; + + if (!isFinal) + { + _output.Advance(chunkTotalLength); + StartNewChunk(sizeHint); + } + else + { + // Write out final chunk (zero-length chunk) + var terminator = "0\r\n\r\n"u8; + if ((spanOffset + terminator.Length) <= span.Length) + { + // There's space for the final chunk in the current span + terminator.CopyTo(span[spanOffset..]); + _output.Advance(chunkTotalLength + terminator.Length); + } + else + { + // Final chunk doesn't fit in current span so just write it directly after advancing the writer + _output.Advance(chunkTotalLength); + _output.Write(terminator); + } + } + + _buffered = 0; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Write(ReadOnlySpan source) + { + ThrowIfEnded(); + + if (_currentChunk.Length >= (source.Length + ChunkTerminator.Length)) + { + source.CopyTo(_currentChunk.Span); + Advance(source.Length); + } + else + { + WriteMultiBuffer(source); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void EnsureMore(int count = 0) + { + if (count > (_currentChunk.Length - _buffered - ChunkTerminator.Length)) + { + if (_buffered > 0) + { + CommitCurrentChunk(isFinal: false, count); + } + else + { + StartNewChunk(count); + } + } + } + + private void WriteMultiBuffer(ReadOnlySpan source) + { + while (source.Length > 0) + { + if ((_currentChunk.Length - ChunkTerminator.Length) == 0) + { + EnsureMore(); + } + + var writable = Math.Min(source.Length, _currentChunk.Length - ChunkTerminator.Length); + source[..writable].CopyTo(_currentChunk.Span); + source = source[writable..]; + Advance(writable); + } + } + + private void ThrowIfEnded() + { + if (_ended) + { + throw new InvalidOperationException("Cannot use the writer after calling End()."); + } + } +} \ No newline at end of file diff --git a/frameworks/CSharp/aspnetcore/PlatformBenchmarks/Data/Providers/RawDbNpgsql.cs b/frameworks/CSharp/aspnetcore/PlatformBenchmarks/Data/Providers/RawDbNpgsql.cs index 931040c65c4..3bcb2a9a1e9 100644 --- a/frameworks/CSharp/aspnetcore/PlatformBenchmarks/Data/Providers/RawDbNpgsql.cs +++ b/frameworks/CSharp/aspnetcore/PlatformBenchmarks/Data/Providers/RawDbNpgsql.cs @@ -83,7 +83,7 @@ public Task LoadCachedQueries(int count) { var id = random.Next(1, 10001); var key = cacheKeys[id]; - if (cache.TryGetValue(key, out object cached)) + if (cache.TryGetValue(key, out var cached)) { result[i] = (CachedWorld)cached; } @@ -102,10 +102,7 @@ static async Task LoadUncachedQueries(int id, int i, int count, R var (cmd, idParameter) = rawdb.CreateReadCommand(db); using var command = cmd; - Func> create = async _ => - { - return await rawdb.ReadSingleRow(cmd); - }; + async Task create(ICacheEntry _) => await ReadSingleRow(cmd); var cacheKeys = _cacheKeys; var key = cacheKeys[id]; @@ -233,7 +230,7 @@ public async Task> LoadFortunesRows() } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private async Task ReadSingleRow(NpgsqlCommand cmd) + private static async Task ReadSingleRow(NpgsqlCommand cmd) { using var rdr = await cmd.ExecuteReaderAsync(System.Data.CommandBehavior.SingleRow); await rdr.ReadAsync(); diff --git a/frameworks/CSharp/aspnetcore/PlatformBenchmarks/DateHeader.cs b/frameworks/CSharp/aspnetcore/PlatformBenchmarks/DateHeader.cs index 89e951f2d9f..99cfdc8f0a6 100644 --- a/frameworks/CSharp/aspnetcore/PlatformBenchmarks/DateHeader.cs +++ b/frameworks/CSharp/aspnetcore/PlatformBenchmarks/DateHeader.cs @@ -17,10 +17,7 @@ internal static class DateHeader const int suffixLength = 2; // crlf const int suffixIndex = dateTimeRLength + prefixLength; - private static readonly Timer s_timer = new((s) => - { - SetDateValues(DateTimeOffset.UtcNow); - }, null, 1000, 1000); + private static readonly Timer s_timer = new(_ => SetDateValues(DateTimeOffset.UtcNow), null, 1000, 1000); private static byte[] s_headerBytesMaster = new byte[prefixLength + dateTimeRLength + 2 * suffixLength]; private static byte[] s_headerBytesScratch = new byte[prefixLength + dateTimeRLength + 2 * suffixLength]; diff --git a/frameworks/CSharp/aspnetcore/PlatformBenchmarks/PlatformBenchmarks.csproj b/frameworks/CSharp/aspnetcore/PlatformBenchmarks/PlatformBenchmarks.csproj index 69afd47eff1..0a68f121bde 100644 --- a/frameworks/CSharp/aspnetcore/PlatformBenchmarks/PlatformBenchmarks.csproj +++ b/frameworks/CSharp/aspnetcore/PlatformBenchmarks/PlatformBenchmarks.csproj @@ -22,6 +22,7 @@ + diff --git a/frameworks/CSharp/aspnetcore/PlatformBenchmarks/Program.cs b/frameworks/CSharp/aspnetcore/PlatformBenchmarks/Program.cs index 5db336614c4..365c772bf3f 100644 --- a/frameworks/CSharp/aspnetcore/PlatformBenchmarks/Program.cs +++ b/frameworks/CSharp/aspnetcore/PlatformBenchmarks/Program.cs @@ -19,10 +19,10 @@ public static async Task Main(string[] args) Console.WriteLine(Encoding.UTF8.GetString(BenchmarkApplication.Paths.Plaintext)); Console.WriteLine(Encoding.UTF8.GetString(BenchmarkApplication.Paths.Json)); #else - Console.WriteLine(Encoding.UTF8.GetString(BenchmarkApplication.Paths.Fortunes)); - Console.WriteLine(Encoding.UTF8.GetString(BenchmarkApplication.Paths.SingleQuery)); - Console.WriteLine(Encoding.UTF8.GetString(BenchmarkApplication.Paths.Updates)); - Console.WriteLine(Encoding.UTF8.GetString(BenchmarkApplication.Paths.MultipleQueries)); + Console.WriteLine(Encoding.UTF8.GetString(BenchmarkApplication.Paths.Fortunes)); + Console.WriteLine(Encoding.UTF8.GetString(BenchmarkApplication.Paths.SingleQuery)); + Console.WriteLine(Encoding.UTF8.GetString(BenchmarkApplication.Paths.Updates)); + Console.WriteLine(Encoding.UTF8.GetString(BenchmarkApplication.Paths.MultipleQueries)); #endif DateHeader.SyncDateTimer(); @@ -30,7 +30,14 @@ public static async Task Main(string[] args) var config = (IConfiguration)host.Services.GetService(typeof(IConfiguration)); BatchUpdateString.DatabaseServer = config.Get().Database; #if DATABASE + try + { await BenchmarkApplication.Db.PopulateCache(); + } + catch (Exception ex) + { + Console.WriteLine($"Error trying to populate database cache: {ex}"); + } #endif await host.RunAsync(); } @@ -39,6 +46,9 @@ public static IWebHost BuildWebHost(string[] args) { var config = new ConfigurationBuilder() .AddJsonFile("appsettings.json") +#if DEBUG + .AddUserSecrets() +#endif .AddEnvironmentVariables() .AddEnvironmentVariables(prefix: "ASPNETCORE_") .AddCommandLine(args) @@ -46,18 +56,18 @@ public static IWebHost BuildWebHost(string[] args) var appSettings = config.Get(); #if DATABASE - Console.WriteLine($"Database: {appSettings.Database}"); - Console.WriteLine($"ConnectionString: {appSettings.ConnectionString}"); + Console.WriteLine($"Database: {appSettings.Database}"); + Console.WriteLine($"ConnectionString: {appSettings.ConnectionString}"); - if (appSettings.Database is DatabaseServer.PostgreSql - or DatabaseServer.MySql) - { - BenchmarkApplication.Db = new RawDb(new ConcurrentRandom(), appSettings); - } - else - { - throw new NotSupportedException($"{appSettings.Database} is not supported"); - } + if (appSettings.Database is DatabaseServer.PostgreSql + or DatabaseServer.MySql) + { + BenchmarkApplication.Db = new RawDb(new ConcurrentRandom(), appSettings); + } + else + { + throw new NotSupportedException($"{appSettings.Database} is not supported"); + } #endif var hostBuilder = new WebHostBuilder() diff --git a/frameworks/CSharp/aspnetcore/PlatformBenchmarks/Templates/FortunesUtf16.cshtml b/frameworks/CSharp/aspnetcore/PlatformBenchmarks/Templates/FortunesUtf16.cshtml new file mode 100644 index 00000000000..a721e3044ce --- /dev/null +++ b/frameworks/CSharp/aspnetcore/PlatformBenchmarks/Templates/FortunesUtf16.cshtml @@ -0,0 +1,2 @@ +@inherits RazorSlice> +Fortunes@foreach (var item in Model){}
idmessage
@item.Id@item.Message
\ No newline at end of file diff --git a/frameworks/CSharp/aspnetcore/PlatformBenchmarks/Templates/FortunesUtf8.cshtml b/frameworks/CSharp/aspnetcore/PlatformBenchmarks/Templates/FortunesUtf8.cshtml new file mode 100644 index 00000000000..4288f407b70 --- /dev/null +++ b/frameworks/CSharp/aspnetcore/PlatformBenchmarks/Templates/FortunesUtf8.cshtml @@ -0,0 +1,2 @@ +@inherits RazorSlice> +Fortunes@foreach (var item in Model){}
idmessage
@item.Id@item.Message
\ No newline at end of file diff --git a/frameworks/CSharp/aspnetcore/PlatformBenchmarks/Templates/_ViewImports.cshtml b/frameworks/CSharp/aspnetcore/PlatformBenchmarks/Templates/_ViewImports.cshtml new file mode 100644 index 00000000000..7cda0a3015e --- /dev/null +++ b/frameworks/CSharp/aspnetcore/PlatformBenchmarks/Templates/_ViewImports.cshtml @@ -0,0 +1,9 @@ +@inherits RazorSlice + +@using System.Globalization; +@using Microsoft.AspNetCore.Razor; +@using RazorSlices; +@using PlatformBenchmarks; + +@tagHelperPrefix __disable_tagHelpers__: +@removeTagHelper *, Microsoft.AspNetCore.Mvc.Razor \ No newline at end of file diff --git a/frameworks/CSharp/aspnetcore/PlatformBenchmarks/appsettings.json b/frameworks/CSharp/aspnetcore/PlatformBenchmarks/appsettings.json index d7a356382f6..2fa97145aaa 100644 --- a/frameworks/CSharp/aspnetcore/PlatformBenchmarks/appsettings.json +++ b/frameworks/CSharp/aspnetcore/PlatformBenchmarks/appsettings.json @@ -1,3 +1,4 @@ { - "ConnectionString": "Server=(localdb)\\mssqllocaldb;Database=aspnetcore-Benchmarks;Trusted_Connection=True;MultipleActiveResultSets=true" + "ConnectionString": "Server=localhost;Database=fortunes;User Id=test;Password=test", + "Database": "PostgreSQL" } From 383d34c361af0f51e2ade294f125663d8468abf4 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Sat, 18 Mar 2023 17:32:12 +0100 Subject: [PATCH 5/5] Turn of SQL parsing/rewriting for Fortunes --- .../CSharp/aspnetcore/PlatformBenchmarks/Program.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/frameworks/CSharp/aspnetcore/PlatformBenchmarks/Program.cs b/frameworks/CSharp/aspnetcore/PlatformBenchmarks/Program.cs index 365c772bf3f..79b8d7de6de 100644 --- a/frameworks/CSharp/aspnetcore/PlatformBenchmarks/Program.cs +++ b/frameworks/CSharp/aspnetcore/PlatformBenchmarks/Program.cs @@ -14,6 +14,14 @@ public static async Task Main(string[] args) { Args = args; +#if NPGSQL + // This disables SQL parsing/rewriting, which requires using positional parameters and NpgsqlBatch everywhere. + // This helps commands where there are no parameters (Fortunes); when there are parameters, their ParameterName + // being null already triggers positional parameters and disables parsing) + // Note that Dapper and EF aren't yet compatible with this mode. + AppContext.SetSwitch("Npgsql.EnableSqlRewriting", false); +#endif + Console.WriteLine(Encoding.UTF8.GetString(BenchmarkApplication.ApplicationName)); #if !DATABASE Console.WriteLine(Encoding.UTF8.GetString(BenchmarkApplication.Paths.Plaintext));