Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build/dependencies.props
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@

<DapperVersion>2.1.66</DapperVersion>
<DapperAotVersion>1.0.48</DapperAotVersion>
<ApexPgClientVersion>0.1.0-preview.3</ApexPgClientVersion>
<RazorSlicesVersion>0.11.1</RazorSlicesVersion>
<SystemCommandLineVersion>2.0.0-beta4.22272.1</SystemCommandLineVersion>
<MicrosoftCrankEventSourcesVersion>0.2.0-alpha.24114.2</MicrosoftCrankEventSourcesVersion>
Expand Down
4 changes: 4 additions & 0 deletions build/trend-database-scenarios.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ parameters:

- displayName: Fortunes Platform
arguments: --scenario fortunes $(platformJobs) --property scenario=FortunesPlatform --property protocol=http
- displayName: Fortunes Platform Apex
arguments: --scenario apex-fortunes --config https://raw.githubusercontent.com/aspnet/Benchmarks/main/src/BenchmarksApps/TechEmpower/ApexFortunes/apex-fortunes.benchmarks.yml --load.connections 1024 --property scenario=FortunesPlatformApex --property protocol=http
- displayName: Fortunes Platform EF
arguments: --scenario fortunes_ef $(platformJobs) --property scenario=FortunesPlatformEF --property protocol=http
- displayName: Fortunes Platform Dapper
Expand All @@ -50,6 +52,8 @@ parameters:

- displayName: Fortunes Minimal APIs
arguments: --scenario fortunes $(minimalJobs) --property scenario=FortunesMinimalApis --property protocol=http
- displayName: Fortunes Minimal APIs Apex
arguments: --scenario minimal-apex-fortunes --config https://raw.githubusercontent.com/aspnet/Benchmarks/main/src/BenchmarksApps/TechEmpower/Minimal/minimal-apex-fortunes.benchmarks.yml --load.connections 1024 --property scenario=FortunesMinimalApisApex --property protocol=http
- displayName: Single Query Minimal APIs
arguments: --scenario single_query $(minimalJobs) --property scenario=SingleQueryMinimalApis --property protocol=http
- displayName: Multiple Queries Minimal APIs
Expand Down
26 changes: 26 additions & 0 deletions src/BenchmarksApps/TechEmpower/ApexFortunes/ApexFortunes.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>preview</LangVersion>
<DefineConstants>$(DefineConstants);DATABASE</DefineConstants>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Apex.PgClient" Version="$(ApexPgClientVersion)" />
<PackageReference Include="RazorSlices" Version="$(RazorSlicesVersion)" />
</ItemGroup>

<ItemGroup>
<Compile Include="../PlatformBenchmarks/BenchmarkApplication.HttpConnection.cs" Link="Platform/BenchmarkApplication.HttpConnection.cs" />
<Compile Include="../PlatformBenchmarks/BufferExtensions.cs" Link="Platform/BufferExtensions.cs" />
<Compile Include="../PlatformBenchmarks/BufferWriter.cs" Link="Platform/BufferWriter.cs" />
<Compile Include="../PlatformBenchmarks/ChunkedPipeWriter.cs" Link="Platform/ChunkedPipeWriter.cs" />
<Compile Include="../PlatformBenchmarks/DateHeader.cs" Link="Platform/DateHeader.cs" />
<Compile Include="../PlatformBenchmarks/HttpApplication.cs" Link="Platform/HttpApplication.cs" />
<Compile Include="../PlatformBenchmarks/IHttpConnection.cs" Link="Platform/IHttpConnection.cs" />
</ItemGroup>

</Project>
117 changes: 117 additions & 0 deletions src/BenchmarksApps/TechEmpower/ApexFortunes/BenchmarkApplication.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
using System.IO.Pipelines;
using System.Runtime.CompilerServices;
using System.Text.Encodings.Web;
using ApexFortunes;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
using Microsoft.Extensions.ObjectPool;
using RazorSlices;

namespace PlatformBenchmarks;

public sealed partial class BenchmarkApplication
{
private static readonly DefaultObjectPool<ChunkedPipeWriter> ChunkedWriterPool =
new(new ChunkedWriterObjectPolicy());

private RequestType _requestType;

internal static FortuneDatabase Database { get; set; } = null!;

public void OnStartLine(
HttpVersionAndMethod versionAndMethod,
TargetOffsetPathLength targetPath,
Span<byte> startLine)
{
_requestType = versionAndMethod.Method ==
Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpMethod.Get &&
startLine.Slice(targetPath.Offset, targetPath.Length).SequenceEqual("/fortunes"u8)
? RequestType.Fortunes
: RequestType.NotRecognized;
}

private Task ProcessRequestAsync() => _requestType switch
{
RequestType.Fortunes => RenderDatabaseAsync(),
_ => OutputEmptyAsync(Writer),
};

private async Task RenderDatabaseAsync() =>
await OutputFortunesAsync(Writer, await Database.LoadAsync());

private ValueTask OutputFortunesAsync(
PipeWriter pipeWriter,
List<Fortune> fortunes)
{
var template = ApexFortunes.Templates.Fortunes.Create(fortunes);
var chunkedWriter = StartResponse(pipeWriter);
var renderTask = template.RenderAsync(chunkedWriter, HtmlEncoder);
if (renderTask.IsCompletedSuccessfully)
{
renderTask.GetAwaiter().GetResult();
EndTemplateRendering(chunkedWriter, template);
return ValueTask.CompletedTask;
}

return AwaitTemplateRenderTask(renderTask, chunkedWriter, template);
}

private static Task OutputEmptyAsync(PipeWriter pipeWriter)
{
var writer = StartResponse(pipeWriter);
writer.Complete();
ReturnChunkedWriter(writer);
return Task.CompletedTask;
}

private static ChunkedPipeWriter StartResponse(PipeWriter pipeWriter)
{
var preamble =
"HTTP/1.1 200 OK\r\nServer: K\r\nContent-Type: text/html; charset=utf-8\r\nTransfer-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);

var writer = ChunkedWriterPool.Get();
writer.SetOutput(pipeWriter, 2048);
return writer;
}

private static async ValueTask AwaitTemplateRenderTask(
ValueTask renderTask,
ChunkedPipeWriter chunkedWriter,
RazorSlice template)
{
await renderTask;
EndTemplateRendering(chunkedWriter, template);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void EndTemplateRendering(
ChunkedPipeWriter chunkedWriter,
RazorSlice template)
{
chunkedWriter.Complete();
ReturnChunkedWriter(chunkedWriter);
template.Dispose();
}

private sealed class ChunkedWriterObjectPolicy :
IPooledObjectPolicy<ChunkedPipeWriter>
{
public ChunkedPipeWriter Create() => new();

public bool Return(ChunkedPipeWriter writer)
{
writer.Reset();
return true;
}
}

private enum RequestType
{
NotRecognized,
Fortunes,
}
}
17 changes: 17 additions & 0 deletions src/BenchmarksApps/TechEmpower/ApexFortunes/Fortune.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace ApexFortunes;

public readonly struct Fortune : IComparable<Fortune>
{
public Fortune(int id, ReadOnlyMemory<byte> message)
{
Id = id;
Message = message;
}

public int Id { get; }

public ReadOnlyMemory<byte> Message { get; }

public int CompareTo(Fortune other) =>
Message.Span.SequenceCompareTo(other.Message.Span);
}
73 changes: 73 additions & 0 deletions src/BenchmarksApps/TechEmpower/ApexFortunes/FortuneDatabase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
using Apex.PgClient;
using Apex.SqlClient;

namespace ApexFortunes;

internal sealed class FortuneDatabase : IAsyncDisposable
{
private static readonly ReadOnlyMemory<byte> s_additionalFortune =
"Additional fortune added at request time."u8.ToArray();
private readonly PgPipelinePool _pool;
private readonly ISqlPreparedStatement _statement;

private FortuneDatabase(
PgPipelinePool pool,
ISqlPreparedStatement statement)
{
_pool = pool;
_statement = statement;
}

public static async ValueTask<FortuneDatabase> CreateAsync(
IConfiguration configuration)
{
var connectionString = configuration["CONNECTION_STRING"] ??
throw new InvalidOperationException("CONNECTION_STRING is required.");
var connectionCount = configuration.GetValue("APEX_CONNECTIONS", 56);
var pipeliningLimit = configuration.GetValue("APEX_PIPELINING", 64);
var options = PgConnectOptions.Parse(connectionString) with
{
PipeliningLimit = pipeliningLimit,
};
var pool = await PgPipelinePool.CreateAsync(
options,
new SqlPipelinePoolOptions { ConnectionCount = connectionCount });
try
{
var statement = await pool.PrepareAsync(
"SELECT id, message FROM fortune");
return new FortuneDatabase(pool, statement);
}
catch
{
await pool.DisposeAsync();
throw;
}
}

public async ValueTask<List<Fortune>> LoadAsync()
{
// Benchmark requirements explicitly prohibit pre-initializing the list size.
List<Fortune> fortunes = [];
await _statement.CollectAsync(
fortunes,
static (results, row) => results.Add(new Fortune(
row.GetInt32(0),
row.Get<ReadOnlyMemory<byte>>(1))));
fortunes.Add(new Fortune(0, s_additionalFortune));
fortunes.Sort();
return fortunes;
}

public async ValueTask DisposeAsync()
{
try
{
await _statement.DisposeAsync();
}
finally
{
await _pool.DisposeAsync();
}
}
}
43 changes: 43 additions & 0 deletions src/BenchmarksApps/TechEmpower/ApexFortunes/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using System.Net;
using System.Runtime.InteropServices;
using ApexFortunes;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using PlatformBenchmarks;

var configuration = new ConfigurationBuilder()
.AddEnvironmentVariables()
.AddCommandLine(args)
.Build();
var url = new Uri(configuration["urls"] ?? "http://0.0.0.0:5000");

await using var database = await FortuneDatabase.CreateAsync(configuration);
BenchmarkApplication.Database = database;
DateHeader.SyncDateTimer();

var hostBuilder = new WebHostBuilder()
.UseConfiguration(configuration)
.UseKestrel(options =>
{
options.Listen(IPAddress.Any, url.Port, listen =>
{
listen.UseHttpApplication<BenchmarkApplication>();
});
})
.Configure(_ => { });

hostBuilder.UseSockets(options =>
{
options.WaitForDataBeforeAllocatingBuffer = false;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
options.UnsafePreferInlineScheduling =
Environment.GetEnvironmentVariable(
"DOTNET_SYSTEM_NET_SOCKETS_INLINE_COMPLETIONS") == "1";
}
});

