From 947f74c9ede1a4ce7a745a6be7120c7b591fec81 Mon Sep 17 00:00:00 2001 From: "Frank R. Haugen" Date: Mon, 4 Mar 2024 01:35:01 +0100 Subject: [PATCH] Work done on benchmarking --- .../SimpleTestLoggerProvider.cs | 4 + .../Frank.Testing.TestBases.csproj | 3 + .../HostApplicationBenchmarkBase.cs | 36 ++++++++ .../StringDelegateBenchmarkLogger.cs | 36 ++++++++ .../TestBases/HostApplicationBenchmarkBase.cs | 88 +++++++++++++++++++ ...HostApplicationTestBaseWithStartupTests.cs | 17 +++- 6 files changed, 182 insertions(+), 2 deletions(-) create mode 100644 Frank.Testing.TestBases/HostApplicationBenchmarkBase.cs create mode 100644 Frank.Testing.TestBases/StringDelegateBenchmarkLogger.cs create mode 100644 Frank.Testing.Tests/TestBases/HostApplicationBenchmarkBase.cs diff --git a/Frank.Testing.Logging/SimpleTestLoggerProvider.cs b/Frank.Testing.Logging/SimpleTestLoggerProvider.cs index df60baa..63c7dc3 100644 --- a/Frank.Testing.Logging/SimpleTestLoggerProvider.cs +++ b/Frank.Testing.Logging/SimpleTestLoggerProvider.cs @@ -11,6 +11,10 @@ public class SimpleTestLoggerProvider(ITestOutputHelper outputHelper, IOptions _loggers = new(); + public SimpleTestLoggerProvider(ITestOutputHelper outputHelper): this(outputHelper, Options.Create(new LoggerFilterOptions() { MinLevel = LogLevel.Information })) + { + } + /// public void Dispose() => _loggers.Clear(); diff --git a/Frank.Testing.TestBases/Frank.Testing.TestBases.csproj b/Frank.Testing.TestBases/Frank.Testing.TestBases.csproj index 699ad4d..8816459 100644 --- a/Frank.Testing.TestBases/Frank.Testing.TestBases.csproj +++ b/Frank.Testing.TestBases/Frank.Testing.TestBases.csproj @@ -7,6 +7,9 @@ + + + diff --git a/Frank.Testing.TestBases/HostApplicationBenchmarkBase.cs b/Frank.Testing.TestBases/HostApplicationBenchmarkBase.cs new file mode 100644 index 0000000..cf55118 --- /dev/null +++ b/Frank.Testing.TestBases/HostApplicationBenchmarkBase.cs @@ -0,0 +1,36 @@ +using System.Globalization; + +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Columns; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Exporters; +using BenchmarkDotNet.Exporters.Csv; +using BenchmarkDotNet.Exporters.Json; +using BenchmarkDotNet.Extensions; +using BenchmarkDotNet.Reports; +using BenchmarkDotNet.Running; + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +using Perfolizer.Horology; + +namespace Frank.Testing.TestBases; + +public abstract class HostApplicationBenchmarkBase : HostApplicationTestBase +{ + /// + protected HostApplicationBenchmarkBase(ILoggerProvider loggerProvider) : base(loggerProvider, LogLevel.Information) + { + } + + /// + /// Run a benchmark of the specified type and return the summary. + /// + /// + protected Summary RunBenchmarks(IConfig config) where T : class + { + return BenchmarkRunner.Run(config); + } +} \ No newline at end of file diff --git a/Frank.Testing.TestBases/StringDelegateBenchmarkLogger.cs b/Frank.Testing.TestBases/StringDelegateBenchmarkLogger.cs new file mode 100644 index 0000000..38df347 --- /dev/null +++ b/Frank.Testing.TestBases/StringDelegateBenchmarkLogger.cs @@ -0,0 +1,36 @@ +using System.Text; + +using BenchmarkDotNet.Loggers; + +namespace Frank.Testing.TestBases; + +public class StringDelegateBenchmarkLogger(Action output) : ILogger +{ + private readonly StringBuilder _stringBuilder = new(); + + /// + public void Write(LogKind logKind, string text) => _stringBuilder.Append(text); + + /// + public void WriteLine() + { + output(_stringBuilder.ToString()); + _stringBuilder.Clear(); + } + + /// + public void WriteLine(LogKind logKind, string text) => _stringBuilder.AppendLine(text); + + /// + public void Flush() + { + output(_stringBuilder.ToString()); + _stringBuilder.Clear(); + } + + /// + public string Id { get; set; } = string.Empty; + + /// + public int Priority { get; set; } = 0; +} \ No newline at end of file diff --git a/Frank.Testing.Tests/TestBases/HostApplicationBenchmarkBase.cs b/Frank.Testing.Tests/TestBases/HostApplicationBenchmarkBase.cs new file mode 100644 index 0000000..aefdd7a --- /dev/null +++ b/Frank.Testing.Tests/TestBases/HostApplicationBenchmarkBase.cs @@ -0,0 +1,88 @@ +using System.Diagnostics; +using System.Text; + +using BenchmarkDotNet.Analysers; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Characteristics; +using BenchmarkDotNet.Columns; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Exporters; +using BenchmarkDotNet.Exporters.Csv; +using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Loggers; + +using BenchmarkDotNetVisualizer; + +using Frank.Testing.Logging; +using Frank.Testing.TestBases; + +using Xunit.Abstractions; + +namespace Frank.Testing.Tests.TestBases; + +public class HostApplicationBenchmarkBaseTests : HostApplicationBenchmarkBase +{ + private readonly ITestOutputHelper _testOutputHelper; + + /// + public HostApplicationBenchmarkBaseTests(ITestOutputHelper testOutputHelper) : base(new SimpleTestLoggerProvider(testOutputHelper)) + { + _testOutputHelper = testOutputHelper; + } + + + [Fact] + public async Task Test1() + { + // Arrange + + // Act + var summary = RunBenchmarks(new DebugInProcessConfig() + .AddDiagnoser(MemoryDiagnoser.Default, new DisassemblyDiagnoser(new DisassemblyDiagnoserConfig()), ThreadingDiagnoser.Default) + .AddExporter(HtmlExporter.Default, CsvExporter.Default, MarkdownExporter.GitHub) + .AddAnalyser(OutliersAnalyser.Default, EnvironmentAnalyser.Default, MultimodalDistributionAnalyzer.Default, ZeroMeasurementAnalyser.Default) + .AddHardwareCounters(HardwareCounter.BranchMispredictions, HardwareCounter.BranchInstructions, HardwareCounter.CacheMisses, HardwareCounter.TotalCycles, HardwareCounter.Timer) + .AddLogger(new StringDelegateBenchmarkLogger(_testOutputHelper.WriteLine)) + ); + + // Assert + // var html = summary.GetMarkdown(new ReportMarkdownOptions() + // { + // Title = "TestBenchmark" + // }); + // + // _testOutputHelper.WriteLine(html); + } + + [HardwareCounters( + HardwareCounter.BranchMispredictions, + HardwareCounter.BranchInstructions)] + public class TestBenchmark + { + [Benchmark] + public void Run() + { + var result = 1f + 1f + 1f; + } + } +} + +public class XUnitBenchmarkConfiguration : ManualConfig +{ + public XUnitBenchmarkConfiguration(ILogger? benchmarkLogger = null) + { + if (benchmarkLogger != null) + AddLogger(benchmarkLogger); + + AddJob(Job.ShortRun); + AddExporter(HtmlExporter.Default, new RichMarkdownExporter(new ReportMarkdownOptions() + { + Title = "Benchmark" + })); + AddDiagnoser(MemoryDiagnoser.Default, new DisassemblyDiagnoser(new DisassemblyDiagnoserConfig()), ThreadingDiagnoser.Default); + WithOptions(ConfigOptions.DisableOptimizationsValidator); + } + + +} diff --git a/Frank.Testing.Tests/TestBases/WebHostApplicationTestBaseWithStartupTests.cs b/Frank.Testing.Tests/TestBases/WebHostApplicationTestBaseWithStartupTests.cs index 98d6bf2..6eb3e82 100644 --- a/Frank.Testing.Tests/TestBases/WebHostApplicationTestBaseWithStartupTests.cs +++ b/Frank.Testing.Tests/TestBases/WebHostApplicationTestBaseWithStartupTests.cs @@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -29,9 +30,19 @@ protected override Task SetupApplicationAsync(WebApplication application) { application.UseRouting(); application.MapControllers(); - application.MapGet("/test1", async httpContext => + application.MapGet("/test1/{id}/test2/{id2}", + async httpContext => + { + await httpContext.Response.WriteAsync("Test1 endpoint"); + }); + application.MapDelete("/test1/{id}/test2/{id2}", + async (HttpContext httpContext, string id, string id2) => + { + await httpContext.Response.WriteAsync("Test1 endpoint"); + }); + application.MapGet("/test2", async httpContext => { - await httpContext.Response.WriteAsync("Test1 endpoint"); + await httpContext.Response.WriteAsync("Test2 endpoint"); }); return Task.CompletedTask; } @@ -46,6 +57,8 @@ public async Task Test() var myServiceLogger = GetServices.GetRequiredService>(); var inMemoryLogger = myServiceLogger as InMemoryLogger; inMemoryLogger?.GetLogEntries().Should().Contain(log => log.Message == "DoSomething"); + + outputHelper.WriteTable(GetEndpoints.Cast().Select(route => new {Name = route.DisplayName, Method = route.Metadata.GetMetadata()!.HttpMethods.FirstOrDefault(), route.RoutePattern.RawText})); } }