Updating a progress bar
Almost any change to VCL controls can cause one or more Windows messages to be sent to the operating system. That takes time, especially as the program waits for the operating system to process the message and return an answer. This can happen even if nothing has changed in the user interface.
The next demonstration will show how an abundance of messages can slow down the execution speed. I must admit that the code in the ProgressBar demo is a bit contrived. Still, I assure you that I have seen a similar code running in production.
This demo simulates reading a large file block by block. (The code doesn't really open and read the file; it just runs a loop which would do the reading.) For each read block, a progress bar is updated.
For speed comparison, this progress bar update is done in two ways. In the first, slow approach, the Max property of the progress bar is set to the size of the file we are reading. After each block, the progress bar's Position is set to the number of bytes read so far:
function TfrmProgressBar.Test0To2G: Integer;
var
total: Integer;
block: Integer;
sw: TStopwatch;
begin
sw := TStopwatch.StartNew;
ProgressBar1.Max := CFileSize;
ProgressBar1.Position := 0;
total := 0;
while total < CFileSize do begin
block := CFileSize - total;
if block > 1024 then
block := 1024;
// reading 'block' bytes
Inc(total, block);
ProgressBar1.Position := total;
ProgressBar1.Update;
end;
Result := sw.ElapsedMilliseconds;
end;
This code runs slow for two reasons. Firstly, setting the Position property sends a message PBM_SETPOS to Windows and that is a relatively slow operation when compared with non-graphical program code. Secondly, when we call Update, Windows API function UpdateWindow gets called. This function repaints the progress bar even if its position didn't change and that takes even more time. As all this is called 1,953,125 times it adds up to a considerable overhead.
The second, faster approach, sets Max to 100. After each block, the progress is calculated in percentage with currPct := Round(total / CFileSize * 100);. The Position property is updated only if this percentage differs from the current progress bar's position.
As reading the Position property also sends one message to the system, the current position is stored in a local variable, lastPct, and a new value is compared to it:
function TfrmProgressBar.Test0To100: Integer;
var
total: Integer;
block: Integer;
sw: TStopwatch;
lastPct: Integer;
currPct: Integer;
begin
sw := TStopwatch.StartNew;
ProgressBar1.Max := 100;
ProgressBar1.Position := 0;
lastPct := 0;
total := 0;
while total < CFileSize do begin
block := CFileSize - total;
if block > 1024 then
block := 1024;
// reading 'block' bytes
Inc(total, block);
currPct := Round(total / CFileSize * 100);
if currPct > lastPct then
begin
lastPct := currPct;
ProgressBar1.Position := currPct;
ProgressBar1.Update;
end;
end;
Result := sw.ElapsedMilliseconds;
end;
File size is set to 2,000,000,000 bytes, or 1.86 GB. That is a lot, but completely reasonable for a video file or database storage. The block is set to 1024 to amplify the problem. With a more reasonable size of 65536 bytes, the difference is less noticeable.
As you can see, the second example contains more code and is a bit more complex. The result of those few additional lines is, however, more than impressive.
When you start the test program and click on a button, it will first run the slow code followed by the fast code. You will see a noticeable difference in display speed. This is also confirmed by the measurements:
If you ran the demo code, you have probably noticed that the message showing the timing results is displayed before the progress bar finishes its crawl towards the full line. This is caused by a feature introduced in Windows Vista.
In Windows XP and before, updates to the progress bar were immediate. If you set Max to 100, Position to 0 and then a bit later updated Position to 50, the progress bar display would jump immediately to the middle. Since Vista, every change to the progress bar is animated. Changing Position from 0 to 50 results in an animation of the progress bar going from the left side to the middle of the control.
This makes for a nicer display but sometimes—as in our example—causes weird program behavior. As far as I know, there is only one way to work around the problem.
When I said that every change results in animation, I lied a bit. In reality, the animation is only triggered if you increase the position. If you decrease it, the change is displayed immediately.
We can, therefore, use the following trick. Instead of setting the Position to the desired value—ProgressBar1.Position := currPct;—we do that in two steps. Firstly we set it a bit too high (this causes the animation to start) and then we correctly position it to the correct value (this causes an immediate update):
ProgressBar1.Position := currPct+1;
ProgressBar1.Position := currPct;
This leaves us with the problem of forcing the progress bar to display a full line when all processing is done. The simplest way I can find is to decrease the Max property so that it is lower than the current Position. That also causes an immediate update to the progress bar:
ProgressBar1.Position := ProgressBar1.Max;
ProgressBar1.Max := ProgressBar1.Max - 1;
This technique of minimizing the number of changes also applies to multithreaded programs, especially when they want to update the user interface. I will return to it in Chapter 7, Exploring Parallel Practices.