// -----------------------------------------------------------------------------
// 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; }
}