// ----------------------------------------------------------------------------- // LatencyComparisonLogic.cs // Sprint: SPRINT_5100_0008_0001_competitor_parity // Task: PARITY-5100-006 - Implement latency comparison // Description: Logic for comparing scan latency between scanners // ----------------------------------------------------------------------------- namespace StellaOps.Parity.Tests; /// /// Compares latency metrics between scanner runs. /// public sealed class LatencyComparisonLogic { /// /// Compares latency from multiple scan runs. /// public LatencyComparisonResult Compare( IEnumerable baselineRuns, IEnumerable candidateRuns) { var baselineList = baselineRuns.Where(r => r.Success).ToList(); var candidateList = candidateRuns.Where(r => r.Success).ToList(); if (baselineList.Count == 0 || candidateList.Count == 0) { return new LatencyComparisonResult { BaselineTool = baselineList.FirstOrDefault()?.ToolName ?? "unknown", CandidateTool = candidateList.FirstOrDefault()?.ToolName ?? "unknown", Error = "Insufficient successful runs for comparison" }; } var baselineMs = baselineList.Select(r => r.DurationMs).OrderBy(d => d).ToList(); var candidateMs = candidateList.Select(r => r.DurationMs).OrderBy(d => d).ToList(); return new LatencyComparisonResult { BaselineTool = baselineList[0].ToolName, CandidateTool = candidateList[0].ToolName, Success = true, // Baseline stats BaselineP50 = CalculatePercentile(baselineMs, 50), BaselineP95 = CalculatePercentile(baselineMs, 95), BaselineP99 = CalculatePercentile(baselineMs, 99), BaselineMin = baselineMs.Min(), BaselineMax = baselineMs.Max(), BaselineMean = baselineMs.Average(), BaselineStdDev = CalculateStdDev(baselineMs), BaselineSampleCount = baselineMs.Count, // Candidate stats CandidateP50 = CalculatePercentile(candidateMs, 50), CandidateP95 = CalculatePercentile(candidateMs, 95), CandidateP99 = CalculatePercentile(candidateMs, 99), CandidateMin = candidateMs.Min(), CandidateMax = candidateMs.Max(), CandidateMean = candidateMs.Average(), CandidateStdDev = CalculateStdDev(candidateMs), CandidateSampleCount = candidateMs.Count, // Comparison metrics P50Ratio = CalculatePercentile(candidateMs, 50) / Math.Max(1, CalculatePercentile(baselineMs, 50)), P95Ratio = CalculatePercentile(candidateMs, 95) / Math.Max(1, CalculatePercentile(baselineMs, 95)), MeanRatio = candidateMs.Average() / Math.Max(1, baselineMs.Average()) }; } /// /// Calculates time-to-first-signal (TTFS) if available in scan output. /// public TimeToFirstSignalResult CalculateTtfs(ScannerOutput output) { return new TimeToFirstSignalResult { ToolName = output.ToolName, TotalDurationMs = output.DurationMs, // TTFS would require streaming output parsing, which most tools don't support // For now, we approximate as total duration TtfsMs = output.DurationMs, TtfsAvailable = false }; } private static double CalculatePercentile(List sortedValues, int percentile) { if (sortedValues.Count == 0) return 0; var index = (percentile / 100.0) * (sortedValues.Count - 1); var lower = (int)Math.Floor(index); var upper = (int)Math.Ceiling(index); if (lower == upper) return sortedValues[lower]; var fraction = index - lower; return sortedValues[lower] * (1 - fraction) + sortedValues[upper] * fraction; } private static double CalculateStdDev(List values) { if (values.Count < 2) return 0; var mean = values.Average(); var sumSquares = values.Sum(v => Math.Pow(v - mean, 2)); return Math.Sqrt(sumSquares / (values.Count - 1)); } } /// /// Result of latency comparison between two scanners. /// public sealed class LatencyComparisonResult { public required string BaselineTool { get; init; } public required string CandidateTool { get; init; } public bool Success { get; set; } public string? Error { get; set; } // Baseline latency stats (milliseconds) public double BaselineP50 { get; set; } public double BaselineP95 { get; set; } public double BaselineP99 { get; set; } public long BaselineMin { get; set; } public long BaselineMax { get; set; } public double BaselineMean { get; set; } public double BaselineStdDev { get; set; } public int BaselineSampleCount { get; set; } // Candidate latency stats (milliseconds) public double CandidateP50 { get; set; } public double CandidateP95 { get; set; } public double CandidateP99 { get; set; } public long CandidateMin { get; set; } public long CandidateMax { get; set; } public double CandidateMean { get; set; } public double CandidateStdDev { get; set; } public int CandidateSampleCount { get; set; } // Comparison ratios (candidate / baseline, <1 means candidate is faster) public double P50Ratio { get; set; } public double P95Ratio { get; set; } public double MeanRatio { get; set; } /// /// Returns true if candidate is faster at P95 (ratio < 1). /// public bool CandidateIsFaster => P95Ratio < 1.0; /// /// Returns the percentage improvement (positive = candidate faster). /// public double ImprovementPercent => (1 - P95Ratio) * 100; } /// /// Time-to-first-signal measurement result. /// public sealed class TimeToFirstSignalResult { public required string ToolName { get; init; } public long TotalDurationMs { get; set; } public long TtfsMs { get; set; } public bool TtfsAvailable { get; set; } }