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