Click here to Skip to main content
Click here to Skip to main content

How long is this process going to take?

, 18 Jul 2006
Rate this:
Please Sign up or sign in to vote.
A class for creating a progress control with text and estimated completion time.

Sample Image - Image3.jpg

Introduction

Several years ago, a co-worker was working on a module that involved a progress bar. After he got the prototype up and running and asked for input from the rest of the team, I suggested to him that, for example, even though I know the operation is 25% done, it'd be nice to know about how much longer it was going to take, or when it would finish. Since a lot of operations were very lengthy (e.g., several hours), an estimated completion time, give or take a few minutes, would be a real benefit.

While he agreed with my suggestion, he declined, stating that it would involve too much code. I offered to him what I thought was involved, but it never came to fruition. Oh well, maybe another day...

First Approach

I guess that day has arrived. Back then, it first occurred to me that if I knew how long each % took, I could extrapolate the remaining % by using an average. It sounded good in theory. For example, if the first % took two seconds, then it stood to reason that the remaining 99% would take roughly 198 seconds. If the second % took three seconds, for an average of 2.5 seconds, the remaining 98% would take roughly 245 seconds. After a while, the numbers should start to level out. The problem I began to notice was that it took a long time, if at all, for this "leveling out" to occur. Why?

Averages, as you know, are highly affected by outliers. If one of those %s took 10 seconds to complete, the average suddenly becomes artificially high. Likewise, if one of those %s took less than a second to complete, the average suddenly becomes artificially low. There are a couple of ways to deal with this.

