From 336adef2c2121ed6854efbe7b9a6f104349bc02a Mon Sep 17 00:00:00 2001 From: yv01p Date: Sat, 20 Dec 2025 17:18:33 +0000 Subject: [PATCH] test: add ContentServiceBenchmarkBase infrastructure class MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds base class for ContentService performance benchmarks with: - RecordBenchmark() for timing capture - MeasureAndRecord() with warmup support for non-destructive ops - MeasureAndRecord() with warmup for read-only ops returning values - JSON output wrapped in [BENCHMARK_JSON] markers for extraction - skipWarmup parameter for destructive operations 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../Testing/ContentServiceBenchmarkBase.cs | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 tests/Umbraco.Tests.Integration/Testing/ContentServiceBenchmarkBase.cs diff --git a/tests/Umbraco.Tests.Integration/Testing/ContentServiceBenchmarkBase.cs b/tests/Umbraco.Tests.Integration/Testing/ContentServiceBenchmarkBase.cs new file mode 100644 index 0000000000..6b76d9a3a1 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Testing/ContentServiceBenchmarkBase.cs @@ -0,0 +1,128 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System.Diagnostics; +using System.Text.Json; +using NUnit.Framework; + +namespace Umbraco.Cms.Tests.Integration.Testing; + +/// +/// Base class for ContentService performance benchmarks. +/// Extends UmbracoIntegrationTestWithContent with structured benchmark recording. +/// +/// +/// Usage: +/// +/// [Test] +/// [LongRunning] +/// public void MyBenchmark() +/// { +/// var sw = Stopwatch.StartNew(); +/// // ... operation under test ... +/// sw.Stop(); +/// RecordBenchmark("MyBenchmark", sw.ElapsedMilliseconds, itemCount); +/// } +/// +/// +/// Results are output in both human-readable and JSON formats for baseline comparison. +/// +public abstract class ContentServiceBenchmarkBase : UmbracoIntegrationTestWithContent +{ + private readonly List _results = new(); + + /// + /// Records a benchmark result for later output. + /// + /// Name of the benchmark (should match method name). + /// Elapsed time in milliseconds. + /// Number of items processed (for per-item metrics). + protected void RecordBenchmark(string name, long elapsedMs, int itemCount) + { + var result = new BenchmarkResult(name, elapsedMs, itemCount); + _results.Add(result); + + // Human-readable output + TestContext.WriteLine($"[BENCHMARK] {name}: {elapsedMs}ms ({result.MsPerItem:F2}ms/item, {itemCount} items)"); + } + + /// + /// Records a benchmark result without item count (for single-item operations). + /// + protected void RecordBenchmark(string name, long elapsedMs) + => RecordBenchmark(name, elapsedMs, 1); + + /// + /// Measures and records a benchmark for the given action. + /// + /// Name of the benchmark. + /// Number of items processed. + /// The action to benchmark. + /// Skip warmup for destructive operations (delete, empty recycle bin). + /// Elapsed time in milliseconds. + protected long MeasureAndRecord(string name, int itemCount, Action action, bool skipWarmup = false) + { + // Warmup iteration: triggers JIT compilation, warms connection pool and caches. + // Skip for destructive operations that would fail on second execution. + if (!skipWarmup) + { + try + { + action(); + } + catch + { + // Warmup failure is acceptable for some operations; continue to measured run + } + } + + var sw = Stopwatch.StartNew(); + action(); + sw.Stop(); + RecordBenchmark(name, sw.ElapsedMilliseconds, itemCount); + return sw.ElapsedMilliseconds; + } + + /// + /// Measures and records a benchmark, returning the result of the function. + /// + /// + /// Performs a warmup call before measurement to trigger JIT compilation. + /// Safe for read-only operations that can be repeated without side effects. + /// + protected T MeasureAndRecord(string name, int itemCount, Func func) + { + // Warmup: triggers JIT compilation, warms caches + try { func(); } catch { /* ignore warmup errors */ } + + var sw = Stopwatch.StartNew(); + var result = func(); + sw.Stop(); + RecordBenchmark(name, sw.ElapsedMilliseconds, itemCount); + return result; + } + + [TearDown] + public void OutputBenchmarkResults() + { + if (_results.Count == 0) + { + return; + } + + // JSON output for automated comparison + // Wrapped in markers for easy extraction from test output + var json = JsonSerializer.Serialize(_results, new JsonSerializerOptions { WriteIndented = true }); + TestContext.WriteLine($"[BENCHMARK_JSON]{json}[/BENCHMARK_JSON]"); + + _results.Clear(); + } + + /// + /// Represents a single benchmark measurement. + /// + internal sealed record BenchmarkResult(string Name, long ElapsedMs, int ItemCount) + { + public double MsPerItem => ItemCount > 0 ? (double)ElapsedMs / ItemCount : 0; + } +}