// Copyright (c) Umbraco. // See LICENSE for more details. using System.Diagnostics; using NUnit.Framework; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Tests.Common.Attributes; using Umbraco.Cms.Tests.Common.Builders; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.Testing; namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services; /// /// Performance benchmarks for ContentService operations. /// Used to establish baseline metrics and detect regressions during refactoring. /// /// /// Run with: dotnet test --filter "Category=Benchmark&FullyQualifiedName~ContentServiceRefactoringBenchmarks" /// Results are output in JSON format for baseline comparison. /// /// v1.3 EXPECTED RANGES: /// After capturing the Phase 0 baseline, document expected ranges in docs/plans/baseline-phase0.json. /// Future phases may add threshold-based assertions. For now, benchmarks capture timing only. /// Example expected range comment (to be added after baseline capture): /// // Baseline: ~50ms, Acceptable range: <100ms /// /// v1.2 WARMUP PATTERN: /// - For non-destructive benchmarks that MUTATE data (Save, Publish, etc.): /// Create throwaway data for warmup (triggers JIT, warms caches), then /// create fresh identical data for the measured run. /// - For read-only benchmarks (GetById, GetByIds, etc.): /// Use MeasureAndRecord with warmup enabled (default). /// - For destructive benchmarks (Delete, EmptyRecycleBin, etc.): /// Use skipWarmup: true since operation cannot be repeated. /// [TestFixture] [NonParallelizable] // Required: benchmarks need isolated database access for accurate measurements [UmbracoTest( Database = UmbracoTestOptions.Database.NewSchemaPerTest, PublishedRepositoryEvents = true, WithApplication = true, Logger = UmbracoTestOptions.Logger.Console)] [Category("Benchmark")] [Category("LongRunning")] internal sealed class ContentServiceRefactoringBenchmarks : ContentServiceBenchmarkBase { #region CRUD Operation Benchmarks (7 tests) /// /// Benchmark 1: Single content save latency. /// [Test] [LongRunning] public void Benchmark_Save_SingleItem() { // v1.2: Warmup with throwaway content (triggers JIT, warms caches) var warmupContent = ContentBuilder.CreateSimpleContent(ContentType, "Warmup_SingleItem", -1); ContentService.Save(warmupContent); // Measured run with fresh, identical setup var content = ContentBuilder.CreateSimpleContent(ContentType, "BenchmarkSingle", -1); var sw = Stopwatch.StartNew(); ContentService.Save(content); sw.Stop(); // Gate: Fail if >20% regression from baseline AssertNoRegression("Save_SingleItem", sw.ElapsedMilliseconds, 1); Assert.That(content.Id, Is.GreaterThan(0)); } /// /// Benchmark 2: Batch save of 100 items. /// [Test] [LongRunning] public void Benchmark_Save_BatchOf100() { const int itemCount = 100; // v1.2: Warmup with throwaway batch var warmupItems = new List(); for (var i = 0; i < itemCount; i++) { warmupItems.Add(ContentBuilder.CreateSimpleContent(ContentType, $"Warmup_Batch100_{i}", -1)); } ContentService.Save(warmupItems); // Measured run with fresh batch var items = new List(); for (var i = 0; i < itemCount; i++) { items.Add(ContentBuilder.CreateSimpleContent(ContentType, $"Batch100_{i}", -1)); } var sw = Stopwatch.StartNew(); ContentService.Save(items); sw.Stop(); // Gate: Fail if >20% regression from baseline AssertNoRegression("Save_BatchOf100", sw.ElapsedMilliseconds, itemCount); Assert.That(items.All(c => c.Id > 0), Is.True); } /// /// Benchmark 3: Batch save of 1000 items (scalability test). /// [Test] [LongRunning] public void Benchmark_Save_BatchOf1000() { const int itemCount = 1000; // v1.2: Warmup with smaller batch (100 items sufficient for JIT warmup) var warmupItems = new List(); for (var i = 0; i < 100; i++) { warmupItems.Add(ContentBuilder.CreateSimpleContent(ContentType, $"Warmup_Batch1000_{i}", -1)); } ContentService.Save(warmupItems); // Measured run with fresh batch var items = new List(); for (var i = 0; i < itemCount; i++) { items.Add(ContentBuilder.CreateSimpleContent(ContentType, $"Batch1000_{i}", -1)); } var sw = Stopwatch.StartNew(); ContentService.Save(items); sw.Stop(); // Gate: Fail if >20% regression from baseline AssertNoRegression("Save_BatchOf1000", sw.ElapsedMilliseconds, itemCount); Assert.That(items.All(c => c.Id > 0), Is.True); } /// /// Benchmark 4: Single item retrieval by ID. /// /// v1.2: Read-only operation - MeasureAndRecord warmup is safe. [Test] [LongRunning] public void Benchmark_GetById_Single() { var content = ContentBuilder.CreateSimpleContent(ContentType, "GetByIdTest", -1); ContentService.Save(content); var id = content.Id; IContent? result = null; MeasureAndAssertNoRegression("GetById_Single", 1, () => { result = ContentService.GetById(id); }); Assert.That(result, Is.Not.Null); } /// /// Benchmark 5: Batch retrieval of 100 items (N+1 detection). /// /// v1.2: Read-only operation - MeasureAndRecord warmup is safe. [Test] [LongRunning] public void Benchmark_GetByIds_BatchOf100() { const int itemCount = 100; var items = new List(); for (var i = 0; i < itemCount; i++) { var content = ContentBuilder.CreateSimpleContent(ContentType, $"GetByIds_{i}", -1); ContentService.Save(content); items.Add(content); } var ids = items.Select(c => c.Id).ToList(); IEnumerable? results = null; MeasureAndAssertNoRegression("GetByIds_BatchOf100", itemCount, () => { results = ContentService.GetByIds(ids); }); Assert.That(results!.Count(), Is.EqualTo(itemCount)); } /// /// Benchmark 6: Single item deletion. /// [Test] [LongRunning] public void Benchmark_Delete_SingleItem() { var content = ContentBuilder.CreateSimpleContent(ContentType, "DeleteTest", -1); ContentService.Save(content); MeasureAndAssertNoRegression("Delete_SingleItem", 1, () => { ContentService.Delete(content); }, skipWarmup: true); // Destructive operation - cannot repeat Assert.That(ContentService.GetById(content.Id), Is.Null); } /// /// Benchmark 7: Delete item with 100 descendants (cascade performance). /// /// v1.2: Standardized from 50 to 100 descendants. [Test] [LongRunning] public void Benchmark_Delete_WithDescendants() { var parent = ContentBuilder.CreateSimpleContent(ContentType, "DeleteParent", -1); ContentService.Save(parent); const int childCount = 100; // v1.2: Standardized to 100 for (var i = 0; i < childCount; i++) { var child = ContentBuilder.CreateSimpleContent(ContentType, $"DeleteChild_{i}", parent.Id); ContentService.Save(child); } MeasureAndAssertNoRegression("Delete_WithDescendants", childCount + 1, () => { ContentService.Delete(parent); }, skipWarmup: true); // Destructive operation - cannot repeat Assert.That(ContentService.GetById(parent.Id), Is.Null); } #endregion #region Query Operation Benchmarks (6 tests) /// /// Benchmark 8: Paged children query (100 items). /// /// v1.2: Read-only operation - MeasureAndRecord warmup is safe. [Test] [LongRunning] public void Benchmark_GetPagedChildren_100Items() { var parent = ContentBuilder.CreateSimpleContent(ContentType, "PagedParent", -1); ContentService.Save(parent); const int childCount = 100; for (var i = 0; i < childCount; i++) { var child = ContentBuilder.CreateSimpleContent(ContentType, $"PagedChild_{i}", parent.Id); ContentService.Save(child); } IEnumerable? results = null; long totalRecords = 0; MeasureAndAssertNoRegression("GetPagedChildren_100Items", childCount, () => { results = ContentService.GetPagedChildren(parent.Id, 0, 100, out totalRecords); }); Assert.That(totalRecords, Is.EqualTo(childCount)); } /// /// Benchmark 9: Paged descendants query (deep tree, 300 total). /// /// v1.2: Read-only operation - MeasureAndRecord warmup is safe. [Test] [LongRunning] public void Benchmark_GetPagedDescendants_DeepTree() { var root = ContentBuilder.CreateSimpleContent(ContentType, "DeepRoot", -1); ContentService.Save(root); // Create 3 levels, 100 items each const int itemsPerLevel = 100; var currentParent = root; for (var level = 0; level < 3; level++) { var firstChild = ContentBuilder.CreateSimpleContent(ContentType, $"Level{level}_0", currentParent.Id); ContentService.Save(firstChild); for (var i = 1; i < itemsPerLevel; i++) { var sibling = ContentBuilder.CreateSimpleContent(ContentType, $"Level{level}_{i}", currentParent.Id); ContentService.Save(sibling); } currentParent = firstChild; } IEnumerable? results = null; long totalRecords = 0; MeasureAndAssertNoRegression("GetPagedDescendants_DeepTree", 300, () => { results = ContentService.GetPagedDescendants(root.Id, 0, 1000, out totalRecords); }); Assert.That(totalRecords, Is.EqualTo(300)); } /// /// Benchmark 10: Get ancestors of deep item (N+1 prone). /// /// v1.2: Read-only operation - MeasureAndRecord warmup is safe. [Test] [LongRunning] public void Benchmark_GetAncestors_DeepHierarchy() { const int depth = 10; var parent = ContentBuilder.CreateSimpleContent(ContentType, "AncestorRoot", -1); ContentService.Save(parent); var current = parent; for (var i = 0; i < depth - 1; i++) { var child = ContentBuilder.CreateSimpleContent(ContentType, $"Ancestor_{i}", current.Id); ContentService.Save(child); current = child; } var deepestId = current.Id; IEnumerable? results = null; MeasureAndAssertNoRegression("GetAncestors_DeepHierarchy", depth, () => { results = ContentService.GetAncestors(deepestId); }); Assert.That(results!.Count(), Is.EqualTo(depth - 1)); // Excludes self } /// /// Benchmark 11: Count by content type (1000 items). /// /// v1.2: Read-only operation - MeasureAndRecord warmup is safe. Standardized from 500 to 1000. [Test] [LongRunning] public void Benchmark_Count_ByContentType() { const int itemCount = 1000; // v1.2: Standardized to 1000 for (var i = 0; i < itemCount; i++) { var content = ContentBuilder.CreateSimpleContent(ContentType, $"Count_{i}", -1); ContentService.Save(content); } int count = 0; MeasureAndRecord("Count_ByContentType", itemCount, () => { count = ContentService.Count(ContentType.Alias); }); // Note: base class may create some content too Assert.That(count, Is.GreaterThanOrEqualTo(itemCount)); } /// /// Benchmark 12: Count descendants (1000 descendants). /// /// v1.2: Read-only operation - MeasureAndRecord warmup is safe. Standardized from 500 to 1000. [Test] [LongRunning] public void Benchmark_CountDescendants_LargeTree() { var parent = ContentBuilder.CreateSimpleContent(ContentType, "CountDescParent", -1); ContentService.Save(parent); const int childCount = 1000; // v1.2: Standardized to 1000 for (var i = 0; i < childCount; i++) { var child = ContentBuilder.CreateSimpleContent(ContentType, $"CountDescChild_{i}", parent.Id); ContentService.Save(child); } int count = 0; MeasureAndRecord("CountDescendants_LargeTree", childCount, () => { count = ContentService.CountDescendants(parent.Id); }); Assert.That(count, Is.EqualTo(childCount)); } /// /// Benchmark 13: HasChildren called 100 times (repeated single lookups). /// /// v1.2: Read-only operation - MeasureAndRecord warmup is safe. [Test] [LongRunning] public void Benchmark_HasChildren_100Nodes() { const int nodeCount = 100; var nodes = new List(); // Create nodes, half with children, half without for (var i = 0; i < nodeCount; i++) { var node = ContentBuilder.CreateSimpleContent(ContentType, $"HasChildren_{i}", -1); ContentService.Save(node); nodes.Add(node); if (i % 2 == 0) { var child = ContentBuilder.CreateSimpleContent(ContentType, $"Child_{i}", node.Id); ContentService.Save(child); } } int trueCount = 0; MeasureAndRecord("HasChildren_100Nodes", nodeCount, () => { // Reset counter to avoid double-counting from warmup run trueCount = 0; foreach (var node in nodes) { if (ContentService.HasChildren(node.Id)) { trueCount++; } } }); Assert.That(trueCount, Is.EqualTo(50)); } #endregion #region Publish Operation Benchmarks (7 tests) /// /// Benchmark 14: Single item publish. /// [Test] [LongRunning] public void Benchmark_Publish_SingleItem() { // v1.2: Warmup with throwaway content var warmupContent = ContentBuilder.CreateSimpleContent(ContentType, "Warmup_PublishSingle", -1); ContentService.Save(warmupContent); ContentService.Publish(warmupContent, new[] { "*" }); // Measured run with fresh content var content = ContentBuilder.CreateSimpleContent(ContentType, "PublishSingle", -1); ContentService.Save(content); var sw = Stopwatch.StartNew(); ContentService.Publish(content, new[] { "*" }); sw.Stop(); RecordBenchmark("Publish_SingleItem", sw.ElapsedMilliseconds, 1); Assert.That(content.Published, Is.True); } /// /// Benchmark 15: Publish 100 items sequentially. /// /// v1.2: Renamed from BatchOf50 to BatchOf100 for standardization. [Test] [LongRunning] public void Benchmark_Publish_BatchOf100() { const int itemCount = 100; // v1.2: Standardized to 100 // v1.2: Warmup with throwaway batch var warmupItems = new List(); for (var i = 0; i < 10; i++) // Small warmup batch for JIT { var warmup = ContentBuilder.CreateSimpleContent(ContentType, $"Warmup_PublishBatch_{i}", -1); ContentService.Save(warmup); warmupItems.Add(warmup); } foreach (var warmup in warmupItems) { ContentService.Publish(warmup, new[] { "*" }); } // Measured run with fresh batch var items = new List(); for (var i = 0; i < itemCount; i++) { var content = ContentBuilder.CreateSimpleContent(ContentType, $"PublishBatch_{i}", -1); ContentService.Save(content); items.Add(content); } var sw = Stopwatch.StartNew(); foreach (var content in items) { ContentService.Publish(content, new[] { "*" }); } sw.Stop(); RecordBenchmark("Publish_BatchOf100", sw.ElapsedMilliseconds, itemCount); Assert.That(items.All(c => c.Published), Is.True); } /// /// Benchmark 16: PublishBranch with shallow tree (1 parent + 100 children). /// /// v1.2: Standardized from 20 to 100 children. [Test] [LongRunning] public void Benchmark_PublishBranch_ShallowTree() { // v1.2: Warmup with throwaway tree var warmupParent = ContentBuilder.CreateSimpleContent(ContentType, "Warmup_BranchParent", -1); ContentService.Save(warmupParent); for (var i = 0; i < 10; i++) // Small warmup tree { var warmupChild = ContentBuilder.CreateSimpleContent(ContentType, $"Warmup_BranchChild_{i}", warmupParent.Id); ContentService.Save(warmupChild); } ContentService.PublishBranch(warmupParent, PublishBranchFilter.Default, new[] { "*" }); // Measured run with fresh tree var parent = ContentBuilder.CreateSimpleContent(ContentType, "BranchParent", -1); ContentService.Save(parent); const int childCount = 100; // v1.2: Standardized to 100 for (var i = 0; i < childCount; i++) { var child = ContentBuilder.CreateSimpleContent(ContentType, $"BranchChild_{i}", parent.Id); ContentService.Save(child); } var sw = Stopwatch.StartNew(); ContentService.PublishBranch(parent, PublishBranchFilter.Default, new[] { "*" }); sw.Stop(); RecordBenchmark("PublishBranch_ShallowTree", sw.ElapsedMilliseconds, childCount + 1); Assert.That(parent.Published, Is.True); } /// /// Benchmark 17: PublishBranch with deep tree (5 levels, 100 items total). /// [Test] [LongRunning] public void Benchmark_PublishBranch_DeepTree() { // v1.2: Warmup with small deep tree (3 levels, 3 items each) var warmupRoot = ContentBuilder.CreateSimpleContent(ContentType, "Warmup_DeepBranchRoot", -1); ContentService.Save(warmupRoot); var warmupParent = warmupRoot; for (var level = 0; level < 3; level++) { var warmupChild = ContentBuilder.CreateSimpleContent(ContentType, $"Warmup_DeepLevel{level}_0", warmupParent.Id); ContentService.Save(warmupChild); warmupParent = warmupChild; } ContentService.PublishBranch(warmupRoot, PublishBranchFilter.Default, new[] { "*" }); // Measured run with fresh deep tree var root = ContentBuilder.CreateSimpleContent(ContentType, "DeepBranchRoot", -1); ContentService.Save(root); // Create 5 levels, 20 items each level var currentParent = root; for (var level = 0; level < 5; level++) { var firstChild = ContentBuilder.CreateSimpleContent(ContentType, $"DeepLevel{level}_0", currentParent.Id); ContentService.Save(firstChild); for (var i = 1; i < 20; i++) { var sibling = ContentBuilder.CreateSimpleContent(ContentType, $"DeepLevel{level}_{i}", currentParent.Id); ContentService.Save(sibling); } currentParent = firstChild; } var sw = Stopwatch.StartNew(); ContentService.PublishBranch(root, PublishBranchFilter.Default, new[] { "*" }); sw.Stop(); RecordBenchmark("PublishBranch_DeepTree", sw.ElapsedMilliseconds, 101); Assert.That(root.Published, Is.True); } /// /// Benchmark 18: Single item unpublish. /// [Test] [LongRunning] public void Benchmark_Unpublish_SingleItem() { // v1.2: Warmup with throwaway content var warmupContent = ContentBuilder.CreateSimpleContent(ContentType, "Warmup_UnpublishTest", -1); ContentService.Save(warmupContent); ContentService.Publish(warmupContent, new[] { "*" }); ContentService.Unpublish(warmupContent); // Measured run with fresh content var content = ContentBuilder.CreateSimpleContent(ContentType, "UnpublishTest", -1); ContentService.Save(content); ContentService.Publish(content, new[] { "*" }); var sw = Stopwatch.StartNew(); ContentService.Unpublish(content); sw.Stop(); RecordBenchmark("Unpublish_SingleItem", sw.ElapsedMilliseconds, 1); Assert.That(content.Published, Is.False); } /// /// Benchmark 19: PerformScheduledPublish with 100 scheduled items. /// /// v1.2: Standardized from 50 to 100 items. [Test] [LongRunning] public void Benchmark_PerformScheduledPublish() { const int itemCount = 100; // v1.2: Standardized to 100 // v1.2: Warmup with small set of scheduled items for (var i = 0; i < 10; i++) { var warmup = ContentBuilder.CreateSimpleContent(ContentType, $"Warmup_Scheduled_{i}", -1); var warmupSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.UtcNow.AddMinutes(-10), null); ContentService.Save(warmup, -1, warmupSchedule); } ContentService.PerformScheduledPublish(DateTime.UtcNow.AddMinutes(-9)); // Create measured items with schedules in the past (ready for publish) for (var i = 0; i < itemCount; i++) { var content = ContentBuilder.CreateSimpleContent(ContentType, $"Scheduled_{i}", -1); var schedule = ContentScheduleCollection.CreateWithEntry(DateTime.UtcNow.AddMinutes(-5), null); ContentService.Save(content, -1, schedule); } var sw = Stopwatch.StartNew(); ContentService.PerformScheduledPublish(DateTime.UtcNow); sw.Stop(); RecordBenchmark("PerformScheduledPublish", sw.ElapsedMilliseconds, itemCount); } /// /// Benchmark 20: GetContentSchedulesByIds for 100 items (N+1 hotspot). /// /// v1.2: Read-only operation - MeasureAndRecord warmup is safe. [Test] [LongRunning] public void Benchmark_GetContentSchedulesByIds_100Items() { const int itemCount = 100; var keys = new List(); for (var i = 0; i < itemCount; i++) { var content = ContentBuilder.CreateSimpleContent(ContentType, $"Schedule_{i}", -1); var schedule = ContentScheduleCollection.CreateWithEntry(DateTime.UtcNow.AddDays(1), null); ContentService.Save(content, -1, schedule); keys.Add(content.Key); } IDictionary>? results = null; MeasureAndRecord("GetContentSchedulesByIds_100Items", itemCount, () => { results = ContentService.GetContentSchedulesByIds(keys.ToArray()); }); Assert.That(results, Is.Not.Null); } #endregion #region Move Operation Benchmarks (8 tests) /// /// Benchmark 21: Single item move. /// [Test] [LongRunning] public void Benchmark_Move_SingleItem() { // v1.2: Warmup with throwaway content var warmupContent = ContentBuilder.CreateSimpleContent(ContentType, "Warmup_MoveTest", -1); ContentService.Save(warmupContent); var warmupTarget = ContentBuilder.CreateSimpleContent(ContentType, "Warmup_MoveTarget", -1); ContentService.Save(warmupTarget); ContentService.Move(warmupContent, warmupTarget.Id); // Measured run with fresh content var content = ContentBuilder.CreateSimpleContent(ContentType, "MoveTest", -1); ContentService.Save(content); var newParent = ContentBuilder.CreateSimpleContent(ContentType, "MoveTarget", -1); ContentService.Save(newParent); var sw = Stopwatch.StartNew(); ContentService.Move(content, newParent.Id); sw.Stop(); RecordBenchmark("Move_SingleItem", sw.ElapsedMilliseconds, 1); Assert.That(content.ParentId, Is.EqualTo(newParent.Id)); } /// /// Benchmark 22: Move item with 100 descendants. /// /// v1.2: Standardized from 50 to 100 descendants. [Test] [LongRunning] public void Benchmark_Move_WithDescendants() { // v1.2: Warmup with small tree var warmupParent = ContentBuilder.CreateSimpleContent(ContentType, "Warmup_MoveParent", -1); ContentService.Save(warmupParent); for (var i = 0; i < 10; i++) { var warmupChild = ContentBuilder.CreateSimpleContent(ContentType, $"Warmup_MoveChild_{i}", warmupParent.Id); ContentService.Save(warmupChild); } var warmupNewParent = ContentBuilder.CreateSimpleContent(ContentType, "Warmup_MoveDescTarget", -1); ContentService.Save(warmupNewParent); ContentService.Move(warmupParent, warmupNewParent.Id); // Measured run with fresh tree var parent = ContentBuilder.CreateSimpleContent(ContentType, "MoveParent", -1); ContentService.Save(parent); const int childCount = 100; // v1.2: Standardized to 100 for (var i = 0; i < childCount; i++) { var child = ContentBuilder.CreateSimpleContent(ContentType, $"MoveChild_{i}", parent.Id); ContentService.Save(child); } var newParent = ContentBuilder.CreateSimpleContent(ContentType, "MoveDescTarget", -1); ContentService.Save(newParent); var sw = Stopwatch.StartNew(); ContentService.Move(parent, newParent.Id); sw.Stop(); RecordBenchmark("Move_WithDescendants", sw.ElapsedMilliseconds, childCount + 1); Assert.That(parent.ParentId, Is.EqualTo(newParent.Id)); } /// /// Benchmark 23: MoveToRecycleBin for published item. /// [Test] [LongRunning] public void Benchmark_MoveToRecycleBin_Published() { // v1.2: Warmup with throwaway content var warmupContent = ContentBuilder.CreateSimpleContent(ContentType, "Warmup_RecycleBinPublished", -1); ContentService.Save(warmupContent); ContentService.Publish(warmupContent, new[] { "*" }); ContentService.MoveToRecycleBin(warmupContent); // Measured run with fresh content var content = ContentBuilder.CreateSimpleContent(ContentType, "RecycleBinPublished", -1); ContentService.Save(content); ContentService.Publish(content, new[] { "*" }); var sw = Stopwatch.StartNew(); ContentService.MoveToRecycleBin(content); sw.Stop(); RecordBenchmark("MoveToRecycleBin_Published", sw.ElapsedMilliseconds, 1); Assert.That(content.Trashed, Is.True); } /// /// Benchmark 24: MoveToRecycleBin for large tree (1000 descendants). /// /// v1.2: Standardized from 100 to 1000 descendants. [Test] [LongRunning] public void Benchmark_MoveToRecycleBin_LargeTree() { // v1.2: Warmup with small tree var warmupParent = ContentBuilder.CreateSimpleContent(ContentType, "Warmup_RecycleLargeParent", -1); ContentService.Save(warmupParent); for (var i = 0; i < 10; i++) { var warmupChild = ContentBuilder.CreateSimpleContent(ContentType, $"Warmup_RecycleLargeChild_{i}", warmupParent.Id); ContentService.Save(warmupChild); } ContentService.MoveToRecycleBin(warmupParent); // Measured run with fresh tree var parent = ContentBuilder.CreateSimpleContent(ContentType, "RecycleLargeParent", -1); ContentService.Save(parent); const int childCount = 1000; // v1.2: Standardized to 1000 for (var i = 0; i < childCount; i++) { var child = ContentBuilder.CreateSimpleContent(ContentType, $"RecycleLargeChild_{i}", parent.Id); ContentService.Save(child); } var sw = Stopwatch.StartNew(); ContentService.MoveToRecycleBin(parent); sw.Stop(); RecordBenchmark("MoveToRecycleBin_LargeTree", sw.ElapsedMilliseconds, childCount + 1); Assert.That(parent.Trashed, Is.True); } /// /// Benchmark 25: Single item copy. /// [Test] [LongRunning] public void Benchmark_Copy_SingleItem() { // v1.2: Warmup with throwaway content var warmupContent = ContentBuilder.CreateSimpleContent(ContentType, "Warmup_CopyTest", -1); ContentService.Save(warmupContent); var warmupTarget = ContentBuilder.CreateSimpleContent(ContentType, "Warmup_CopyTarget", -1); ContentService.Save(warmupTarget); ContentService.Copy(warmupContent, warmupTarget.Id, false); // Measured run with fresh content var content = ContentBuilder.CreateSimpleContent(ContentType, "CopyTest", -1); ContentService.Save(content); var copyTarget = ContentBuilder.CreateSimpleContent(ContentType, "CopyTarget", -1); ContentService.Save(copyTarget); IContent? copy = null; var sw = Stopwatch.StartNew(); copy = ContentService.Copy(content, copyTarget.Id, false); sw.Stop(); RecordBenchmark("Copy_SingleItem", sw.ElapsedMilliseconds, 1); Assert.That(copy, Is.Not.Null); } /// /// Benchmark 26: Recursive copy of 100 items. /// /// v1.2: Renamed from 50Items to 100Items for standardization. [Test] [LongRunning] public void Benchmark_Copy_Recursive_100Items() { // v1.2: Warmup with small tree var warmupParent = ContentBuilder.CreateSimpleContent(ContentType, "Warmup_CopyRecParent", -1); ContentService.Save(warmupParent); for (var i = 0; i < 10; i++) { var warmupChild = ContentBuilder.CreateSimpleContent(ContentType, $"Warmup_CopyRecChild_{i}", warmupParent.Id); ContentService.Save(warmupChild); } var warmupCopyTarget = ContentBuilder.CreateSimpleContent(ContentType, "Warmup_CopyRecTarget", -1); ContentService.Save(warmupCopyTarget); ContentService.Copy(warmupParent, warmupCopyTarget.Id, false, true); // Measured run with fresh tree var parent = ContentBuilder.CreateSimpleContent(ContentType, "CopyRecParent", -1); ContentService.Save(parent); const int childCount = 100; // v1.2: Standardized to 100 for (var i = 0; i < childCount; i++) { var child = ContentBuilder.CreateSimpleContent(ContentType, $"CopyRecChild_{i}", parent.Id); ContentService.Save(child); } var copyTarget = ContentBuilder.CreateSimpleContent(ContentType, "CopyRecTarget", -1); ContentService.Save(copyTarget); IContent? copy = null; var sw = Stopwatch.StartNew(); copy = ContentService.Copy(parent, copyTarget.Id, false, true); sw.Stop(); RecordBenchmark("Copy_Recursive_100Items", sw.ElapsedMilliseconds, childCount + 1); Assert.That(copy, Is.Not.Null); } /// /// Benchmark 27: Sort 100 children. /// [Test] [LongRunning] public void Benchmark_Sort_100Children() { // v1.2: Warmup with small tree var warmupParent = ContentBuilder.CreateSimpleContent(ContentType, "Warmup_SortParent", -1); ContentService.Save(warmupParent); var warmupChildren = new List(); for (var i = 0; i < 10; i++) { var warmupChild = ContentBuilder.CreateSimpleContent(ContentType, $"Warmup_SortChild_{i}", warmupParent.Id); ContentService.Save(warmupChild); warmupChildren.Add(warmupChild); } warmupChildren.Reverse(); ContentService.Sort(warmupChildren); // Measured run with fresh tree var parent = ContentBuilder.CreateSimpleContent(ContentType, "SortParent", -1); ContentService.Save(parent); const int childCount = 100; var children = new List(); for (var i = 0; i < childCount; i++) { var child = ContentBuilder.CreateSimpleContent(ContentType, $"SortChild_{i}", parent.Id); ContentService.Save(child); children.Add(child); } // Reverse the order children.Reverse(); var sw = Stopwatch.StartNew(); ContentService.Sort(children); sw.Stop(); RecordBenchmark("Sort_100Children", sw.ElapsedMilliseconds, childCount); } /// /// Benchmark 28: EmptyRecycleBin with 100 items. /// [Test] [LongRunning] public void Benchmark_EmptyRecycleBin_100Items() { const int itemCount = 100; for (var i = 0; i < itemCount; i++) { var content = ContentBuilder.CreateSimpleContent(ContentType, $"Trash_{i}", -1); ContentService.Save(content); ContentService.MoveToRecycleBin(content); } MeasureAndRecord("EmptyRecycleBin_100Items", itemCount, () => { ContentService.EmptyRecycleBin(); }, skipWarmup: true); // Destructive operation - cannot repeat Assert.That(ContentService.RecycleBinSmells(), Is.False); } #endregion #region Version Operation Benchmarks (4 tests) /// /// Benchmark 29: GetVersions for item with 100 versions. /// /// /// v1.2: Renamed from 50Versions to 100Versions for standardization. Read-only operation. /// v1.3: Fixed to create actual versions by publishing. Save() updates existing version; /// Publish() creates a new version. /// [Test] [LongRunning] public void Benchmark_GetVersions_ItemWith100Versions() { var content = ContentBuilder.CreateSimpleContent(ContentType, "VersionTest", -1); ContentService.Save(content); ContentService.Publish(content, new[] { "*" }); // Initial publish creates version 1 // Create 100 versions by publishing repeatedly // Note: Save() updates existing version; Publish() creates new version const int versionCount = 100; // v1.2: Standardized to 100 for (var i = 0; i < versionCount; i++) { content.Name = $"VersionTest_v{i}"; ContentService.Save(content); ContentService.Publish(content, new[] { "*" }); // Each publish creates a new version } IEnumerable? versions = null; MeasureAndRecord("GetVersions_ItemWith100Versions", versionCount, () => { versions = ContentService.GetVersions(content.Id); }); // versionCount publishes + 1 initial publish = versionCount + 1 versions Assert.That(versions!.Count(), Is.GreaterThanOrEqualTo(versionCount)); } /// /// Benchmark 30: GetVersionsSlim with paging (100 versions, page of 10). /// /// /// v1.2: Standardized from 50 to 100 versions. Read-only operation. /// v1.3: Fixed to create actual versions by publishing. Save() updates existing version; /// Publish() creates a new version. /// [Test] [LongRunning] public void Benchmark_GetVersionsSlim_Paged() { var content = ContentBuilder.CreateSimpleContent(ContentType, "SlimVersionTest", -1); ContentService.Save(content); ContentService.Publish(content, new[] { "*" }); // Initial publish creates version 1 // Create 100 versions by publishing repeatedly // Note: Save() updates existing version; Publish() creates new version const int versionCount = 100; // v1.2: Standardized to 100 for (var i = 0; i < versionCount; i++) { content.Name = $"SlimVersionTest_v{i}"; ContentService.Save(content); ContentService.Publish(content, new[] { "*" }); // Each publish creates a new version } IEnumerable? versions = null; MeasureAndRecord("GetVersionsSlim_Paged", 10, () => { versions = ContentService.GetVersionsSlim(content.Id, 0, 10); }); Assert.That(versions!.Count(), Is.EqualTo(10)); } /// /// Benchmark 31: Rollback to previous version. /// /// /// v1.3: Fixed to create actual versions by publishing. Save() updates existing version; /// Publish() creates a new version. /// [Test] [LongRunning] public void Benchmark_Rollback_ToVersion() { var content = ContentBuilder.CreateSimpleContent(ContentType, "RollbackTest", -1); ContentService.Save(content); ContentService.Publish(content, new[] { "*" }); // Initial publish creates version 1 // Create 10 versions by publishing repeatedly // Note: Save() updates existing version; Publish() creates new version for (var i = 0; i < 10; i++) { content.Name = $"RollbackTest_v{i}"; ContentService.Save(content); ContentService.Publish(content, new[] { "*" }); // Each publish creates a new version } var versions = ContentService.GetVersions(content.Id).ToList(); // v1.2: Defensive assertion and relative indexing to avoid index out of range Assert.That(versions.Count, Is.GreaterThanOrEqualTo(6), "Need at least 6 versions for rollback test"); var targetVersionId = versions[versions.Count / 2].VersionId; // Rollback to middle version MeasureAndRecord("Rollback_ToVersion", 1, () => { ContentService.Rollback(content.Id, targetVersionId); }, skipWarmup: true); // Modifying operation - results differ on repeat } /// /// Benchmark 32: DeleteVersions by date (100 versions). /// /// /// v1.2: Standardized from 50 to 100 versions. /// v1.3: Fixed to create actual versions by publishing. Save() updates existing version; /// Publish() creates a new version. /// [Test] [LongRunning] public void Benchmark_DeleteVersions_ByDate() { var content = ContentBuilder.CreateSimpleContent(ContentType, "DeleteVersionsTest", -1); ContentService.Save(content); ContentService.Publish(content, new[] { "*" }); // Initial publish creates version 1 // Create 100 versions by publishing repeatedly // Note: Save() updates existing version; Publish() creates new version const int versionCount = 100; // v1.2: Standardized to 100 for (var i = 0; i < versionCount; i++) { content.Name = $"DeleteVersionsTest_v{i}"; ContentService.Save(content); ContentService.Publish(content, new[] { "*" }); // Each publish creates a new version } // Delete all versions before "now" (which should be all of them) MeasureAndRecord("DeleteVersions_ByDate", versionCount, () => { ContentService.DeleteVersions(content.Id, DateTime.UtcNow.AddMinutes(1)); }, skipWarmup: true); // Destructive operation - cannot repeat } #endregion #region Baseline Comparison (1 test) /// /// Benchmark 33: Meta-benchmark for comparison validation. /// This test runs a representative sample and outputs summary statistics. /// /// /// v1.2: Uses manual Stopwatch timing (not MeasureAndRecord) because this benchmark /// times a composite sequence of operations (save, publish, query, trash, empty) /// to measure overall system performance, not individual operation latency. /// [Test] [LongRunning] public void Benchmark_BaselineComparison() { // This test exists to provide a consistent comparison point. // It runs a simple operation sequence that represents typical usage. // v1.2: Manual timing used for composite operation measurement var sw = Stopwatch.StartNew(); // Create 10 items var items = new List(); for (var i = 0; i < 10; i++) { var content = ContentBuilder.CreateSimpleContent(ContentType, $"Baseline_{i}", -1); ContentService.Save(content); items.Add(content); } // Publish all foreach (var item in items) { ContentService.Publish(item, new[] { "*" }); } // Query var count = ContentService.Count(ContentType.Alias); // Move to recycle bin foreach (var item in items) { ContentService.MoveToRecycleBin(item); } // Empty recycle bin ContentService.EmptyRecycleBin(); sw.Stop(); RecordBenchmark("BaselineComparison", sw.ElapsedMilliseconds, 10); TestContext.WriteLine($"[BASELINE] Total time for representative operations: {sw.ElapsedMilliseconds}ms"); } #endregion }