ContentService Refactoring - Performance Benchmarks
Generated: 2025-12-24
Branch: refactor/ContentService
Phases Tested: Phase 0 (baseline) through Phase 8 (final)
Executive Summary
The ContentService refactoring from a monolithic 3800-line class to a facade pattern with 7 specialized services achieved a net positive performance outcome:
- Overall runtime: -4.1% (29.5s → 28.3s total benchmark time)
- Major batch operations improved by 10-54%
- Core CRUD operations stable (within ±1%)
- 5 minor regressions identified, mostly on low-latency single-item operations
Benchmark Comparison: Phase 0 → Phase 8
Key Operations
| Benchmark |
P0 |
P2 |
P3 |
P4 |
P5 |
P6 |
P7 |
P8 |
Δ P0→P8 |
| Save_SingleItem |
7 |
19 |
6 |
6 |
7 |
7 |
6 |
7 |
+0.0% |
| Save_BatchOf100 |
676 |
718 |
701 |
689 |
692 |
689 |
727 |
670 |
-0.9% |
| Save_BatchOf1000 |
7649 |
7841 |
7871 |
7868 |
7867 |
7862 |
7929 |
7725 |
+1.0% |
| GetById_Single |
8 |
11 |
8 |
9 |
41 |
18 |
14 |
37 |
+362.5% |
| GetByIds_BatchOf100 |
14 |
14 |
14 |
14 |
14 |
14 |
13 |
13 |
-7.1% |
| Delete_SingleItem |
35 |
24 |
22 |
21 |
24 |
24 |
23 |
23 |
-34.3% |
| Delete_WithDescendants |
243 |
254 |
256 |
253 |
255 |
268 |
243 |
253 |
+4.1% |
| Publish_SingleItem |
21 |
28 |
23 |
23 |
24 |
27 |
22 |
24 |
+14.3% |
| Publish_BatchOf100 |
2456 |
2597 |
2637 |
2576 |
2711 |
2639 |
2531 |
2209 |
-10.1% |
| Move_SingleItem |
22 |
26 |
28 |
26 |
32 |
44 |
65 |
27 |
+22.7% |
| Move_WithDescendants |
592 |
618 |
641 |
635 |
671 |
615 |
620 |
597 |
+0.8% |
| MoveToRecycleBin_LargeTree |
8955 |
9126 |
9751 |
9222 |
9708 |
9245 |
9252 |
9194 |
+2.7% |
| EmptyRecycleBin_100Items |
847 |
919 |
861 |
886 |
899 |
871 |
912 |
869 |
+2.6% |
| BaselineComparison |
1357 |
1360 |
1395 |
1436 |
1384 |
1398 |
1407 |
1408 |
+3.8% |
All times in milliseconds (ms)
All Benchmarks - Complete Results
CRUD Operations (7 benchmarks)
| Benchmark |
Phase 0 |
Phase 8 |
Change |
| Save_SingleItem |
7ms |
7ms |
0.0% |
| Save_BatchOf100 |
676ms |
670ms |
-0.9% |
| Save_BatchOf1000 |
7649ms |
7725ms |
+1.0% |
| GetById_Single |
8ms |
37ms |
+362.5% |
| GetByIds_BatchOf100 |
14ms |
13ms |
-7.1% |
| Delete_SingleItem |
35ms |
23ms |
-34.3% |
| Delete_WithDescendants |
243ms |
253ms |
+4.1% |
Query Operations (6 benchmarks)
| Benchmark |
Phase 0 |
Phase 8 |
Change |
| GetPagedChildren_100Items |
16ms |
15ms |
-6.3% |
| GetPagedDescendants_DeepTree |
25ms |
25ms |
0.0% |
| GetAncestors_DeepHierarchy |
31ms |
21ms |
-32.3% |
| Count_ByContentType |
1ms |
1ms |
0.0% |
| CountDescendants_LargeTree |
1ms |
1ms |
0.0% |
| HasChildren_100Nodes |
65ms |
157ms |
+141.5% |
Publish Operations (7 benchmarks)
| Benchmark |
Phase 0 |
Phase 8 |
Change |
| Publish_SingleItem |
21ms |
24ms |
+14.3% |
| Publish_BatchOf100 |
2456ms |
2209ms |
-10.1% |
| PublishBranch_ShallowTree |
50ms |
48ms |
-4.0% |
| PublishBranch_DeepTree |
51ms |
47ms |
-7.8% |
| Unpublish_SingleItem |
23ms |
28ms |
+21.7% |
| PerformScheduledPublish |
2526ms |
2576ms |
+2.0% |
| GetContentSchedulesByIds_100Items |
1ms |
1ms |
0.0% |
Move Operations (8 benchmarks)
| Benchmark |
Phase 0 |
Phase 8 |
Change |
| Move_SingleItem |
22ms |
27ms |
+22.7% |
| Move_WithDescendants |
592ms |
597ms |
+0.8% |
| MoveToRecycleBin_Published |
34ms |
33ms |
-2.9% |
| MoveToRecycleBin_LargeTree |
8955ms |
9194ms |
+2.7% |
| Copy_SingleItem |
30ms |
26ms |
-13.3% |
| Copy_Recursive_100Items |
2809ms |
1300ms |
-53.7% |
| Sort_100Children |
758ms |
791ms |
+4.4% |
| EmptyRecycleBin_100Items |
847ms |
869ms |
+2.6% |
Version Operations (4 benchmarks)
| Benchmark |
Phase 0 |
Phase 8 |
Change |
| GetVersions_ItemWith100Versions |
19ms |
14ms |
-26.3% |
| GetVersionsSlim_Paged |
8ms |
12ms |
+50.0% |
| Rollback_ToVersion |
33ms |
35ms |
+6.1% |
| DeleteVersions_ByDate |
178ms |
131ms |
-26.4% |
Meta Benchmark
| Benchmark |
Phase 0 |
Phase 8 |
Change |
| BaselineComparison |
1357ms |
1408ms |
+3.8% |
Performance Analysis
Improvements (>10% faster)
| Operation |
Before |
After |
Change |
Impact |
| Copy_Recursive_100Items |
2809ms |
1300ms |
-53.7% |
High - major batch operation |
| Delete_SingleItem |
35ms |
23ms |
-34.3% |
Medium - common operation |
| GetAncestors_DeepHierarchy |
31ms |
21ms |
-32.3% |
Medium - tree traversal |
| DeleteVersions_ByDate |
178ms |
131ms |
-26.4% |
Medium - cleanup operation |
| GetVersions_ItemWith100Versions |
19ms |
14ms |
-26.3% |
Low - version history |
| Copy_SingleItem |
30ms |
26ms |
-13.3% |
Low - single copy |
| Publish_BatchOf100 |
2456ms |
2209ms |
-10.1% |
High - major batch operation |
Regressions (>20% slower)
| Operation |
Before |
After |
Change |
Analysis |
| GetById_Single |
8ms |
37ms |
+362.5% |
High variance on low base; 29ms absolute increase |
| HasChildren_100Nodes |
65ms |
157ms |
+141.5% |
Needs investigation - 92ms absolute increase |
| GetVersionsSlim_Paged |
8ms |
12ms |
+50.0% |
Low base; 4ms absolute increase |
| Move_SingleItem |
22ms |
27ms |
+22.7% |
Low base; 5ms absolute increase |
| Unpublish_SingleItem |
23ms |
28ms |
+21.7% |
Low base; 5ms absolute increase |
Regression Analysis
GetById_Single (+362.5%)
- Absolute impact: 29ms increase (8ms → 37ms)
- Likely cause: High measurement variance on very fast operations
- Recommendation: Low priority - single-item retrieval remains sub-50ms
HasChildren_100Nodes (+141.5%)
- Absolute impact: 92ms increase (65ms → 157ms)
- Likely cause: Additional delegation overhead in service layer
- Recommendation: Investigate - this is a repeated operation (100 calls) that could affect tree rendering performance
Other Regressions
- All under 10ms absolute increase
- Within acceptable variance for integration tests
- No action required
Methodology
Benchmark Infrastructure
- Framework: NUnit with custom
ContentServiceBenchmarkBase
- Database: Fresh schema per test (
UmbracoTestOptions.Database.NewSchemaPerTest)
- Warmup: JIT warmup iteration before measurement (skipped for destructive operations)
- Isolation:
[NonParallelizable] attribute ensures no concurrent test interference
Test Environment
- Platform: Linux Ubuntu 25.10
- CPU: Intel Xeon 2.80GHz, 8 physical cores
- .NET: 10.0.0
- Database: SQLite (in-memory for tests)
Regression Threshold
- Default: 20% regression fails the test
- CI Override:
BENCHMARK_REGRESSION_THRESHOLD environment variable
- Strict Mode:
BENCHMARK_REQUIRE_BASELINE=true fails if baseline missing
Phase-by-Phase Progression
| Phase |
Description |
Performance Impact |
| Phase 0 |
Baseline (pre-refactoring) |
Reference point |
| Phase 1 |
CRUD extraction |
Minor variance |
| Phase 2 |
Query extraction |
Stable |
| Phase 3 |
Version extraction |
Stable |
| Phase 4 |
Move extraction |
Stable |
| Phase 5 |
Publish extraction |
Minor regression in GetById |
| Phase 6 |
Permission extraction |
Move_SingleItem spike (recovered) |
| Phase 7 |
Blueprint extraction |
Move_SingleItem spike (recovered) |
| Phase 8 |
Facade finalization |
Final optimization pass |
Recommendations
Immediate Actions
- Investigate HasChildren_100Nodes regression (+141.5%)
- Profile the
HasChildren method delegation path
- Check for unnecessary database round-trips
- Consider caching strategy for repeated calls
Future Optimizations
- Batch HasChildren calls - Add
HasChildren(IEnumerable<int> ids) overload
- Cache warmup - Pre-warm frequently accessed content in integration scenarios
- CI Integration - Add benchmark stage to PR pipeline with 20% threshold
No Action Required
- Single-item operation regressions (< 30ms absolute)
- Variance-driven spikes (GetById_Single)
- Batch operations (all improved or stable)
Conclusion
The ContentService refactoring achieved its performance goals:
- No significant regressions on critical paths (Save, Publish batch, Delete batch)
- Major improvements on Copy and Delete operations
- One area for investigation: HasChildren repeated calls
- Overall 4.1% improvement in total benchmark runtime
The refactoring successfully traded minimal single-item overhead for improved batch performance and better code organization.