Updated.
The first version of the answer calculated the values of RemainingTime and TotalTime at the end of every iteration. Given the way the for loops are nested, it could cause long delays between updates. In order to read those values at given intervals, some changes are made.
Let's start with the Loop class. It is used to keep track of the details of every for loop, like the total amount of iterations, the current iteration and the time consumed in every iteration. To acomplish the later, two System.Diagnostic Stopwatch are used. One is kept running free, not reseted, to ease the calculation of the average time for an iteration. The other clock is reseted on every iteration to provide a value for LoopTime, used in calculations on the fly of RemainingTime and TotalTime, when accesed via the property getters. When the iteration ends, signaled by the method StopClock(), the average loop time and the related properties are updated. The values obtained here are more accurate than the ones calculated on the fly.
Its parent, LoopTimer class, is in charge of creating and storing references of Loop instances, start and stop clocks and calculate the global ETA. The method EnterLoop() is used at the beginning of the for loop. It creates a new Loop. The overload EnterLoop() with a single parameter is used in the rest of iterations to retrieve Loop instances stored in a Dictionary. The method ExitLoop(), at the end of the loop, stops the clock and updates calculations.
Functions ExitLoopRetETA() and ExitLoopRetTotalEta() are provided as replacements of ExitLoop(), to print data at the end of the loop, for testing or debugging. Method Bonus() shows how to use it. Method ShowStatus() and function GetStatus provide internal information on the Loop objects.
To show the values periodically, a Task is used, to run DoUpdate() (or DoUpdateTotal() or DoUpdateStatus())in a separate thread.
The target framework is .Net 4.0
The working classes:
using System;
using System.Collections.Generic;
using System.Diagnostics;
namespace LoopTimer
{
public struct TotalEta
{
public TimeSpan Eta;
public TimeSpan Total;
}
internal class LoopTimer
{
// Private helper class
private class Loop
{
// Declarations
private Stopwatch _clock;
private Stopwatch _interval_clock;
// Constructor
public Loop(int index, int count)
{
Index = index;
Count = count;
_clock = new Stopwatch();
_interval_clock = new Stopwatch();
}
// Properties
internal int Index { get; set; }
internal int Count { get; private set; }
private double _loopTimeMs;
internal double LoopTimeMs
{
get
{
if (_clock.IsRunning)
{
return _interval_clock.Elapsed.TotalMilliseconds;
}
else
return _loopTimeMs; ;
}
}
private double _remainingTimeMs;
internal double RemainingTimeMs
{
get
{
if (_clock.IsRunning)
return CalculateRemainingTime();
else
return _remainingTimeMs;
}
}
private double _totalTimeMs;
internal double TotalTimeMs
{
get
{
if (_clock.IsRunning)
return CalculateTotalTime();
else
return _totalTimeMs;
}
}
internal TimeSpan LoopTime
{
get { return TimeSpan.FromMilliseconds(LoopTimeMs); }
}
internal TimeSpan TotalTime
{
get { return TimeSpan.FromMilliseconds(TotalTimeMs); }
}
internal TimeSpan RemainingTime
{
get { return TimeSpan.FromMilliseconds(RemainingTimeMs); }
}
// Methods
internal void StartClock()
{
_clock.Start();
_interval_clock.Start();
}
internal void StopClock()
{
_clock.Stop();
_interval_clock.Stop();
UpdateTimes();
_interval_clock.Reset();
}
private void UpdateTimes()
{
// reading clock
double elapsed = _clock.Elapsed.TotalMilliseconds;
// Calculating average loop time. The Stopwatch is not reseted between iterations.
_loopTimeMs = elapsed / (Index + 1);
// Calculating estimated remaining time = average loop time * remaining iterations.
_remainingTimeMs = CalculateRemainingTime();
// Calculating estimated total time = average loop time * iterations.
_totalTimeMs = CalculateTotalTime();
}
private double CalculateRemainingTime()
{
// Calculating estimated remaining time = average loop time * remaining iterations.
double time;
int countt = Count - Index;
if (countt > 1)
time = LoopTimeMs * countt;
else if (countt == 1)
time = LoopTimeMs;
else
time = 0;
return time;
}
private double CalculateTotalTime()
{
return LoopTimeMs * Count;
}
}
// End Private helper class
// Declarations
private Dictionary<int, Loop> _loopDict;
private int _loopIndex;
// Constructor
public LoopTimer()
{
_loopDict = new Dictionary<int, Loop>();
_loopIndex = -1;
}
// Properties
internal TimeSpan TotalTime
{
get { return TimeSpan.FromMilliseconds(TotalTimeMs); }
}
internal TimeSpan RemainingTime
{
get { return TimeSpan.FromMilliseconds(RemainingTimeMs); }
}
private double TotalTimeMs
{ get { return CalculateTotalTime(); } }
private double RemainingTimeMs
{ get { return CalculateRemainingTime(); } }
// Methods
internal void EnterLoop(int index, int count)
{
Loop newLoop;
// increase index
_loopIndex++;
if (!_loopDict.ContainsKey(_loopIndex))
{
// create new Loop
newLoop = new Loop(index, count);
_loopDict[_loopIndex] = newLoop;
}
else
{ // retrieve Loop from Dict
newLoop = _loopDict[_loopIndex];
}
newLoop.StartClock();
}
internal void EnterLoop(int index)
{
// increase index
_loopIndex++;
// retrive loop & start clock
_loopDict[_loopIndex].Index = index;
_loopDict[_loopIndex].StartClock();
}
internal void ExitLoop()
{ // retrive loop & stop clock
_loopDict[_loopIndex].StopClock();
// decrease index
_loopIndex--;
}
// bonus method
internal TimeSpan ExitLoopRetETA()
{ // retrive loop & stop clock
_loopDict[_loopIndex].StopClock();
// decrease index
_loopIndex--;
return RemainingTime;
}
// bonus method
internal TotalEta ExitLoopRetTotalEta()
{
TotalEta retval;
retval.Eta = ExitLoopRetETA();
retval.Total = TotalTime;
return retval;
}
// debug method
internal void ShowStatus()
{
Console.WriteLine("Status:");
Console.WriteLine(" RemainingTime:");
for (int i = 0; i < _loopDict.Count; i++)
{
TimeSpan time = _loopDict[i].RemainingTime;
Console.WriteLine(string.Format(" Loop: {0} Value: {1}", i, time.ToString()));
}
Console.WriteLine();
}
// debug method
internal TotalEta[] GetStatus()
{
TotalEta[] retArr = new TotalEta[_loopDict.Count];
TotalEta retval;
for (int i = 0; i < _loopDict.Count; i++)
{
retval = new TotalEta();
retval.Eta = _loopDict[i].RemainingTime;
retval.Total = _loopDict[i].TotalTime;
retArr[i] = retval;
}
return retArr;
}
private double CalculateRemainingTime()
{
double max, time;
max = 0;
// Remaining Time, the greater of all
for (int i = 0; i < _loopDict.Count; i++)
{
time = _loopDict[i].RemainingTimeMs;
if (time > max)
max = time;
}
return max;
}
// Total Time, bonus
private double CalculateTotalTime()
{
double max, time;
max = 0;
// Total Time, the greater of all
for (int i = 0; i < _loopDict.Count; i++)
{
time = _loopDict[i].TotalTimeMs;
if (time > max)
max = time;
}
return max;
}
}
}
The sample program:
using System;
using System.Threading;
using System.Threading.Tasks;
namespace LoopTimer
{
class Solution
{
static CancellationTokenSource ts;
static void Main(string[] args)
{
Console.Clear();
LoopTimer lm = new LoopTimer();
var random = new Random();
// For easy change test parameters
int minRndCount = 1;
int maxRndCount = 10;
int minRndSleep = 100;
int maxRndSleep = 1000;
// A task to update console, with cancellation token
ts = new CancellationTokenSource();
Task updater = new Task(() => DoUpdate(lm), ts.Token);
// Uncomment to show estimated total time.
//Task updater = new Task(() => DoUpdateTotal(lm), ts.Token);
// Uncomment to show estimated total time and internal values of every loop.
//Task updater = new Task(() => DoUpdateStatus(lm), ts.Token);
// Simulate initiation delay
Thread.Sleep(random.Next(minRndSleep, maxRndSleep));
// Console.WriteLine("initiate");
updater.Start();
int intCountL1 = random.Next(minRndCount, maxRndCount);
for (int loop1 = 0; loop1 <= intCountL1; loop1++)
{
// Entering loop1
if (loop1 == 0)
lm.EnterLoop(loop1, intCountL1);
else
lm.EnterLoop(loop1);
// Simulate loop1 delay
//Console.WriteLine("\tloop1");
Thread.Sleep(random.Next(minRndSleep, maxRndSleep));
int intCountL2 = random.Next(minRndCount, maxRndCount);
for (int loop2 = 0; loop2 <= intCountL2; loop2++)
{
// Entering loop2
if (loop2 == 0)
lm.EnterLoop(loop2, intCountL2);
else
lm.EnterLoop(loop2);
// Simulate loop2 delay
//Console.WriteLine("\t\tloop2");
Thread.Sleep(random.Next(minRndSleep, maxRndSleep));
int intCountL3 = random.Next(minRndCount, maxRndCount);
for (int loop3 = 0; loop3 <= intCountL3; loop3++)
{
// Entering loop3
if (loop3 == 0)
lm.EnterLoop(loop3, intCountL3);
else
lm.EnterLoop(loop3);
// Simulate loop3 delay
//Console.WriteLine("\t\t\tloop3");
Thread.Sleep(random.Next(minRndSleep, maxRndSleep));
int intCountL4 = random.Next(minRndCount, maxRndCount);
for (int loop4 = 0; loop4 <= intCountL4; loop4++)
{
// Entering loop4
if (loop4 == 0)
lm.EnterLoop(loop4, intCountL4);
else
lm.EnterLoop(loop4);
// Simulate loop4 delay
//Console.WriteLine("\t\t\t\tloop4");
Thread.Sleep(random.Next(minRndSleep, maxRndSleep));
// Exiting loop4
lm.ExitLoop();
}
// Exiting loop3
lm.ExitLoop();
}
// Exiting loop2
lm.ExitLoop();
}
// Exiting loop1
lm.ExitLoop();
}
ts.Cancel();
}
static private void DoUpdate(LoopTimer lm)
{
char[] animchar = { '|', '/', '-', '\\' };
int index = 0;
Thread.Sleep(100);
while (true)
{
TimeSpan eta = lm.RemainingTime;
Console.SetCursorPosition(0, 0);
Console.Write(string.Format(" {4} ETA: {0} Days, {1} Hours, {2} Minutes, {3} Seconds", eta.Days.ToString().PadLeft(3, '0'), eta.Hours.ToString().PadLeft(2, '0'), eta.Minutes.ToString().PadLeft(2, '0'), eta.Seconds.ToString().PadLeft(2, '0'), animchar[index].ToString()));
if (++index > 3)
index = 0;
Thread.Sleep(1000);
ts.Token.ThrowIfCancellationRequested();
}
}
/*
This method is provided as a sample on displaying the estimated total time.
*/
static private void DoUpdateTotal(LoopTimer lm)
{
char[] animchar = { '|', '/', '-', '\\' };
int index = 0;
Thread.Sleep(100);
while (true)
{
TimeSpan eta = lm.RemainingTime;
TimeSpan total = lm.TotalTime;
Console.SetCursorPosition(0, 0);
Console.Write(string.Format(" {4} ETA: {0} Days, {1} Hours, {2} Minutes, {3} Seconds", eta.Days.ToString().PadLeft(3, '0'), eta.Hours.ToString().PadLeft(2, '0'), eta.Minutes.ToString().PadLeft(2, '0'), eta.Seconds.ToString().PadLeft(2, '0'), animchar[index].ToString()));
Console.Write(string.Format("\n Total: {0} Days, {1} Hours, {2} Minutes, {3} Seconds", total.Days.ToString().PadLeft(3, '0'), total.Hours.ToString().PadLeft(2, '0'), total.Minutes.ToString().PadLeft(2, '0'), total.Seconds.ToString().PadLeft(2, '0')));
if (++index > 3)
index = 0;
Thread.Sleep(1000);
ts.Token.ThrowIfCancellationRequested();
}
}
/*
This method is provided as a sample on displaying the estimated total time, and
the internal values of every loop.
*/
static private void DoUpdateStatus(LoopTimer lm)
{
char[] animchar = { '|', '/', '-', '\\' };
int index = 0;
Thread.Sleep(100);
while (true)
{
TimeSpan eta = lm.RemainingTime;
TimeSpan total = lm.TotalTime;
TotalEta[] status = lm.GetStatus();
Console.SetCursorPosition(0, 0);
Console.Write(string.Format(" {4} ETA: {0} Days, {1} Hours, {2} Minutes, {3} Seconds", eta.Days.ToString().PadLeft(3, '0'), eta.Hours.ToString().PadLeft(2, '0'), eta.Minutes.ToString().PadLeft(2, '0'), eta.Seconds.ToString().PadLeft(2, '0'), animchar[index].ToString()));
Console.Write(string.Format("\n Total: {0} Days, {1} Hours, {2} Minutes, {3} Seconds", total.Days.ToString().PadLeft(3, '0'), total.Hours.ToString().PadLeft(2, '0'), total.Minutes.ToString().PadLeft(2, '0'), total.Seconds.ToString().PadLeft(2, '0')));
Console.WriteLine();
int loop = 0;
foreach (var item in status)
{
Console.Write(string.Format("\n Loop: {0} ETA: {1} \tTotal: {2}", loop, item.Eta.ToString(@"hh\:mm\:ss\.FFFF"), item.Total.ToString(@"hh\:mm\:ss\.FFFF")));
loop++;
}
if (++index > 3)
index = 0;
Thread.Sleep(1000);
ts.Token.ThrowIfCancellationRequested();
}
}
/*
This method is provided as a sample for variations on
the ExitLoopRet method. Uses in-place calls.
*/
static internal void Bonus()
{
TotalEta remVal;
TimeSpan remTime;
LoopTimer lm = new LoopTimer();
Console.Clear();
// easy change test parameters
var random = new Random();
int minRndCount = 1;
int maxRndCount = 5;
int maxRndSleep = 1000;
// First, outer loop
int intCountL1 = random.Next(minRndCount, maxRndCount);
for (int i = 0; i < intCountL1; i++)
{
if (i == 0)
lm.EnterLoop(i, intCountL1);
else
lm.EnterLoop(i);
Console.WriteLine(string.Format("\nLoop1 begin iteration: {0} of {1}. Will work(sleep) for {2} ms.", i, intCountL1 - 1, maxRndSleep));
Thread.Sleep(maxRndSleep);
// Second, middle loop
int intCountL2 = random.Next(minRndCount, maxRndCount);
for (int j = 0; j < intCountL2; j++)
{
if (j == 0)
lm.EnterLoop(j, intCountL2);
else
lm.EnterLoop(j);
Console.WriteLine(string.Format("\n\tLoop2 begin iteration: {0} of {1}. Will work(sleep) for {2} ms.", j, intCountL2 - 1, maxRndSleep));
Thread.Sleep(maxRndSleep);
// Third, inner loop
int intCountL3 = random.Next(minRndCount, maxRndCount);
for (int k = 0; k < intCountL3; k++)
{
if (k == 0)
lm.EnterLoop(k, intCountL3);
else
lm.EnterLoop(k);
Console.WriteLine(string.Format("\n\t\tLoop3 begin iteration: {0} of {1}. Will work(sleep) for {2} ms.", k, intCountL3 - 1, maxRndSleep));
Thread.Sleep(maxRndSleep);
lm.ExitLoop();
Console.WriteLine(string.Format("\n\t\tLoop3 end iteration: {0} of {1}", k, intCountL3 - 1));
lm.ShowStatus();
}
remTime = lm.ExitLoopRetETA();
Console.WriteLine(string.Format("\n\tLoop2 end iteration: {0} of {1}", j, intCountL2 - 1));
Console.WriteLine("\t\tRem: " + remTime.ToString());
}
remVal = lm.ExitLoopRetTotalEta();
Console.WriteLine(string.Format("\nLoop1 end iteration: {0} of {1}", i, intCountL1 - 1));
Console.WriteLine("\t\tTot: " + remVal.Total.ToString());
Console.WriteLine("\t\tRem: " + remVal.Eta.ToString());
}
}
}
}