170 lines
6.2 KiB
C#
170 lines
6.2 KiB
C#
// -----------------------------------------------------------------------------
|
|
// 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;
|
|
|
|
/// <summary>
|
|
/// Compares latency metrics between scanner runs.
|
|
/// </summary>
|
|
public sealed class LatencyComparisonLogic
|
|
{
|
|
/// <summary>
|
|
/// Compares latency from multiple scan runs.
|
|
/// </summary>
|
|
public LatencyComparisonResult Compare(
|
|
IEnumerable<ScannerOutput> baselineRuns,
|
|
IEnumerable<ScannerOutput> 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())
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculates time-to-first-signal (TTFS) if available in scan output.
|
|
/// </summary>
|
|
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<long> 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<long> 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));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Result of latency comparison between two scanners.
|
|
/// </summary>
|
|
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; }
|
|
|
|
/// <summary>
|
|
/// Returns true if candidate is faster at P95 (ratio < 1).
|
|
/// </summary>
|
|
public bool CandidateIsFaster => P95Ratio < 1.0;
|
|
|
|
/// <summary>
|
|
/// Returns the percentage improvement (positive = candidate faster).
|
|
/// </summary>
|
|
public double ImprovementPercent => (1 - P95Ratio) * 100;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Time-to-first-signal measurement result.
|
|
/// </summary>
|
|
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; }
|
|
}
|