148 lines
4.6 KiB
C#
148 lines
4.6 KiB
C#
namespace NATS.Server.Auth;
|
||
|
||
/// <summary>
|
||
/// Tracks service request latency using a sorted list of samples for percentile calculation.
|
||
/// Go reference: accounts.go serviceLatency / serviceExportLatencyStats.
|
||
/// </summary>
|
||
public sealed class ServiceLatencyTracker
|
||
{
|
||
private readonly Lock _lock = new();
|
||
private readonly List<double> _samples = [];
|
||
private readonly int _maxSamples;
|
||
private long _totalRequests;
|
||
|
||
/// <summary>
|
||
/// Creates a latency tracker with a bounded in-memory sample window.
|
||
/// </summary>
|
||
/// <param name="maxSamples">Maximum number of latency samples retained for percentile calculations.</param>
|
||
public ServiceLatencyTracker(int maxSamples = 10000)
|
||
{
|
||
_maxSamples = maxSamples;
|
||
}
|
||
|
||
/// <summary>Records a latency sample in milliseconds.</summary>
|
||
/// <param name="latencyMs">Observed end-to-end service latency in milliseconds.</param>
|
||
public void RecordLatency(double latencyMs)
|
||
{
|
||
lock (_lock)
|
||
{
|
||
if (_samples.Count >= _maxSamples)
|
||
_samples.RemoveAt(0);
|
||
_samples.Add(latencyMs);
|
||
_totalRequests++;
|
||
}
|
||
}
|
||
|
||
/// <summary>Returns the 50th percentile (median) latency in milliseconds.</summary>
|
||
public double GetP50() => GetPercentile(0.50);
|
||
/// <summary>Returns the 90th percentile latency in milliseconds.</summary>
|
||
public double GetP90() => GetPercentile(0.90);
|
||
/// <summary>Returns the 99th percentile latency in milliseconds.</summary>
|
||
public double GetP99() => GetPercentile(0.99);
|
||
|
||
/// <summary>Returns the value at the given percentile (0.0–1.0) over recorded samples.</summary>
|
||
/// <param name="percentile">Percentile fraction between 0.0 and 1.0.</param>
|
||
public double GetPercentile(double percentile)
|
||
{
|
||
lock (_lock)
|
||
return ComputePercentile(_samples, percentile);
|
||
}
|
||
|
||
// Must be called under _lock.
|
||
private static double ComputePercentile(List<double> samples, double percentile)
|
||
{
|
||
if (samples.Count == 0)
|
||
return 0;
|
||
var sorted = new List<double>(samples);
|
||
sorted.Sort();
|
||
var index = (int)(percentile * (sorted.Count - 1));
|
||
return sorted[index];
|
||
}
|
||
|
||
// Must be called under _lock.
|
||
private static double ComputeAverage(List<double> samples)
|
||
{
|
||
if (samples.Count == 0)
|
||
return 0;
|
||
var sum = 0.0;
|
||
foreach (var s in samples)
|
||
sum += s;
|
||
return sum / samples.Count;
|
||
}
|
||
|
||
/// <summary>Total number of latency observations recorded.</summary>
|
||
public long TotalRequests
|
||
{
|
||
get { lock (_lock) return _totalRequests; }
|
||
}
|
||
|
||
/// <summary>Arithmetic mean latency across currently retained samples.</summary>
|
||
public double AverageLatencyMs
|
||
{
|
||
get { lock (_lock) return ComputeAverage(_samples); }
|
||
}
|
||
|
||
/// <summary>Minimum latency among currently retained samples.</summary>
|
||
public double MinLatencyMs
|
||
{
|
||
get
|
||
{
|
||
lock (_lock)
|
||
return _samples.Count == 0 ? 0 : _samples.Min();
|
||
}
|
||
}
|
||
|
||
/// <summary>Maximum latency among currently retained samples.</summary>
|
||
public double MaxLatencyMs
|
||
{
|
||
get
|
||
{
|
||
lock (_lock)
|
||
return _samples.Count == 0 ? 0 : _samples.Max();
|
||
}
|
||
}
|
||
|
||
/// <summary>Number of samples currently retained in memory.</summary>
|
||
public int SampleCount
|
||
{
|
||
get { lock (_lock) return _samples.Count; }
|
||
}
|
||
|
||
/// <summary>Clears all samples and resets the total request counter.</summary>
|
||
public void Reset()
|
||
{
|
||
lock (_lock)
|
||
{
|
||
_samples.Clear();
|
||
_totalRequests = 0;
|
||
}
|
||
}
|
||
|
||
/// <summary>Returns an immutable snapshot of the current tracker state.</summary>
|
||
public ServiceLatencySnapshot GetSnapshot()
|
||
{
|
||
lock (_lock)
|
||
{
|
||
return new ServiceLatencySnapshot(
|
||
TotalRequests: _totalRequests,
|
||
P50Ms: ComputePercentile(_samples, 0.50),
|
||
P90Ms: ComputePercentile(_samples, 0.90),
|
||
P99Ms: ComputePercentile(_samples, 0.99),
|
||
AverageMs: ComputeAverage(_samples),
|
||
MinMs: _samples.Count == 0 ? 0 : _samples.Min(),
|
||
MaxMs: _samples.Count == 0 ? 0 : _samples.Max(),
|
||
SampleCount: _samples.Count);
|
||
}
|
||
}
|
||
}
|
||
|
||
public sealed record ServiceLatencySnapshot(
|
||
long TotalRequests,
|
||
double P50Ms,
|
||
double P90Ms,
|
||
double P99Ms,
|
||
double AverageMs,
|
||
double MinMs,
|
||
double MaxMs,
|
||
int SampleCount);
|