2024-04-30 10:38:09 +02:00
|
|
|
using System.Diagnostics;
|
2024-05-07 20:41:06 +02:00
|
|
|
using System.Globalization;
|
|
|
|
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
2024-04-30 10:38:09 +02:00
|
|
|
|
|
|
|
namespace TanksServer.GameLogic;
|
|
|
|
|
2024-05-07 20:41:06 +02:00
|
|
|
internal sealed class UpdatesPerSecondCounter(
|
|
|
|
ILogger<UpdatesPerSecondCounter> logger
|
|
|
|
) : ITickStep, IHealthCheck
|
2024-04-30 10:38:09 +02:00
|
|
|
{
|
2024-04-30 11:16:26 +02:00
|
|
|
private static readonly TimeSpan LongTime = TimeSpan.FromSeconds(5);
|
|
|
|
private static readonly TimeSpan CriticalUpdateTime = TimeSpan.FromMilliseconds(50);
|
|
|
|
|
2024-04-30 10:38:09 +02:00
|
|
|
private readonly Stopwatch _long = Stopwatch.StartNew();
|
2024-05-07 20:41:06 +02:00
|
|
|
|
|
|
|
private readonly record struct Statistics(
|
|
|
|
ulong Updates,
|
|
|
|
TimeSpan TotalTime,
|
|
|
|
double AverageUpdatesPerSecond,
|
|
|
|
TimeSpan MinFrameTime,
|
|
|
|
TimeSpan AverageFrameTime,
|
|
|
|
TimeSpan MaxFrameTime)
|
|
|
|
{
|
|
|
|
public override string ToString() =>
|
|
|
|
$"{nameof(Updates)}: {Updates}, {nameof(TotalTime)}: {TotalTime}, {nameof(AverageUpdatesPerSecond)}: {AverageUpdatesPerSecond}, {nameof(MinFrameTime)}: {MinFrameTime}, {nameof(AverageFrameTime)}: {AverageFrameTime}, {nameof(MaxFrameTime)}: {MaxFrameTime}";
|
|
|
|
|
|
|
|
public Dictionary<string, object> ToDictionary() => new()
|
|
|
|
{
|
|
|
|
[nameof(Updates)] = Updates.ToString(),
|
|
|
|
[nameof(TotalTime)] = TotalTime.ToString(),
|
|
|
|
[nameof(AverageUpdatesPerSecond)] = AverageUpdatesPerSecond.ToString(CultureInfo.InvariantCulture),
|
|
|
|
[nameof(MinFrameTime)] = MinFrameTime.ToString(),
|
|
|
|
[nameof(AverageFrameTime)] = AverageFrameTime.ToString(),
|
|
|
|
[nameof(MaxFrameTime)] = MaxFrameTime.ToString()
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
private Statistics? _currentStatistics = null;
|
|
|
|
|
2024-04-30 10:38:09 +02:00
|
|
|
private ulong _updatesSinceLongReset;
|
|
|
|
private TimeSpan _minFrameTime = TimeSpan.MaxValue;
|
|
|
|
private TimeSpan _maxFrameTime = TimeSpan.MinValue;
|
|
|
|
|
|
|
|
public ValueTask TickAsync(TimeSpan delta)
|
|
|
|
{
|
|
|
|
if (logger.IsEnabled(LogLevel.Trace))
|
|
|
|
logger.LogTrace("time since last update: {}", delta);
|
2024-04-30 11:16:26 +02:00
|
|
|
|
|
|
|
if (delta > CriticalUpdateTime)
|
|
|
|
{
|
|
|
|
logger.LogCritical("a single update took {}, which is longer than the allowed {}",
|
|
|
|
delta, CriticalUpdateTime);
|
|
|
|
}
|
2024-04-30 10:38:09 +02:00
|
|
|
|
|
|
|
if (_minFrameTime > delta)
|
|
|
|
_minFrameTime = delta;
|
|
|
|
if (_maxFrameTime < delta)
|
|
|
|
_maxFrameTime = delta;
|
|
|
|
|
|
|
|
_updatesSinceLongReset++;
|
|
|
|
|
2024-04-30 11:16:26 +02:00
|
|
|
if (_long.Elapsed < LongTime)
|
2024-04-30 10:38:09 +02:00
|
|
|
return ValueTask.CompletedTask;
|
|
|
|
|
|
|
|
LogCounters();
|
|
|
|
ResetCounters();
|
|
|
|
return ValueTask.CompletedTask;
|
|
|
|
}
|
|
|
|
|
|
|
|
private void LogCounters()
|
|
|
|
{
|
2024-05-07 20:41:06 +02:00
|
|
|
var time = _long.Elapsed;
|
|
|
|
|
|
|
|
_currentStatistics = new Statistics(
|
|
|
|
_updatesSinceLongReset,
|
|
|
|
time,
|
|
|
|
_updatesSinceLongReset / time.TotalSeconds,
|
|
|
|
_minFrameTime,
|
|
|
|
time / _updatesSinceLongReset,
|
|
|
|
_maxFrameTime);
|
|
|
|
|
2024-04-30 10:38:09 +02:00
|
|
|
if (!logger.IsEnabled(LogLevel.Debug))
|
|
|
|
return;
|
|
|
|
|
2024-05-07 20:41:06 +02:00
|
|
|
logger.LogDebug("statistics: {}", _currentStatistics);
|
2024-04-30 10:38:09 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
private void ResetCounters()
|
|
|
|
{
|
|
|
|
_long.Restart();
|
|
|
|
_updatesSinceLongReset = 0;
|
|
|
|
_minFrameTime = TimeSpan.MaxValue;
|
|
|
|
_maxFrameTime = TimeSpan.MinValue;
|
|
|
|
}
|
2024-05-07 20:41:06 +02:00
|
|
|
|
|
|
|
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context,
|
|
|
|
CancellationToken cancellationToken = new())
|
|
|
|
{
|
|
|
|
var stats = _currentStatistics;
|
|
|
|
if (stats == null)
|
|
|
|
{
|
|
|
|
return Task.FromResult(
|
|
|
|
HealthCheckResult.Degraded("no statistics available yet - this is expected shortly after start"));
|
|
|
|
}
|
|
|
|
|
|
|
|
if (stats.Value.MaxFrameTime > CriticalUpdateTime)
|
|
|
|
{
|
|
|
|
return Task.FromResult(HealthCheckResult.Degraded("max frame time too high", null,
|
|
|
|
stats.Value.ToDictionary()));
|
|
|
|
}
|
|
|
|
|
|
|
|
return Task.FromResult(HealthCheckResult.Healthy("", stats.Value.ToDictionary()));
|
|
|
|
}
|
2024-04-30 10:38:09 +02:00
|
|
|
}
|