From e53b74e0fac4feffb329252101c5dc51675d9728 Mon Sep 17 00:00:00 2001 From: Steve Davis Date: Tue, 17 Dec 2024 10:31:22 -0600 Subject: [PATCH] adding NProg tracker class --- OrderCloud.Catalyst/Core/NProg.cs | 225 ++++++++++++++++++++++++++++++ 1 file changed, 225 insertions(+) create mode 100644 OrderCloud.Catalyst/Core/NProg.cs diff --git a/OrderCloud.Catalyst/Core/NProg.cs b/OrderCloud.Catalyst/Core/NProg.cs new file mode 100644 index 0000000..a83f2c7 --- /dev/null +++ b/OrderCloud.Catalyst/Core/NProg.cs @@ -0,0 +1,225 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace OrderCloud.Catalyst +{ + public class Tracker + { + private long _startTime = DateTime.UtcNow.Ticks; + private long _endTime; + private int _total; + private int _started; + private int _succeeded; + private int _failed; + + private readonly List _actions = new List(); + private readonly List _tasks = new List(); + private readonly List _timers = new List(); + + public Tracker() { } + public Tracker(int itemCount) => _total = itemCount; + + public void Start() + { + _startTime = DateTime.UtcNow.Ticks; + _timers.ForEach(t => t.Start(this)); + } + + public void Stop() + { + _endTime = DateTime.UtcNow.Ticks; + _timers.ForEach(t => t.Stop()); + } + + public void Every(Trigger trigger, Action action) => _actions.Add(new ProgressAction { Trigger = trigger, Action = action, Recurring = true }); + public void On(Trigger trigger, Action action) => _actions.Add(new ProgressAction { Trigger = trigger, Action = action, Recurring = false }); + public void Every(TimeSpan interval, Action action) => _timers.Add(new TimerAction { Interval = interval, Action = action }); + public void OnComplete(Action action) => On(_total.ItemsDone(), action); + public void Now(Action action) => action(GetProgress()); + + public void Every(Trigger trigger, Func action) => _actions.Add(new ProgressAction { Trigger = trigger, AsyncAction = action, Recurring = true }); + public void On(Trigger trigger, Func action) => _actions.Add(new ProgressAction { Trigger = trigger, AsyncAction = action, Recurring = false }); + public void Every(TimeSpan interval, Func action) => _timers.Add(new TimerAction { Interval = interval, AsyncAction = action }); + public void OnComplete(Func action) => On(_total.ItemsDone(), action); + public void Now(Func action) => action(GetProgress()); + + private readonly object _lock = new object(); + + public void ItemStarted() => ProcessTriggers(ref _started); + public void ItemSucceeded() => ProcessTriggers(ref _succeeded); + public void ItemFailed() => ProcessTriggers(ref _failed); + + public void ItemsDiscovered(int count) + { + Interlocked.Add(ref _total, count); + } + + /// + /// Returns a completion task that should be awaited if any async actions were triggered. + /// Async actions are NOT awaited inline. + /// + public Task CompleteAsync() => Task.WhenAll(_tasks); + + public Progress GetProgress() => new Progress(_startTime, _endTime, _total, _started, _succeeded, _failed); + + private void ProcessTriggers(ref int fieldToIncrement) + { + var fired = new List(); + Progress prog; + + lock (_lock) + { + fieldToIncrement++; + prog = GetProgress(); + fired = _actions.Where(act => act.Trigger.IsFired(prog)).ToList(); + + foreach (var act in fired) + { + if (!act.Recurring) + _actions.Remove(act); + } + + _tasks.RemoveAll(t => t.IsCompleted); + } + + fired.ForEach(act => act.Invoke(prog, _tasks)); + } + + private abstract class ActionBase + { + public Action Action { get; set; } + public Func AsyncAction { get; set; } + + public void Invoke(Progress prog, IList tasks) + { + var task = AsyncAction?.Invoke(prog); + Action?.Invoke(prog); + if (task?.IsCompleted == false) + tasks.Add(task); + } + } + + private class ProgressAction : ActionBase + { + public Trigger Trigger { get; set; } + public bool Recurring { get; set; } + } + + private class TimerAction : ActionBase + { + private Timer _timer; + + public TimeSpan Interval { get; set; } + + public void Start(Tracker tracker) + { + _timer = new Timer(_ => Invoke(tracker.GetProgress(), tracker._tasks), null, Interval, Interval); + } + + public void Stop() + { + _timer.Dispose(); + } + } + } + + public class Trigger + { + private readonly int _targetNumber; + private readonly Func _getCurrentNumber; + private int _nextNumber; + + public Trigger(int targetNumber, Func getCurrentNumber) + { + _nextNumber = _targetNumber = targetNumber; + _getCurrentNumber = getCurrentNumber; + } + + public bool IsFired(Progress prog) + { + if (_getCurrentNumber(prog) >= _nextNumber) + { + _nextNumber += _targetNumber; + return true; + } + return false; + } + } + + public static class TriggerBuilderExtensions + { + public static Trigger ItemsStarted(this int i) => new Trigger(i, p => p.ItemsStarted); + public static Trigger ItemsDone(this int i) => new Trigger(i, p => p.ItemsDone); + public static Trigger ItemsSucceeded(this int i) => new Trigger(i, p => p.ItemsSucceeded); + public static Trigger ItemsFailed(this int i) => new Trigger(i, p => p.ItemsFailed); + + public static Trigger PercentStarted(this int i) => new Trigger(i, p => p.PercentStarted); + public static Trigger PercentDone(this int i) => new Trigger(i, p => p.PercentDone); + public static Trigger PercentSucceeded(this int i) => new Trigger(i, p => p.PercentSucceeded); + public static Trigger PercentFailed(this int i) => new Trigger(i, p => p.PercentFailed); + + public static TimeSpan Seconds(this int i) => TimeSpan.FromSeconds(i); + public static TimeSpan Minutes(this int i) => TimeSpan.FromMinutes(i); + public static TimeSpan Hours(this int i) => TimeSpan.FromHours(i); + } + + public class Progress + { + private readonly long _startTime; + private readonly long _endTime; + + public int TotalItems { get; } + public int ItemsStarted { get; } + public int ItemsSucceeded { get; } + public int ItemsFailed { get; } + + public int ItemsDone => ItemsSucceeded + ItemsFailed; + public int ItemsInProgress => ItemsStarted - ItemsDone; + public int ItemsRemaining => TotalItems - ItemsStarted; + + public int PercentStarted => SafeDivide(100 * ItemsStarted, TotalItems); + public int PercentDone => SafeDivide(100 * ItemsDone, TotalItems); + public int PercentSucceeded => SafeDivide(100 * ItemsSucceeded, TotalItems); + public int PercentFailed => SafeDivide(100 * ItemsFailed, TotalItems); + public int PercentInProgress => SafeDivide(100 * ItemsInProgress, TotalItems); + public int PercentRemaining => SafeDivide(100 * ItemsRemaining, TotalItems); + + public double PercentStartedExact => SafeDivideExact(100 * ItemsStarted, TotalItems); + public double PercentDoneExact => SafeDivideExact(100 * ItemsDone, TotalItems); + public double PercentSucceededExact => SafeDivideExact(100 * ItemsSucceeded, TotalItems); + public double PercentFailedExact => SafeDivideExact(100 * ItemsFailed, TotalItems); + public double PercentInProgressExact => SafeDivideExact(100 * ItemsInProgress, TotalItems); + public double PercentRemainingExact => SafeDivideExact(100 * ItemsRemaining, TotalItems); + + private long ElapsedTicks => (_endTime == 0 ? DateTime.UtcNow.Ticks : _endTime) - _startTime; + + public TimeSpan ElapsedTime => TimeSpan.FromTicks(ElapsedTicks); + public int ElapsedSeconds => (int)ElapsedTime.TotalSeconds; + public int ElapsedMinutes => (int)ElapsedTime.TotalMinutes; + public int ElapsedHours => (int)ElapsedTime.TotalHours; + + public TimeSpan EstTotalTime => TimeSpan.FromTicks(SafeDivide(ElapsedTicks * TotalItems, ItemsDone)); + public TimeSpan EstTimeRemaining => EstTotalTime - ElapsedTime; + public DateTime EstEndTimeUtc => DateTime.UtcNow + EstTimeRemaining; + public DateTime EstEndTimeLocal => DateTime.Now + EstTimeRemaining; + + public bool IsDone => ItemsDone == TotalItems; + + public Progress(long startTime, long endTime, int total, int started, int succeeded, int failed) + { + _startTime = startTime; + _endTime = endTime; + TotalItems = total; + ItemsStarted = started; + ItemsSucceeded = succeeded; + ItemsFailed = failed; + } + + private int SafeDivide(int x, int y) => y == 0 ? 0 : x / y; + private long SafeDivide(long x, long y) => y == 0 ? 0 : x / y; + private double SafeDivideExact(long x, long y) => y == 0 ? 0 : (double)x / y; + } +} \ No newline at end of file