diff --git a/.dockerignore b/.dockerignore index da193f387..78e7fc0af 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,2 +1,5 @@ .git* .dockerignore +# Ignore all built assets +**/[b|B]in/ +**/[O|o]bj/ diff --git a/src/BenchmarksApps/TechEmpower/Minimal/Program.cs b/src/BenchmarksApps/TechEmpower/Minimal/Program.cs index 287f7a035..05d3b7b8c 100644 --- a/src/BenchmarksApps/TechEmpower/Minimal/Program.cs +++ b/src/BenchmarksApps/TechEmpower/Minimal/Program.cs @@ -5,6 +5,10 @@ using Minimal; using Minimal.Database; using Minimal.Models; +using Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets; +using System.Net.Sockets; +using System.Net; +using System.Runtime.InteropServices; var builder = WebApplication.CreateBuilder(args); @@ -13,9 +17,41 @@ builder.WebHost.ConfigureKestrel(options => { - options.AllowSynchronousIO = true; + options.AllowSynchronousIO = true; }); +// Allow multiple processes bind to the same port. This also "works" on Windows in that it will +// prevent address in use errors and hand off to another process if no others are available, +// but it wouldn't round-robin new connections between processes like it will on Linux. +if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) +{ + builder.WebHost.UseSockets(options => + { + options.CreateBoundListenSocket = endpoint => + { + if (endpoint is not IPEndPoint ip) + { + return SocketTransportOptions.CreateDefaultBoundListenSocket(endpoint); + } + + // Normally, we'd call CreateDefaultBoundListenSocket for the IPEndpoint too, but we need + // to set ReuseAddress before calling bind, and CreateDefaultBoundListenSocket calls bind. + var listenSocket = new Socket(ip.AddressFamily, SocketType.Stream, ProtocolType.Tcp); + + // Kestrel expects IPv6Any to bind to both IPv6 and IPv4 + if (ip.Address.Equals(IPAddress.IPv6Any)) + { + listenSocket.DualMode = true; + } + + listenSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); + listenSocket.Bind(ip); + + return listenSocket; + }; + }); +} + // Load custom configuration var appSettings = new AppSettings(); builder.Configuration.Bind(appSettings); @@ -40,7 +76,8 @@ var createFortunesTemplate = RazorSlice.ResolveSliceFactory>("/Templates/Fortunes.cshtml"); var htmlEncoder = CreateHtmlEncoder(); -app.MapGet("/fortunes", async (HttpContext context, Db db) => { +app.MapGet("/fortunes", async (HttpContext context, Db db) => +{ var fortunes = await db.LoadFortunesRows(); var template = (RazorSliceHttpResult>)createFortunesTemplate(fortunes); template.HtmlEncoder = htmlEncoder; diff --git a/src/BenchmarksApps/TechEmpower/PlatformBenchmarks/PlatformBenchmarks.csproj b/src/BenchmarksApps/TechEmpower/PlatformBenchmarks/PlatformBenchmarks.csproj index 119e04f60..8e4a8b2d0 100644 --- a/src/BenchmarksApps/TechEmpower/PlatformBenchmarks/PlatformBenchmarks.csproj +++ b/src/BenchmarksApps/TechEmpower/PlatformBenchmarks/PlatformBenchmarks.csproj @@ -1,7 +1,7 @@  - net7.0;net8.0 + net7.0;net8.0;net9.0 Exe true true diff --git a/src/BenchmarksApps/TechEmpower/PlatformBenchmarks/Program.cs b/src/BenchmarksApps/TechEmpower/PlatformBenchmarks/Program.cs index 58238dbe2..b379841f5 100644 --- a/src/BenchmarksApps/TechEmpower/PlatformBenchmarks/Program.cs +++ b/src/BenchmarksApps/TechEmpower/PlatformBenchmarks/Program.cs @@ -2,10 +2,13 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Net; +using System.Net.Sockets; using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets; using Microsoft.Extensions.Configuration; #if DATABASE using Npgsql; @@ -106,6 +109,32 @@ public static IWebHost BuildWebHost(string[] args) if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { options.UnsafePreferInlineScheduling = Environment.GetEnvironmentVariable("DOTNET_SYSTEM_NET_SOCKETS_INLINE_COMPLETIONS") == "1"; + + // Allow multiple processes bind to the same port. This also "works" on Windows in that it will + // prevent address in use errors and hand off to another process if no others are available, + // but it wouldn't round-robin new connections between processes like it will on Linux. + options.CreateBoundListenSocket = endpoint => + { + if (endpoint is not IPEndPoint ip) + { + return SocketTransportOptions.CreateDefaultBoundListenSocket(endpoint); + } + + // Normally, we'd call CreateDefaultBoundListenSocket for the IPEndpoint too, but we need + // to set ReuseAddress before calling bind, and CreateDefaultBoundListenSocket calls bind. + var listenSocket = new Socket(ip.AddressFamily, SocketType.Stream, ProtocolType.Tcp); + + // Kestrel expects IPv6Any to bind to both IPv6 and IPv4 + if (ip.Address.Equals(IPAddress.IPv6Any)) + { + listenSocket.DualMode = true; + } + + listenSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); + listenSocket.Bind(ip); + + return listenSocket; + }; } }); diff --git a/src/BenchmarksApps/TechEmpower/dockerfile b/src/BenchmarksApps/TechEmpower/dockerfile new file mode 100644 index 000000000..0664c548d --- /dev/null +++ b/src/BenchmarksApps/TechEmpower/dockerfile @@ -0,0 +1,15 @@ +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build +WORKDIR /app +COPY . . +RUN dotnet publish ./src/BenchmarksApps/TechEmpower/PlatformBenchmarks/PlatformBenchmarks.csproj -c Release -o out -f net9.0 +COPY ./src/BenchmarksApps/TechEmpower/startprocess.sh out + +FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS runtime +ENV URLS=http://+:8080 + +WORKDIR /app +COPY --from=build /app/out ./ + +EXPOSE 8080 + +CMD ["./startprocess.sh"] diff --git a/src/BenchmarksApps/TechEmpower/dockerfile-db b/src/BenchmarksApps/TechEmpower/dockerfile-db new file mode 100644 index 000000000..258547129 --- /dev/null +++ b/src/BenchmarksApps/TechEmpower/dockerfile-db @@ -0,0 +1,15 @@ +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build +WORKDIR /app +COPY . . +RUN dotnet publish ./src/BenchmarksApps/TechEmpower/PlatformBenchmarks/PlatformBenchmarks.csproj -c Release -o out -f net9.0 /p:IsDatabase=true +COPY ./src/BenchmarksApps/TechEmpower/startprocess.sh out + +FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS runtime +ENV URLS=http://+:8080 + +WORKDIR /app +COPY --from=build /app/out ./ + +EXPOSE 8080 + +CMD ["./startprocess.sh"] diff --git a/src/BenchmarksApps/TechEmpower/startprocess.sh b/src/BenchmarksApps/TechEmpower/startprocess.sh new file mode 100755 index 000000000..6577ea8ea --- /dev/null +++ b/src/BenchmarksApps/TechEmpower/startprocess.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +# Install dependencies +apt-get update \ + && apt-get install -y --no-install-recommends \ + cgroup-tools + +cgcreate -g cpu,cpuset:/cpugroup1 +cgcreate -g cpu,cpuset:/cpugroup2 + +cgset -r cpuset.cpus=0-5 /cpugroup1 +cgset -r cpuset.cpus=6-12 /cpugroup2 + +cgexec -g cpu:/cpugroup1 dotnet ./PlatformBenchmarks.dll & +cgexec -g cpu:/cpugroup2 dotnet ./PlatformBenchmarks.dll + +cgdelete -g cpu:/cpugroup1 +cgdelete -g cpu:/cpugroup2 diff --git a/src/BenchmarksApps/TechEmpower/te.benchmarks.yml b/src/BenchmarksApps/TechEmpower/te.benchmarks.yml new file mode 100644 index 000000000..2b17f6709 --- /dev/null +++ b/src/BenchmarksApps/TechEmpower/te.benchmarks.yml @@ -0,0 +1,66 @@ +imports: + - https://raw.githubusercontent.com/dotnet/crank/main/src/Microsoft.Crank.Jobs.Wrk/wrk.yml + - https://raw.githubusercontent.com/dotnet/crank/main/src/Microsoft.Crank.Jobs.Bombardier/bombardier.yml + - https://github.com/aspnet/Benchmarks/blob/main/scenarios/aspnet.profiles.yml?raw=true + +jobs: + + aspnetcore: + source: + repository: https://github.com/aspnet/Benchmarks + branchOrCommit: main + dockerFile: src/BenchmarksApps/TechEmpower/dockerfile + dockerImageName: aspnetcore_reuse + dockerContextDirectory: ./ + readyStateText: Application started + arguments: --add-host="tfb-database:{{databaseServer}}" + noClean: true + + postgresql: + 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: + +# ASP.NET Core (Platform) + plaintext_aspnetcore: + application: + job: aspnetcore + load: + job: wrk + variables: + presetHeaders: plaintext + path: /plaintext + pipeline: 16 + serverPort: 8080 + + json_aspnetcore: + application: + job: aspnetcore + load: + job: wrk + variables: + presetHeaders: json + path: /json + serverPort: 8080 + + fortunes_aspnetcore: + db: + job: postgresql + application: + job: aspnetcore + source: + dockerFile: src/BenchmarksApps/TechEmpower/dockerfile-db + dockerImageName: aspnetcore_reuse_db + load: + job: wrk + variables: + presetHeaders: html + path: /fortunes + serverPort: 8080