One way to deal with outliers is to not use them. That, however, requires the list of times to be sorted so that we could then lop off a determinate number of low and high times. This much sorting was not desirable. :(

The other way of dealing with outliers is to use a FIFO approach, or let the oldest times fall off the end, and let the newest times enter on the other end. Think of it as keeping a sliding window of most recent times. This approach had merit, but the question comes up of how big this sliding window should be (i.e., how many samples to retain).

For this exercise, I copied a 43.9 MB file over the network, 80 KB at a time. The sample sizes I used were 50, 75, 100, 150, and ∞. The following chart shows the result:

The smaller sample sizes (e.g., dark blue) are just all over the place. This means that the completion time will keep adjusting itself right up to the very end, which is not very helpful. With the larger numbers (e.g., cyan), the lines slowly started leveling out, but they were still far from useful. It wasn't until I averaged all of the times together (e.g., magenta) that a "smooth" line was revealed. That produced a fairly accurate completion time, but there was one major drawback: it was very inefficient. Each time an 80 KB chunk of the file was copied, another time value was added to the list. This list was then averaged 563 times! The loop to do this averaging was having to process 158,766 time values. Imagine how bad this would be for a file 2-3 times larger.

Each time the progress bar was updated, the following code was executed:

double dAverage = 0.0;
int    nLower,
       nUpper;

COleDateTime timeCurrent = COleDateTime::GetCurrentTime();

// if our list is full, remove the oldest item
if ((UINT) m_listTimes.GetCount() == m_uSampleSize)
    m_listTimes.RemoveHead();

m_listTimes.AddTail(timeCurrent);

// if we have at least two, average them
if (m_listTimes.GetCount() > 1)
{
    POSITION pos = m_listTimes.GetHeadPosition();
    double dSum = 0.0;
        
    for (int nIndex = 0; nIndex < m_listTimes.GetCount() - 1; nIndex++)
    {
        COleDateTime time1 = m_listTimes.GetNext(pos);
        COleDateTime time2 = m_listTimes.GetAt(pos);
    
        dSum += (time2 - time1);
    }

    dAverage = dSum / (double) m_listTimes.GetCount();  
}

int nPos = GetPos();
GetRange(nLower, nUpper);

// assert that the current position is between the lower and upper limits
ASSERT(nLower <= nPos);
ASSERT(nPos <= nUpper);

// what % (0-100) are we through?
UINT uPercent = (UINT) ((double) nPos / (double) nUpper * 100.0);
    
// now spread that average out over the remaining percent, and
// add the result to the current time
m_timeEstimated = timeCurrent.m_dt + (dAverage * (100U - uPercent));

Surely, a better way exists.

Second Approach

After looking a bit closer at the math, I realized that a better solution was to simply divide the elapsed time by the % done. The result would be the estimated completion time. By adding this value to the operation's start time, we knew approximately when the operation would finish.

Copying the same file as before, the results look vastly different.

While the starting numbers are still a bit rough up front, they quickly level out and remain so for the duration of the operation. This equates to the estimated completion time being fairly accurate very early on.

The changed code looks like:

int     nLower,
        nUpper;

COleDateTime timeCurrent = COleDateTime::GetCurrentTime();

// save starting time if we don't have one
if (m_timeStart.GetStatus() == COleDateTime::invalid)
    m_timeStart = timeCurrent;

// how much time has elapsed?
COleDateTimeSpan timeElapsed = timeCurrent - m_timeStart;

int nPos = GetPos();
GetRange(nLower, nUpper);

// assert that the current position
// is between the lower and upper limits
ASSERT(nLower <= nPos);
ASSERT(nPos <= nUpper);

// what % are we done
double dPercent = (double) nPos / (double) nUpper;

// based on how long it has taken so far,
// how long will it take altogether
double dEstTotalTime = timeElapsed.m_span / dPercent;

// add the estimated completion time to the operation's start time
m_timeEstimated = m_timeStart.m_dt + dEstTotalTime;
    
// subtract the elapsed time from the estimated completion time
m_timeRemaining = dEstTotalTime - timeElapsed.m_span;

The result of these calculations is the CEstProgressCtrl class. While it could be used as a direct replacement for any CProgressCtrl object, it currently has no support for the "block" style.

The CEstProgressCtrl Class

CEstProgressCtrl Construct a CEstProgressCtrl object.
GetCompletionTime Get the control's estimated completion time.
GetRemainingTime Get the control's estimated remaining time.
SetProgressText Set the control's text to display in the progress area.

bool GetCompletionTime( COleDateTime &timeEstimated ) const

Return Value

  • true if the estimated completion time is valid;
  • false otherwise.

Parameters

  • timeCompletion
  • Reference to a COleDateTime object that will receive the estimated completion time.

bool GetRemainingTime( COleDateTime &timeRemaining ) const

Return Value

  • true if the estimated remaining time is valid;
  • false otherwise.

Parameters

  • timeRemaining
  • Reference to a COleDateTime object that will receive the estimated remaining time.

virtual CString SetProgressText( void ) const

Return Value

A CString object representing the text to be displayed in the control. Along with showing the percent complete, the estimated completion time is also displayed. This time's format (e.g., hh:mm) comes from whatever the system's current time format is.

Remarks

This virtual method has access to the three member variables that represent the estimated completion time, the estimated remaining time, and the percent complete value. Override this method to customize the text displayed in the control.

Rendering the Text

Since we are deriving from CProgressCtrl and drawing on the control in the OnPaint() method, we are in charge of the control's background and foreground colors, as well as its font.

In the OnPaint() method, the first thing we need to do is get the size of the control's client area. This is a simple call to GetClientRect():

// get this control's size
CRect rc;
GetClientRect(rc);

Our OnPaint() method is going to be called by the framework each time Windows needs to repaint a portion of the control. This is in contrast to us having to call some 'calculate' function each time we deem it necessary (e.g., when SetPos() is called). Therefore, to keep one repaint from obscuring another, we need to start with a clean slate each time:

// erase what's in the control
CPaintDC dc(this);
COLORREF crErase(GetSysColor(COLOR_3DFACE));
dc.FillSolidRect(rc, crErase);

I chose to use FillSolidRect() instead of FillRect() since it is a bit faster when dealing with a solid palette. Now we can render the correct amount of the progress bar depending on the percent complete:

// render the right amount of completeness
CRect rcPct(rc);
rcPct.right = (LONG) ((double) rcPct.right * m_dPctComplete);
dc.FillSolidRect(rcPct, crBackground);

At this point, the control behaves like normal (i.e., solid bar, no text). The next few code snippets deal with rendering text on the control in a visually-appealing and theme-aware manner. The first thing we'll need is an in-memory DC:

// create an in-memory DC
CDC dcText;
dcText.CreateCompatibleDC(&dc);

Before we can use this DC, we must create and select a bitmap of the correct width, height, and color into it:

CBitmap bmpText;
bmpText.CreateCompatibleBitmap(&dc, rc.Width(), rc.Height());
CBitmap *pOldBitmap = dcText.SelectObject(&bmpText);

Rather than set the DC's font to some arbitrary value, that may be different from how the system has been configured, we will use the control's current font:

// select the control's font into the DC
CFont *pFont = GetFont();
CFont *pOldFont = dcText.SelectObject(pFont);

We'll need to set the background to be transparent, otherwise the background color behind the text will be undesirable. The color we selected for the text looks quite odd. An explanation is in order. The effect we want is for the text's color to be one thing when the progress bar and the text have mixed, and another color when they have not. The benefit to this is that the text is not obscured by whatever color is being used by the progress bar (e.g., blue on blue). See the picture at the top of this article for an example. The text in the completed area is white (on blue). The text in the non-completed area is blue (on gray). The colors in parenthesis represent the transparent effect. If your computer happens to have a different theme than the default blue one, the correct colors will still be used.

To get 'blue' text, whose RGB value is 0x00316AC5, we must flip the bits using the one's complement operator. This yields 0xFFCE953A. And, since the SetTextColor() method is not interested in the most significant 8-bits, we'll just mask those out. This gives us a final RGB value of 0x003A95CE. Contrary to what MSDN states, as best as I can tell, GetSysColor() returns a COLORREF value instead of an RGB value. This doesn't necessarily affect anything, other than the occasional need to switch back and forth between the two formats.

// text starts out 'blue' on 'gray'
// as progress bar advances, text changes to 'white' on 'blue'
dcText.SetBkMode(TRANSPARENT);
dcText.SetTextColor((~GetSysColor(COLOR_HIGHLIGHT)) & 0x00ffffff);

At this point, we can render the text, centered both vertically and horizontally, using DrawText():

dcText.DrawText(str, rc, DT_VCENTER | DT_SINGLELINE | DT_CENTER);

That's it. We now have a progress bar, very similar to the default one, that also shows the estimated completion time and how much time is remaining. Enjoy!

Similar Implementations

Indeed there is more than one way to skin a cat:

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

DavidCrow
Software Developer (Senior) Pinnacle Business Systems
United States United States

The page you are looking for might have been removed, had its name changed, or is temporarily unavailable.
 
HTTP 404 - File not found
Internet Information Services

Comments and Discussions

 
Generalinteresting PinmemberChris Losinger2-Sep-07 9:39 
QuestionA small problem Pinmembermc_cappy20-Nov-06 5:27 
QuestionRe: A small problem PinmemberDavidCrow20-Nov-06 6:05 
AnswerRe: A small problem Pinmembermc_cappy24-Nov-06 6:56 
GeneralAveraging... PinmemberMJessick19-Jul-06 10:21 
If you have previously processed 100,000 data points for a mean, and want to add another data point to the mean, you do not need to maintain all the old data to perform this update. See any statistics text.
 
- Matt Jessick
QuestionRe: Averaging... PinmemberDavidCrow19-Jul-06 11:07 
AnswerRe: Averaging... PinmemberPaul Hooper19-Jul-06 15:29 
GeneralRe: Averaging... PinmemberDavidCrow20-Jul-06 3:12 
GeneralCool, it's like mine :) PinmemberKochise18-Jul-06 21:15 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web01 | 2.8.140721.1 | Last Updated 18 Jul 2006
Article Copyright 2006 by DavidCrow
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid