// ============================================================================= // StellaOps.Integration.Performance - Performance Baseline Tests // Sprint 3500.0004.0003 - T7: Performance Baseline Tests // ============================================================================= using FluentAssertions; using System.Diagnostics; using System.Text.Json; using Xunit; namespace StellaOps.Integration.Performance; /// /// Performance baseline tests to establish and validate performance characteristics. /// Uses timing measurements against known baselines with 20% regression threshold. /// /// /// T7-AC1: Score computation time baseline /// T7-AC2: Proof bundle generation baseline /// T7-AC3: Call graph extraction baseline /// T7-AC4: Reachability computation baseline /// T7-AC5: Regression alerts on >20% degradation /// [Trait("Category", "Performance")] [Trait("Category", "Integration")] public class PerformanceBaselineTests : IClassFixture { private readonly PerformanceTestFixture _fixture; private const double RegressionThresholdPercent = 20.0; public PerformanceBaselineTests(PerformanceTestFixture fixture) { _fixture = fixture; } #region T7-AC1: Score Computation Baseline [Fact(DisplayName = "T7-AC1.1: Score computation completes within baseline")] public async Task ScoreComputation_CompletesWithinBaseline() { // Arrange var baseline = _fixture.GetBaseline("score_computation_ms"); var findings = GenerateSampleFindings(100); // Act var sw = Stopwatch.StartNew(); var score = await ComputeScoreAsync(findings); sw.Stop(); // Assert var actualMs = sw.ElapsedMilliseconds; var threshold = baseline * (1 + RegressionThresholdPercent / 100); actualMs.Should().BeLessThanOrEqualTo((long)threshold, $"Score computation took {actualMs}ms, exceeding baseline {baseline}ms + {RegressionThresholdPercent}% threshold"); // Record for baseline updates _fixture.RecordMeasurement("score_computation_ms", actualMs); } [Fact(DisplayName = "T7-AC1.2: Score computation scales linearly with findings")] public async Task ScoreComputation_ScalesLinearly() { // Arrange var sizes = new[] { 10, 50, 100, 200 }; var times = new List<(int size, long ms)>(); // Act foreach (var size in sizes) { var findings = GenerateSampleFindings(size); var sw = Stopwatch.StartNew(); await ComputeScoreAsync(findings); sw.Stop(); times.Add((size, sw.ElapsedMilliseconds)); } // Assert - verify roughly linear scaling (within 3x of linear) var baseRatio = times[0].ms / (double)times[0].size; foreach (var (size, ms) in times.Skip(1)) { var actualRatio = ms / (double)size; var scaleFactor = actualRatio / baseRatio; scaleFactor.Should().BeLessThan(3.0, $"Score computation at size {size} shows non-linear scaling (factor: {scaleFactor:F2}x)"); } } [Fact(DisplayName = "T7-AC1.3: Score computation handles large finding sets")] public async Task ScoreComputation_HandlesLargeSets() { // Arrange var baseline = _fixture.GetBaseline("score_computation_large_ms"); var findings = GenerateSampleFindings(1000); // Act var sw = Stopwatch.StartNew(); var score = await ComputeScoreAsync(findings); sw.Stop(); // Assert var threshold = baseline * (1 + RegressionThresholdPercent / 100); sw.ElapsedMilliseconds.Should().BeLessThanOrEqualTo((long)threshold); _fixture.RecordMeasurement("score_computation_large_ms", sw.ElapsedMilliseconds); } #endregion #region T7-AC2: Proof Bundle Generation Baseline [Fact(DisplayName = "T7-AC2.1: Proof bundle generation completes within baseline")] public async Task ProofBundleGeneration_CompletesWithinBaseline() { // Arrange var baseline = _fixture.GetBaseline("proof_bundle_generation_ms"); var manifest = GenerateSampleManifest(); // Act var sw = Stopwatch.StartNew(); var bundle = await GenerateProofBundleAsync(manifest); sw.Stop(); // Assert var threshold = baseline * (1 + RegressionThresholdPercent / 100); sw.ElapsedMilliseconds.Should().BeLessThanOrEqualTo((long)threshold, $"Proof bundle generation took {sw.ElapsedMilliseconds}ms, exceeding baseline {baseline}ms"); _fixture.RecordMeasurement("proof_bundle_generation_ms", sw.ElapsedMilliseconds); } [Fact(DisplayName = "T7-AC2.2: Proof signing performance within baseline")] public async Task ProofSigning_WithinBaseline() { // Arrange var baseline = _fixture.GetBaseline("proof_signing_ms"); var payload = GenerateSamplePayload(10 * 1024); // 10KB payload // Act var sw = Stopwatch.StartNew(); var signature = await SignPayloadAsync(payload); sw.Stop(); // Assert var threshold = baseline * (1 + RegressionThresholdPercent / 100); sw.ElapsedMilliseconds.Should().BeLessThanOrEqualTo((long)threshold); _fixture.RecordMeasurement("proof_signing_ms", sw.ElapsedMilliseconds); } #endregion #region T7-AC3: Call Graph Extraction Baseline [Fact(DisplayName = "T7-AC3.1: .NET call graph extraction within baseline")] public async Task DotNetCallGraphExtraction_WithinBaseline() { // Arrange var baseline = _fixture.GetBaseline("dotnet_callgraph_extraction_ms"); var assemblyPath = _fixture.GetTestAssemblyPath("DotNetSample"); // Act var sw = Stopwatch.StartNew(); var graph = await ExtractDotNetCallGraphAsync(assemblyPath); sw.Stop(); // Assert var threshold = baseline * (1 + RegressionThresholdPercent / 100); sw.ElapsedMilliseconds.Should().BeLessThanOrEqualTo((long)threshold, $"Call graph extraction took {sw.ElapsedMilliseconds}ms, exceeding baseline {baseline}ms"); _fixture.RecordMeasurement("dotnet_callgraph_extraction_ms", sw.ElapsedMilliseconds); } [Fact(DisplayName = "T7-AC3.2: Call graph scales with assembly size")] public async Task CallGraphExtraction_ScalesWithSize() { // Arrange var assemblies = _fixture.GetTestAssemblies(); var results = new List<(string name, int nodes, long ms)>(); // Act foreach (var assembly in assemblies) { var sw = Stopwatch.StartNew(); var graph = await ExtractDotNetCallGraphAsync(assembly.Path); sw.Stop(); results.Add((assembly.Name, graph.NodeCount, sw.ElapsedMilliseconds)); } // Assert - log results for baseline establishment foreach (var (name, nodes, ms) in results) { _fixture.RecordMeasurement($"callgraph_{name}_ms", ms); } // Verify no catastrophic performance (>10s for any assembly) results.Should().AllSatisfy(r => r.ms.Should().BeLessThan(10000)); } #endregion #region T7-AC4: Reachability Computation Baseline [Fact(DisplayName = "T7-AC4.1: Reachability computation within baseline")] public async Task ReachabilityComputation_WithinBaseline() { // Arrange var baseline = _fixture.GetBaseline("reachability_computation_ms"); var callGraph = GenerateSampleCallGraph(500, 1000); // 500 nodes, 1000 edges // Act var sw = Stopwatch.StartNew(); var result = await ComputeReachabilityAsync(callGraph); sw.Stop(); // Assert var threshold = baseline * (1 + RegressionThresholdPercent / 100); sw.ElapsedMilliseconds.Should().BeLessThanOrEqualTo((long)threshold, $"Reachability computation took {sw.ElapsedMilliseconds}ms, exceeding baseline {baseline}ms"); _fixture.RecordMeasurement("reachability_computation_ms", sw.ElapsedMilliseconds); } [Fact(DisplayName = "T7-AC4.2: Large graph reachability within baseline")] public async Task LargeGraphReachability_WithinBaseline() { // Arrange var baseline = _fixture.GetBaseline("reachability_large_graph_ms"); var callGraph = GenerateSampleCallGraph(2000, 5000); // 2000 nodes, 5000 edges // Act var sw = Stopwatch.StartNew(); var result = await ComputeReachabilityAsync(callGraph); sw.Stop(); // Assert var threshold = baseline * (1 + RegressionThresholdPercent / 100); sw.ElapsedMilliseconds.Should().BeLessThanOrEqualTo((long)threshold, $"Large graph reachability took {sw.ElapsedMilliseconds}ms, exceeding baseline {baseline}ms"); _fixture.RecordMeasurement("reachability_large_graph_ms", sw.ElapsedMilliseconds); } [Fact(DisplayName = "T7-AC4.3: Reachability with deep paths within baseline")] public async Task DeepPathReachability_WithinBaseline() { // Arrange var baseline = _fixture.GetBaseline("reachability_deep_path_ms"); var callGraph = GenerateDeepCallGraph(100); // 100 levels deep // Act var sw = Stopwatch.StartNew(); var result = await ComputeReachabilityAsync(callGraph); sw.Stop(); // Assert var threshold = baseline * (1 + RegressionThresholdPercent / 100); sw.ElapsedMilliseconds.Should().BeLessThanOrEqualTo((long)threshold); _fixture.RecordMeasurement("reachability_deep_path_ms", sw.ElapsedMilliseconds); } #endregion #region T7-AC5: Regression Alerts [Fact(DisplayName = "T7-AC5.1: All baselines within threshold")] public void AllBaselines_WithinThreshold() { // Arrange var measurements = _fixture.GetAllMeasurements(); var regressions = new List(); // Act & Assert foreach (var (metric, measured) in measurements) { var baseline = _fixture.GetBaseline(metric); var threshold = baseline * (1 + RegressionThresholdPercent / 100); if (measured > threshold) { var regression = (measured - baseline) / baseline * 100; regressions.Add($"{metric}: {measured}ms vs baseline {baseline}ms (+{regression:F1}%)"); } } regressions.Should().BeEmpty( $"Performance regressions detected (>{RegressionThresholdPercent}%):\n" + string.Join("\n", regressions)); } [Fact(DisplayName = "T7-AC5.2: Generate regression report")] public void GenerateRegressionReport() { // Arrange var measurements = _fixture.GetAllMeasurements(); // Act var report = new PerformanceReport { GeneratedAt = DateTime.UtcNow, ThresholdPercent = RegressionThresholdPercent, Metrics = measurements.Select(m => new MetricReport { Name = m.metric, Baseline = _fixture.GetBaseline(m.metric), Measured = m.value, DeltaPercent = (m.value - _fixture.GetBaseline(m.metric)) / _fixture.GetBaseline(m.metric) * 100 }).ToList() }; // Assert - report should be valid report.Metrics.Should().NotBeEmpty(); // Write report for CI consumption var json = JsonSerializer.Serialize(report, new JsonSerializerOptions { WriteIndented = true }); _fixture.SaveReport("performance-report.json", json); } #endregion #region Helper Methods private static List GenerateSampleFindings(int count) { return Enumerable.Range(1, count) .Select(i => new SampleFinding { Id = $"finding-{i:D4}", CveId = $"CVE-2024-{i:D5}", Severity = (i % 4) switch { 0 => "CRITICAL", 1 => "HIGH", 2 => "MEDIUM", _ => "LOW" }, CvssScore = 10.0 - (i % 10) }) .ToList(); } private static async Task ComputeScoreAsync(List findings) { // Simulated score computation await Task.Delay(findings.Count / 10); // ~10 findings per ms return findings.Sum(f => f.CvssScore) / findings.Count; } private static SampleManifest GenerateSampleManifest() { return new SampleManifest { Id = Guid.NewGuid().ToString(), CreatedAt = DateTime.UtcNow, Findings = GenerateSampleFindings(50) }; } private static async Task GenerateProofBundleAsync(SampleManifest manifest) { await Task.Delay(50); // Simulated bundle generation return JsonSerializer.SerializeToUtf8Bytes(manifest); } private static byte[] GenerateSamplePayload(int sizeBytes) { var random = new Random(42); var buffer = new byte[sizeBytes]; random.NextBytes(buffer); return buffer; } private static async Task SignPayloadAsync(byte[] payload) { await Task.Delay(10); // Simulated signing using var sha256 = System.Security.Cryptography.SHA256.Create(); return sha256.ComputeHash(payload); } private static async Task ExtractDotNetCallGraphAsync(string assemblyPath) { await Task.Delay(100); // Simulated extraction return new SampleCallGraph { NodeCount = 100, EdgeCount = 250 }; } private static SampleCallGraph GenerateSampleCallGraph(int nodes, int edges) { return new SampleCallGraph { NodeCount = nodes, EdgeCount = edges }; } private static SampleCallGraph GenerateDeepCallGraph(int depth) { return new SampleCallGraph { NodeCount = depth, EdgeCount = depth - 1, Depth = depth }; } private static async Task ComputeReachabilityAsync(SampleCallGraph graph) { // Simulated reachability - O(V + E) complexity var delay = (graph.NodeCount + graph.EdgeCount) / 100; await Task.Delay(Math.Max(1, delay)); return new ReachabilityResult { ReachableNodes = graph.NodeCount / 2 }; } #endregion #region Sample Types private record SampleFinding { public string Id { get; init; } = ""; public string CveId { get; init; } = ""; public string Severity { get; init; } = ""; public double CvssScore { get; init; } } private record SampleManifest { public string Id { get; init; } = ""; public DateTime CreatedAt { get; init; } public List Findings { get; init; } = new(); } private record SampleCallGraph { public int NodeCount { get; init; } public int EdgeCount { get; init; } public int Depth { get; init; } } private record ReachabilityResult { public int ReachableNodes { get; init; } } private record PerformanceReport { public DateTime GeneratedAt { get; init; } public double ThresholdPercent { get; init; } public List Metrics { get; init; } = new(); } private record MetricReport { public string Name { get; init; } = ""; public double Baseline { get; init; } public double Measured { get; init; } public double DeltaPercent { get; init; } } #endregion }