using var host = hostBuilder.Build();
await host.StartAsync();
Console.WriteLine("Application started.");
await host.WaitForShutdownAsync();
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@inherits RazorSlice<List<Fortune>>
<!DOCTYPE html><html><head><title>Fortunes</title></head><body><table><tr><th>id</th><th>message</th></tr>@foreach (var item in Model){<tr><td>@WriteNumber(item.Id, default, CultureInfo.InvariantCulture, false)</td><td>@item.Message.Span</td></tr>}</table></body></html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
@inherits RazorSlice

@using System.Globalization;
@using Microsoft.AspNetCore.Razor;
@using RazorSlices;
@using ApexFortunes;

@tagHelperPrefix __disable_tagHelpers__:
@removeTagHelper *, Microsoft.AspNetCore.Mvc.Razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
imports:
- https://raw.githubusercontent.com/dotnet/crank/main/src/Microsoft.Crank.Jobs.Wrk/wrk.yml
- https://raw.githubusercontent.com/aspnet/Benchmarks/main/scenarios/aspnet.profiles.standard.yml

variables:
serverPort: 5000

jobs:
apex-fortunes:
source:
repository: https://github.com/aspnet/benchmarks.git
branchOrCommit: main
project: src/BenchmarksApps/TechEmpower/ApexFortunes/ApexFortunes.csproj
readyStateText: Application started.
arguments: "--urls {{serverScheme}}://{{serverAddress}}:{{serverPort}}"
variables:
serverScheme: http
environmentVariables:
CONNECTION_STRING: "Host={{databaseServer}} Database=hello_world Username=benchmarkdbuser Password=benchmarkdbpass"
APEX_CONNECTIONS: 56
APEX_PIPELINING: 64

postgresql-apex:
source:
repository: https://github.com/TechEmpower/FrameworkBenchmarks.git
branchOrCommit: master
dockerFile: toolset/databases/postgres/postgres.dockerfile
dockerImageName: postgres_te
dockerContextDirectory: toolset/databases/postgres
readyStateText: ready to accept connections
noClean: true

scenarios:
apex-fortunes:
db:
job: postgresql-apex
application:
job: apex-fortunes
load:
job: wrk
variables:
presetHeaders: html
path: /fortunes
connections: 1024
Loading
Loading