servicepoint-tanks/tanks-backend/TanksServer/GameLogic/UpdatesPerSecondCounter.cs

115 lines
3.7 KiB
C#
Raw Normal View History

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
}