From ac80de97dc35d19e527ee2e95073ae0727b168e5 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Tue, 17 Dec 2024 13:46:16 -0800 Subject: [PATCH 1/3] Add Fortunes endpoint that returns RazorComponentResult --- .../TechEmpower/Minimal/Database/Db.cs | 27 +++++++- .../Minimal/FortunesRazorParameters.cs | 63 +++++++++++++++++++ .../TechEmpower/Minimal/Program.cs | 17 ++++- .../Minimal/Templates/FortunesRazor.razor | 5 ++ .../Minimal/Templates/_Imports.razor | 2 + .../Minimal/minimal.benchmarks.yml | 4 +- 6 files changed, 113 insertions(+), 5 deletions(-) create mode 100644 src/BenchmarksApps/TechEmpower/Minimal/FortunesRazorParameters.cs create mode 100644 src/BenchmarksApps/TechEmpower/Minimal/Templates/FortunesRazor.razor create mode 100644 src/BenchmarksApps/TechEmpower/Minimal/Templates/_Imports.razor diff --git a/src/BenchmarksApps/TechEmpower/Minimal/Database/Db.cs b/src/BenchmarksApps/TechEmpower/Minimal/Database/Db.cs index 56e161f53..84bb79e79 100644 --- a/src/BenchmarksApps/TechEmpower/Minimal/Database/Db.cs +++ b/src/BenchmarksApps/TechEmpower/Minimal/Database/Db.cs @@ -1,4 +1,4 @@ -using System.Data.Common; +using System.Data.Common; using Dapper; using Minimal.Models; @@ -99,4 +99,29 @@ public async Task> LoadFortunesRows() return result; } + + public Task> LoadFortunesRowsNoDb() + { + // Benchmark requirements explicitly prohibit pre-initializing the list size + var result = new List + { + new(1, "fortune: No such file or directory"), + new(2, "A computer scientist is someone who fixes things that aren't broken."), + new(3, "After enough decimal places, nobody gives a damn."), + new(4, "A bad random number generator: 1, 1, 1, 1, 1, 4.33e+67, 1, 1, 1"), + new(5, "A computer program does what you tell it to do, not what you want it to do."), + new(6, "Emacs is a nice operating system, but I prefer UNIX. — Tom Christaensen"), + new(7, "Any program that runs right is obsolete."), + new(8, "A list is only as strong as its weakest link. — Donald Knuth"), + new(9, "Feature: A bug with seniority."), + new(10, "Computers make very fast, very accurate mistakes."), + new(11, ""), + new(12, "フレームワークのベンチマーク"), + new(0, "Additional fortune added at request time.") + }; + + result.Sort(FortuneSortComparison); + + return Task.FromResult(result); + } } \ No newline at end of file diff --git a/src/BenchmarksApps/TechEmpower/Minimal/FortunesRazorParameters.cs b/src/BenchmarksApps/TechEmpower/Minimal/FortunesRazorParameters.cs new file mode 100644 index 000000000..c6e56b09c --- /dev/null +++ b/src/BenchmarksApps/TechEmpower/Minimal/FortunesRazorParameters.cs @@ -0,0 +1,63 @@ +using System.Collections; +using System.Diagnostics.CodeAnalysis; +using Minimal.Models; +using Minimal.Templates; + +namespace Minimal; + +internal readonly struct FortunesRazorParameters(List model) : IReadOnlyDictionary +{ + private const string ModelKeyName = nameof(FortunesRazor.Model); + + private readonly KeyValuePair _modelKvp = new(ModelKeyName, model); + + public object? this[string key] => KeyIsModel(key) ? model : null; + + public IEnumerable Keys { get; } = [ModelKeyName]; + + public IEnumerable Values { get; } = [model]; + + public int Count { get; } = 1; + + public bool ContainsKey(string key) => KeyIsModel(key); + + public IEnumerator> GetEnumerator() => new Enumerator(_modelKvp); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public bool TryGetValue(string key, [MaybeNullWhen(false)] out object? value) + { + if (KeyIsModel(key)) + { + value = model; + return true; + } + value = default; + return false; + } + + private static bool KeyIsModel(string key) => ModelKeyName.Equals(key, StringComparison.Ordinal); + + private struct Enumerator(KeyValuePair kvp) : IEnumerator> + { + private bool _moved; + + public readonly KeyValuePair Current { get; } = kvp; + + readonly object IEnumerator.Current => Current; + + public bool MoveNext() + { + if (_moved) + { + return false; + } + _moved = true; + return true; + } + + public readonly void Dispose() { } + + public void Reset() => throw new NotSupportedException(); + } +} diff --git a/src/BenchmarksApps/TechEmpower/Minimal/Program.cs b/src/BenchmarksApps/TechEmpower/Minimal/Program.cs index 9b0d160e4..311a282e0 100644 --- a/src/BenchmarksApps/TechEmpower/Minimal/Program.cs +++ b/src/BenchmarksApps/TechEmpower/Minimal/Program.cs @@ -1,7 +1,6 @@ using System.Text.Encodings.Web; using System.Text.Unicode; using Microsoft.AspNetCore.Http.HttpResults; -using RazorSlices; using Minimal; using Minimal.Database; using Minimal.Models; @@ -23,6 +22,7 @@ // Add services to the container. builder.Services.AddSingleton(new Db(appSettings)); +builder.Services.AddRazorComponents(); var app = builder.Build(); @@ -42,11 +42,24 @@ app.MapGet("/fortunes", async (HttpContext context, Db db) => { var fortunes = await db.LoadFortunesRows(); + //var fortunes = await db.LoadFortunesRowsNoDb(); // Don't call the database var template = (RazorSliceHttpResult>)Fortunes.Create(fortunes); template.HtmlEncoder = htmlEncoder; return template; }); +app.MapGet("/fortunes/razor", async (HttpContext context, Db db) => { + var fortunes = await db.LoadFortunesRows(); + //var fortunes = await db.LoadFortunesRowsNoDb(); // Don't call the database + var parameters = new Dictionary { { nameof(FortunesRazor.Model), fortunes } }; + //var parameters = new FortunesRazorParameters(fortunes); // Custom parameters class to avoid allocating a Dictionary + var result = new RazorComponentResult(parameters) + { + PreventStreamingRendering = true + }; + return result; +}); + app.MapGet("/queries/{count}", async (Db db, int count) => await db.LoadMultipleQueriesRows(count)); app.MapGet("/queries/{count}/result", async (Db db, int count) => Results.Json(await db.LoadMultipleQueriesRows(count))); @@ -65,4 +78,4 @@ static HtmlEncoder CreateHtmlEncoder() var settings = new TextEncoderSettings(UnicodeRanges.BasicLatin, UnicodeRanges.Katakana, UnicodeRanges.Hiragana); settings.AllowCharacter('\u2014'); // allow EM DASH through return HtmlEncoder.Create(settings); -} \ No newline at end of file +} diff --git a/src/BenchmarksApps/TechEmpower/Minimal/Templates/FortunesRazor.razor b/src/BenchmarksApps/TechEmpower/Minimal/Templates/FortunesRazor.razor new file mode 100644 index 000000000..3acec2536 --- /dev/null +++ b/src/BenchmarksApps/TechEmpower/Minimal/Templates/FortunesRazor.razor @@ -0,0 +1,5 @@ +Fortunes@foreach (var item in Model){}
idmessage
@(new MarkupString(item.Id.ToString(CultureInfo.InvariantCulture)))@item.Message
+@code { + [Parameter] + public required List Model { get; set; } +} \ No newline at end of file diff --git a/src/BenchmarksApps/TechEmpower/Minimal/Templates/_Imports.razor b/src/BenchmarksApps/TechEmpower/Minimal/Templates/_Imports.razor new file mode 100644 index 000000000..00b58faaa --- /dev/null +++ b/src/BenchmarksApps/TechEmpower/Minimal/Templates/_Imports.razor @@ -0,0 +1,2 @@ +@using System.Globalization; +@using Minimal.Models; \ No newline at end of file diff --git a/src/BenchmarksApps/TechEmpower/Minimal/minimal.benchmarks.yml b/src/BenchmarksApps/TechEmpower/Minimal/minimal.benchmarks.yml index 0b0472011..780aeb1e6 100644 --- a/src/BenchmarksApps/TechEmpower/Minimal/minimal.benchmarks.yml +++ b/src/BenchmarksApps/TechEmpower/Minimal/minimal.benchmarks.yml @@ -78,7 +78,7 @@ scenarios: presetHeaders: html path: /fortunes - fortunes_result: + fortunes_razor: db: job: postgresql application: @@ -87,7 +87,7 @@ scenarios: job: wrk variables: presetHeaders: html - path: /fortunes/result + path: /fortunes/razor single_query: db: From adf3c1eaa38e591ca722b3725b262fcf80a80813 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Tue, 17 Dec 2024 13:59:21 -0800 Subject: [PATCH 2/3] Add custom HtmlEncoder to DI --- src/BenchmarksApps/TechEmpower/BlazorSSR/Program.cs | 1 - src/BenchmarksApps/TechEmpower/Minimal/Program.cs | 5 ++--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/BenchmarksApps/TechEmpower/BlazorSSR/Program.cs b/src/BenchmarksApps/TechEmpower/BlazorSSR/Program.cs index 5c65cd6e7..362309dca 100644 --- a/src/BenchmarksApps/TechEmpower/BlazorSSR/Program.cs +++ b/src/BenchmarksApps/TechEmpower/BlazorSSR/Program.cs @@ -25,7 +25,6 @@ builder.Services.AddRazorComponents(); builder.Services.AddSingleton(serviceProvider => { - // TODO: This custom configured HtmlEncoder won't actually be used until Blazor supports it: https://github.com/dotnet/aspnetcore/issues/47477 var settings = new TextEncoderSettings(UnicodeRanges.BasicLatin, UnicodeRanges.Katakana, UnicodeRanges.Hiragana); settings.AllowCharacter('\u2014'); // allow EM DASH through return HtmlEncoder.Create(settings); diff --git a/src/BenchmarksApps/TechEmpower/Minimal/Program.cs b/src/BenchmarksApps/TechEmpower/Minimal/Program.cs index 311a282e0..94d0185ba 100644 --- a/src/BenchmarksApps/TechEmpower/Minimal/Program.cs +++ b/src/BenchmarksApps/TechEmpower/Minimal/Program.cs @@ -23,6 +23,7 @@ // Add services to the container. builder.Services.AddSingleton(new Db(appSettings)); builder.Services.AddRazorComponents(); +builder.Services.AddSingleton(CreateHtmlEncoder()); var app = builder.Build(); @@ -38,9 +39,7 @@ app.MapGet("/db/result", async (Db db) => Results.Json(await db.LoadSingleQueryRow())); -var htmlEncoder = CreateHtmlEncoder(); - -app.MapGet("/fortunes", async (HttpContext context, Db db) => { +app.MapGet("/fortunes", async (HttpContext context, Db db, HtmlEncoder htmlEncoder) => { var fortunes = await db.LoadFortunesRows(); //var fortunes = await db.LoadFortunesRowsNoDb(); // Don't call the database var template = (RazorSliceHttpResult>)Fortunes.Create(fortunes); From 20301ed04d4306a56cf6e23297c0af49fa72dc01 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Wed, 18 Dec 2024 16:27:15 -0800 Subject: [PATCH 3/3] Move to BlazorSSR project --- .../Components/FortunesParameters.razor | 19 +++++++++++ .../TechEmpower/BlazorSSR/Database/Db.cs | 29 ++++++++++++++-- .../FortunesRazorParameters.cs | 8 ++--- .../TechEmpower/BlazorSSR/Program.cs | 12 +++++++ .../BlazorSSR/blazorssr.benchmarks.yml | 33 +++++++++++++++++++ .../TechEmpower/Minimal/Program.cs | 13 -------- .../Minimal/Templates/FortunesRazor.razor | 5 --- .../Minimal/Templates/_Imports.razor | 2 -- .../Minimal/minimal.benchmarks.yml | 4 +-- 9 files changed, 97 insertions(+), 28 deletions(-) create mode 100644 src/BenchmarksApps/TechEmpower/BlazorSSR/Components/FortunesParameters.razor rename src/BenchmarksApps/TechEmpower/{Minimal => BlazorSSR}/FortunesRazorParameters.cs (91%) delete mode 100644 src/BenchmarksApps/TechEmpower/Minimal/Templates/FortunesRazor.razor delete mode 100644 src/BenchmarksApps/TechEmpower/Minimal/Templates/_Imports.razor diff --git a/src/BenchmarksApps/TechEmpower/BlazorSSR/Components/FortunesParameters.razor b/src/BenchmarksApps/TechEmpower/BlazorSSR/Components/FortunesParameters.razor new file mode 100644 index 000000000..ab2aa39f1 --- /dev/null +++ b/src/BenchmarksApps/TechEmpower/BlazorSSR/Components/FortunesParameters.razor @@ -0,0 +1,19 @@ + + + + Fortunes + + + + + @foreach (var item in Rows) + { + + } +
idmessage
@item.Id@item.Message
+ + +@code { + [Parameter] + public required List Rows { get; set; } +} diff --git a/src/BenchmarksApps/TechEmpower/BlazorSSR/Database/Db.cs b/src/BenchmarksApps/TechEmpower/BlazorSSR/Database/Db.cs index 2d6f48887..0a49d6098 100644 --- a/src/BenchmarksApps/TechEmpower/BlazorSSR/Database/Db.cs +++ b/src/BenchmarksApps/TechEmpower/BlazorSSR/Database/Db.cs @@ -22,7 +22,7 @@ public async Task> LoadFortunesRowsDapper() await using var connection = _dataSource.CreateConnection(); var result = (await connection.QueryAsync($"SELECT id, message FROM fortune")).AsList(); - result.Add(new Fortune { Id = 0, Message = "Additional fortune added at request time." }); + result.Add(new() { Id = 0, Message = "Additional fortune added at request time." }); result.Sort(FortuneSortComparison); return result; @@ -37,11 +37,36 @@ public async Task> LoadFortunesRowsEf(AppDbContext dbContext) result.Add(fortune); } - result.Add(new Fortune { Id = 0, Message = "Additional fortune added at request time." }); + result.Add(new() { Id = 0, Message = "Additional fortune added at request time." }); result.Sort(FortuneSortComparison); return result; } + public Task> LoadFortunesRowsNoDb() + { + // Benchmark requirements explicitly prohibit pre-initializing the list size + var result = new List + { + new() { Id = 1, Message = "fortune: No such file or directory" }, + new() { Id = 2, Message = "A computer scientist is someone who fixes things that aren't broken." }, + new() { Id = 3, Message = "After enough decimal places, nobody gives a damn." }, + new() { Id = 4, Message = "A bad random number generator: 1, 1, 1, 1, 1, 4.33e+67, 1, 1, 1" }, + new() { Id = 5, Message = "A computer program does what you tell it to do, not what you want it to do." }, + new() { Id = 6, Message = "Emacs is a nice operating system, but I prefer UNIX. — Tom Christaensen" }, + new() { Id = 7, Message = "Any program that runs right is obsolete." }, + new() { Id = 8, Message = "A list is only as strong as its weakest link. — Donald Knuth" }, + new() { Id = 9, Message = "Feature: A bug with seniority." }, + new() { Id = 10, Message = "Computers make very fast, very accurate mistakes." }, + new() { Id = 11, Message = "" }, + new() { Id = 12, Message = "フレームワークのベンチマーク" }, + new() { Id = 0, Message = "Additional fortune added at request time." } + }; + + result.Sort(FortuneSortComparison); + + return Task.FromResult(result); + } + ValueTask IAsyncDisposable.DisposeAsync() => _dataSource.DisposeAsync(); } diff --git a/src/BenchmarksApps/TechEmpower/Minimal/FortunesRazorParameters.cs b/src/BenchmarksApps/TechEmpower/BlazorSSR/FortunesRazorParameters.cs similarity index 91% rename from src/BenchmarksApps/TechEmpower/Minimal/FortunesRazorParameters.cs rename to src/BenchmarksApps/TechEmpower/BlazorSSR/FortunesRazorParameters.cs index c6e56b09c..21adb0dd1 100644 --- a/src/BenchmarksApps/TechEmpower/Minimal/FortunesRazorParameters.cs +++ b/src/BenchmarksApps/TechEmpower/BlazorSSR/FortunesRazorParameters.cs @@ -1,13 +1,13 @@ using System.Collections; using System.Diagnostics.CodeAnalysis; -using Minimal.Models; -using Minimal.Templates; +using BlazorSSR.Components; +using BlazorSSR.Models; -namespace Minimal; +namespace BlazorSSR; internal readonly struct FortunesRazorParameters(List model) : IReadOnlyDictionary { - private const string ModelKeyName = nameof(FortunesRazor.Model); + private const string ModelKeyName = nameof(FortunesParameters.Rows); private readonly KeyValuePair _modelKvp = new(ModelKeyName, model); diff --git a/src/BenchmarksApps/TechEmpower/BlazorSSR/Program.cs b/src/BenchmarksApps/TechEmpower/BlazorSSR/Program.cs index 362309dca..06861d0ee 100644 --- a/src/BenchmarksApps/TechEmpower/BlazorSSR/Program.cs +++ b/src/BenchmarksApps/TechEmpower/BlazorSSR/Program.cs @@ -39,6 +39,18 @@ app.MapGet("/direct/fortunes", () => new RazorComponentResult()); app.MapGet("/direct/fortunes-ef", () => new RazorComponentResult()); +app.MapGet("/direct/fortunes/params", async (HttpContext context, Db db) => { + var fortunes = await db.LoadFortunesRowsDapper(); + //var fortunes = await db.LoadFortunesRowsNoDb(); // Don't call the database + var parameters = new Dictionary { { nameof(FortunesParameters.Rows), fortunes } }; + //var parameters = new FortunesRazorParameters(fortunes); // Custom parameters class to avoid allocating a Dictionary + var result = new RazorComponentResult(parameters) + { + PreventStreamingRendering = true + }; + return result; +}); + app.Lifetime.ApplicationStarted.Register(() => Console.WriteLine("Application started. Press Ctrl+C to shut down.")); app.Lifetime.ApplicationStopping.Register(() => Console.WriteLine("Application is shutting down...")); diff --git a/src/BenchmarksApps/TechEmpower/BlazorSSR/blazorssr.benchmarks.yml b/src/BenchmarksApps/TechEmpower/BlazorSSR/blazorssr.benchmarks.yml index 4950cc078..8189c81ac 100644 --- a/src/BenchmarksApps/TechEmpower/BlazorSSR/blazorssr.benchmarks.yml +++ b/src/BenchmarksApps/TechEmpower/BlazorSSR/blazorssr.benchmarks.yml @@ -51,6 +51,39 @@ scenarios: presetHeaders: html path: /fortunes-ef + fortunes-direct: + db: + job: postgresql + application: + job: blazorssr + load: + job: wrk + variables: + presetHeaders: html + path: /direct/fortunes + + fortunes-direct-ef: + db: + job: postgresql + application: + job: blazorssr + load: + job: wrk + variables: + presetHeaders: html + path: /direct/fortunes-ef + + fortunes-direct-params: + db: + job: postgresql + application: + job: blazorssr + load: + job: wrk + variables: + presetHeaders: html + path: /direct/fortunes/params + profiles: # this profile uses the local folder as the source # instead of the public repository diff --git a/src/BenchmarksApps/TechEmpower/Minimal/Program.cs b/src/BenchmarksApps/TechEmpower/Minimal/Program.cs index 94d0185ba..9fce631a8 100644 --- a/src/BenchmarksApps/TechEmpower/Minimal/Program.cs +++ b/src/BenchmarksApps/TechEmpower/Minimal/Program.cs @@ -22,7 +22,6 @@ // Add services to the container. builder.Services.AddSingleton(new Db(appSettings)); -builder.Services.AddRazorComponents(); builder.Services.AddSingleton(CreateHtmlEncoder()); var app = builder.Build(); @@ -47,18 +46,6 @@ return template; }); -app.MapGet("/fortunes/razor", async (HttpContext context, Db db) => { - var fortunes = await db.LoadFortunesRows(); - //var fortunes = await db.LoadFortunesRowsNoDb(); // Don't call the database - var parameters = new Dictionary { { nameof(FortunesRazor.Model), fortunes } }; - //var parameters = new FortunesRazorParameters(fortunes); // Custom parameters class to avoid allocating a Dictionary - var result = new RazorComponentResult(parameters) - { - PreventStreamingRendering = true - }; - return result; -}); - app.MapGet("/queries/{count}", async (Db db, int count) => await db.LoadMultipleQueriesRows(count)); app.MapGet("/queries/{count}/result", async (Db db, int count) => Results.Json(await db.LoadMultipleQueriesRows(count))); diff --git a/src/BenchmarksApps/TechEmpower/Minimal/Templates/FortunesRazor.razor b/src/BenchmarksApps/TechEmpower/Minimal/Templates/FortunesRazor.razor deleted file mode 100644 index 3acec2536..000000000 --- a/src/BenchmarksApps/TechEmpower/Minimal/Templates/FortunesRazor.razor +++ /dev/null @@ -1,5 +0,0 @@ -Fortunes@foreach (var item in Model){}
idmessage
@(new MarkupString(item.Id.ToString(CultureInfo.InvariantCulture)))@item.Message
-@code { - [Parameter] - public required List Model { get; set; } -} \ No newline at end of file diff --git a/src/BenchmarksApps/TechEmpower/Minimal/Templates/_Imports.razor b/src/BenchmarksApps/TechEmpower/Minimal/Templates/_Imports.razor deleted file mode 100644 index 00b58faaa..000000000 --- a/src/BenchmarksApps/TechEmpower/Minimal/Templates/_Imports.razor +++ /dev/null @@ -1,2 +0,0 @@ -@using System.Globalization; -@using Minimal.Models; \ No newline at end of file diff --git a/src/BenchmarksApps/TechEmpower/Minimal/minimal.benchmarks.yml b/src/BenchmarksApps/TechEmpower/Minimal/minimal.benchmarks.yml index 780aeb1e6..0b0472011 100644 --- a/src/BenchmarksApps/TechEmpower/Minimal/minimal.benchmarks.yml +++ b/src/BenchmarksApps/TechEmpower/Minimal/minimal.benchmarks.yml @@ -78,7 +78,7 @@ scenarios: presetHeaders: html path: /fortunes - fortunes_razor: + fortunes_result: db: job: postgresql application: @@ -87,7 +87,7 @@ scenarios: job: wrk variables: presetHeaders: html - path: /fortunes/razor + path: /fortunes/result single_query: db: