From 9f94290439e2ac3e509c7bbc5c7a3d64605c69a6 Mon Sep 17 00:00:00 2001 From: Michael DiLeo Date: Thu, 6 Aug 2026 13:40:00 -0500 Subject: [PATCH] remove the total history tracking and the priority queue for just tracking the variables. --- dotnet/SafelyYou/Devices.cs | 92 +++++++++++++++---------------------- 1 file changed, 38 insertions(+), 54 deletions(-) diff --git a/dotnet/SafelyYou/Devices.cs b/dotnet/SafelyYou/Devices.cs index ff69c49..8fddba7 100644 --- a/dotnet/SafelyYou/Devices.cs +++ b/dotnet/SafelyYou/Devices.cs @@ -1,6 +1,4 @@ using System.Collections.Concurrent; -using System.Runtime.InteropServices; -using SafelyYou.Config; namespace SafelyYou; @@ -16,82 +14,68 @@ public sealed class KnownDevices public bool Contains(DeviceId deviceId) => _knownDevices.Contains(deviceId); } -public readonly record struct HeartBeatRange(DateTimeOffset Start, DateTimeOffset? End); - public sealed class DeviceHeartBeatHistory { - private readonly HeartBeatHistoryConfig _config; - public enum AddResult { Added, OutOfSequence } - public DeviceHeartBeatHistory(HeartBeatHistoryConfig config) => _config = config; + public long NumberOfHeartBeats { get; private set; } + public DateTimeOffset? FirstHeartBeat { get; private set; } + public DateTimeOffset? LastHeartBeat { get; private set; } - private readonly PriorityQueue _histories = - // lowest priority is first, but this allows the highest date to be dequeued first - new PriorityQueue(new InverseDateTimeComparer()); - - private sealed class InverseDateTimeComparer : IComparer - { - public int Compare(DateTimeOffset x, DateTimeOffset y) => -(x.CompareTo(y)); - } - - /// Returns Added, needed to be used as an expression - private static AddResult Replace(PriorityQueue histories, HeartBeatRange updated) - { - histories.DequeueEnqueue(updated, updated.Start); - return AddResult.Added; - } - - /// Returns Added, needed to be used as an expression - private static AddResult EnqueueNew(PriorityQueue histories, DateTimeOffset sentAt) - { - histories.Enqueue(new HeartBeatRange(sentAt, null), sentAt); - return AddResult.Added; - } - public AddResult Add(DateTimeOffset sentAt) { - // check the latest first, assuming things are in order - // if it's "in the past" and doesn't make sense, return an error - - // histories that are adjacent are combined, if there's more than a one minute gap (what about a margin of error?) - // then create a new entry. The margin is coming from the config. I've set it to 1 second. - - if (!_histories.TryPeek(out var last, out _)) - return EnqueueNew(_histories, sentAt); - - // last registered heartbeat is End if set, otherwise Start - return (sentAt, last.Start, last.End) switch + // Reject heartbeats that arrive before the latest registered time. + // Gaps are fine: uptime can be derived from first, last, and count. + if (LastHeartBeat is null) { - var (sent, start, _) when sent < start => AddResult.OutOfSequence, - (var sent, _, { } end) when sent < end => AddResult.OutOfSequence, - // no End yet: extend if within Window of Start - (var sent, var start, null) when sent <= start + _config.Window => Replace(_histories, last with { End = sentAt }), - // End set: extend if within Window of End - (var sent, _, { } end) when sent <= end + _config.Window => Replace(_histories, last with { End = sentAt }), - // gap larger than Window: start a new range - _ => EnqueueNew(_histories, sentAt), - }; + FirstHeartBeat = sentAt; + LastHeartBeat = sentAt; + NumberOfHeartBeats = 1; + return AddResult.Added; + } + + if (sentAt < LastHeartBeat) + return AddResult.OutOfSequence; + + // note: does not account for rejecting heartbeats that come between the one minute window. + LastHeartBeat = sentAt; + NumberOfHeartBeats++; + return AddResult.Added; } } public sealed class HeartBeatHistories { - private readonly HeartBeatHistoryConfig _config; private readonly ConcurrentDictionary _histories = new(); - public HeartBeatHistories(HeartBeatHistoryConfig config) => _config = config; - public DeviceHeartBeatHistory.AddResult Add(DeviceId deviceId, DateTimeOffset sentAt) { if (!_histories.TryGetValue(deviceId, out var history)) { - history = new(_config); + history = new(); _histories[deviceId] = history; } return history.Add(sentAt); } + + public double Uptime(DeviceId deviceId) + { + if (!_histories.TryGetValue(deviceId, out var history)) return 0; + + var heartBeatMinutes = + (history.FirstHeartBeat, history.LastHeartBeat) switch + { + ({} first, {} second ) => (second - first).TotalMinutes, + (not null, null) => 1, + (null, _) => 0 + }; + + if (heartBeatMinutes == 0) return 0L; + + var uptime = (history.NumberOfHeartBeats / heartBeatMinutes) * 100L; + return uptime; + } }