Files
Explorer-Workbench/src/Explorer.Application/HostActivityLog.cs

88 lines
2.4 KiB
C#

using Explorer.Contracts;
namespace Explorer.Application;
public interface IHostActivitySink
{
void Record(string category, string message);
IReadOnlyList<HostActivityEvent> TakeRecent(int max = 120);
}
public sealed class NullHostActivitySink : IHostActivitySink
{
public static NullHostActivitySink Instance { get; } = new();
public void Record(string category, string message)
{
}
public IReadOnlyList<HostActivityEvent> TakeRecent(int max = 120) => [];
}
/// <summary>Bounded ring of host activity lines for the live monitor. Cheap to write; snapshot is a copy.</summary>
public sealed class HostActivityLog : IHostActivitySink
{
private readonly object _gate = new();
private readonly HostActivityEvent[] _ring;
private int _next;
private int _count;
private string? _lastKey;
private DateTimeOffset _lastUtc;
public HostActivityLog(int capacity = 48)
{
_ring = new HostActivityEvent[Math.Clamp(capacity, 32, 200)];
}
public void Record(string category, string message)
{
if (string.IsNullOrWhiteSpace(message))
{
return;
}
var cat = string.IsNullOrWhiteSpace(category) ? "Host" : category.Trim();
var msg = message.Trim();
var key = cat + "\u001f" + msg;
var now = DateTimeOffset.UtcNow;
lock (_gate)
{
if (key == _lastKey && (now - _lastUtc) < TimeSpan.FromMilliseconds(750))
{
return;
}
_lastKey = key;
_lastUtc = now;
_ring[_next] = new HostActivityEvent { Utc = now, Category = cat, Message = msg };
_next = (_next + 1) % _ring.Length;
if (_count < _ring.Length)
{
_count++;
}
}
}
public IReadOnlyList<HostActivityEvent> TakeRecent(int max = 120)
{
max = Math.Clamp(max, 1, _ring.Length);
lock (_gate)
{
var take = Math.Min(max, _count);
if (take == 0)
{
return [];
}
var result = new HostActivityEvent[take];
var start = (_next - take + _ring.Length) % _ring.Length;
for (var i = 0; i < take; i++)
{
result[i] = _ring[(start + i) % _ring.Length];
}
return result;
}
}
}