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 5d951303e0b..793d20af48b 100644 --- a/frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkApplication.Fortunes.cs +++ b/frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkApplication.Fortunes.cs @@ -3,53 +3,63 @@ #if DATABASE -using System.Collections.Generic; 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); + // 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); - // Date header - writer.Write(DateHeader.HeaderBytes); + // 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); - var bodyStart = writer.Buffered; - // Body - writer.Write(_fortunesTableStart); - foreach (var item in model) + if (renderTask.IsCompletedSuccessfully) { - writer.Write(_fortunesRowStart); - writer.WriteNumeric((uint)item.Id); - writer.Write(_fortunesColumn); - writer.WriteUtf8String(HtmlEncoder.Encode(item.Message)); - 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 static async ValueTask AwaitTemplateRenderTask(ValueTask renderTask, ChunkedBufferWriter chunkedWriter, RazorSlice template) + { + await renderTask; + EndTemplateRendering(chunkedWriter, template); + } + + [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/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/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/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/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 72257928094..3bcb2a9a1e9 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; @@ -18,53 +14,63 @@ 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)) - { - await db.OpenAsync(); + using var db = _dataSource.CreateConnection(); + 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]; + var results = new World[count]; + + using var connection = await _dataSource.OpenConnectionAsync(); - using (var db = new NpgsqlConnection(_connectionString)) + using var batch = new NpgsqlBatch(connection) { - await db.OpenAsync(); + // 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 + }; - var (cmd, idParameter) = CreateReadCommand(db); - using (cmd) + for (var i = 0; i < count; i++) + { + batch.BatchCommands.Add(new() { - for (int i = 0; i < result.Length; i++) - { - result[i] = await ReadSingleRow(cmd); - idParameter.TypedValue = _random.Next(1, 10001); - } - } + 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) @@ -77,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; } @@ -91,32 +97,25 @@ 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)) - { - await db.OpenAsync(); + using var db = rawdb._dataSource.CreateConnection(); + await db.OpenAsync(); - var (cmd, idParameter) = rawdb.CreateReadCommand(db); - using (cmd) - { - Func> create = async _ => - { - return await rawdb.ReadSingleRow(cmd); - }; + var (cmd, idParameter) = rawdb.CreateReadCommand(db); + using var command = cmd; + async Task create(ICacheEntry _) => await ReadSingleRow(cmd); - var cacheKeys = _cacheKeys; - var key = cacheKeys[id]; + var cacheKeys = _cacheKeys; + var key = cacheKeys[id]; - idParameter.TypedValue = id; + idParameter.TypedValue = id; - for (; i < result.Length; i++) - { - result[i] = await rawdb._cache.GetOrCreateAsync(key, create); + 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]; - } - } + id = rawdb._random.Next(1, 10001); + idParameter.TypedValue = id; + key = cacheKeys[id]; } return result; @@ -125,21 +124,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 = _dataSource.CreateConnection(); + 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,70 +144,81 @@ public async Task LoadMultipleUpdatesRows(int count) { var results = new World[count]; - using (var db = new NpgsqlConnection(_connectionString)) + using var connection = _dataSource.CreateConnection(); + await connection.OpenAsync(); + + using (var batch = new NpgsqlBatch(connection)) { - await db.OpenAsync(); + // 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; - var (queryCmd, queryParameter) = CreateReadCommand(db); - using (queryCmd) + for (var i = 0; i < count; i++) { - for (int i = 0; i < results.Length; i++) + batch.BatchCommands.Add(new() { - results[i] = await ReadSingleRow(queryCmd); - queryParameter.TypedValue = _random.Next(1, 10001); - } + CommandText = "SELECT id, randomnumber FROM world WHERE id = $1", + Parameters = { new NpgsqlParameter { TypedValue = _random.Next(1, 10001) } } + }); } - using (var updateCmd = new NpgsqlCommand(BatchUpdateString.Query(count), db)) - { - var ids = BatchUpdateString.Ids; - var randoms = BatchUpdateString.Randoms; + using var reader = await batch.ExecuteReaderAsync(); - for (int i = 0; i < results.Length; i++) - { - var randomNumber = _random.Next(1, 10001); + 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(); + } + } - updateCmd.Parameters.Add(new NpgsqlParameter(parameterName: ids[i], value: results[i].Id)); - updateCmd.Parameters.Add(new NpgsqlParameter(parameterName: randoms[i], value: randomNumber)); + using (var updateCmd = new NpgsqlCommand(BatchUpdateString.Query(count), connection)) + { + for (int i = 0; i < results.Length; i++) + { + var randomNumber = _random.Next(1, 10001); - results[i].RandomNumber = randomNumber; - } + updateCmd.Parameters.Add(new NpgsqlParameter { TypedValue = results[i].Id }); + updateCmd.Parameters.Add(new NpgsqlParameter { TypedValue = randomNumber }); - await updateCmd.ExecuteNonQueryAsync(); + results[i].RandomNumber = randomNumber; } + + await updateCmd.ExecuteNonQueryAsync(); } return results; } - public async Task> LoadFortunesRows() + public async Task> LoadFortunesRows() { - var result = new List(); + var result = new List(); - using (var db = new NpgsqlConnection(_connectionString)) + using (var db = _dataSource.CreateConnection()) { 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 FortuneUtf8 + ( + id:rdr.GetInt32(0), + 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); @@ -224,18 +230,16 @@ 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(); + 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(); 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 74ab1839180..0a68f121bde 100644 --- a/frameworks/CSharp/aspnetcore/PlatformBenchmarks/PlatformBenchmarks.csproj +++ b/frameworks/CSharp/aspnetcore/PlatformBenchmarks/PlatformBenchmarks.csproj @@ -22,7 +22,8 @@ - + + diff --git a/frameworks/CSharp/aspnetcore/PlatformBenchmarks/Program.cs b/frameworks/CSharp/aspnetcore/PlatformBenchmarks/Program.cs index 5db336614c4..79b8d7de6de 100644 --- a/frameworks/CSharp/aspnetcore/PlatformBenchmarks/Program.cs +++ b/frameworks/CSharp/aspnetcore/PlatformBenchmarks/Program.cs @@ -14,15 +14,23 @@ 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)); 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 +38,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 +54,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 +64,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" }