I am trying to add to my application interface a "Transfer Rate : XGb p\min" feature. I am computing it at a millisecond level using Lazarus 1.2.2 and Freepascal 2.6.4.
I have a loop that reads 64Kb blocks from a disk, does stuff with each 64Kb block, and repeats until the whole disk is read or until the user clicks an abort button. I am trying to time how long each read of 64Kb takes and then compute the average rate of data read per minute, i.e. "3Gb per minute".
I have a custom function called 'GetTimeInMilliseconds' that computes the standard time to milliseconds.
Also, to avoid the interface being refreshed ever partial millisecond, I have a counter that tries to ensure the interface is only refreshed every 50 times round the loop, i.e. when 64Kb chunks have been read 50 times, then the counter is reset to 0.
The trouble is, the transfer display is either not getting populated or it gets populated with an inaccurate figure like "234Mb p\min" for a RAID0 device! At other times, it gets something more realistic like "3.4Gb p\min". It should be consistently accurate if ran repeatedly on the same PC and same disk, not inconsistantly inaccurate.
Here is my Try...finally loop that does the looping. It also called FormatByteSize which is a custom function that computers an integer size and converts it into XMb, XGb, XTb etc.
var
ProgressCounter, BytesTransferred, BytesPerSecond : integer;
Buffer : array [0..65535] of Byte; // 65536, 64Kb buffer
ctx : TSHA1Context;
Digest : TSHA1Digest;
NewPos, ExactDiskSize, SectorCount, TimeStartRead, TimeEndRead,
MillisecondsElapsed, BytesPerMillisecond, BytesPerMinute : Int64;
StartTime, EndTime, TimeTaken : TDateTime;
...
begin
... // Various stuff related to handles on disks etc
try
SHA1Init(ctx);
FileSeek(hSelectedDisk, 0, 0);
repeat
ProgressCounter := ProgressCounter + 1; // We use this update the progress display occasionally, instead of every buffer read
TimeStartRead := GetTimeInMilliSeconds(Time); // Starting time, in Ms
// The hashing bit...
FileRead(hSelectedDisk, Buffer, 65536); // Read 65536 = 64kb at a time
NewPos := NewPos + SizeOf(Buffer);
SHA1Update(ctx, Buffer, SizeOf(Buffer));
FileSeek(hSelectedDisk, NewPos, 0);
lblBytesLeftToHashB.Caption := IntToStr(ExactDiskSize - NewPos) + ' bytes, ' + FormatByteSize(ExactDiskSize - NewPos);
// End of the hashing bit...
TimeEndRead := GetTimeInMilliSeconds(Time);; // End time in Ms
MillisecondsElapsed := (TimeEndRead - TimeStartRead); // Record how many Ms's have elapsed
// Only if the loop has gone round a while (50 times?), update the progress display
if ProgressCounter = 50 then
begin
if (TimeStartRead > 0) and (TimeEndRead > 0) and (MillisecondsElapsed > 0) then // Only do the divisions and computations if all timings have computed to a positive number
begin
BytesTransferred := SizeOf(Buffer);
BytesPerMillisecond := BytesTransferred DIV MilliSecondsElapsed; // BytesPerMillisecond if often reported as zero, even though BytesTRansferred and MilliSecondsElapsed are valid numbers? Maybe this is the fault?
BytesPerSecond := BytesPerMillisecond * 1000; // Convert the bytes read per millisecond into bytes read per second
BytesPerMinute := BytesPerSecond * 60; // Convert the bytes read per second into bytes read per minute
lblSpeedB.Caption := FormatByteSize(BytesPerMinute) + ' p\min'; // now convert the large "bytes per minute" figure into Mb, Gb or Tb
// Reset the progress counters to zero for another chunk of looping
ProgressCounter := 0;
BytesPerMillisecond := 0;
BytesPerSecond := 0;
BytesPerMinute := 0;
ProgressCounter := 0;
end;
end;
Application.ProcessMessages;
until (NewPos >= ExactDiskSize) or (Stop = true); // Stop looping over the disk
// Compute the final hash value
SHA1Final(ctx, Digest);
lblBytesLeftToHashB.Caption:= '0';
finally
// The handle may have been released by pressing stop. If not, the handle will still be active so lets close it.
if not hSelectedDisk = INVALID_HANDLE_VALUE then CloseHandle(hSelectedDisk);
EndTime := Now;
TimeTaken := EndTime - StartTime;
lblEndTimeB.Caption := FormatDateTime('dd/mm/yy hh:mm:ss', EndTime);
if not stop then edtComputedHash.Text := SHA1Print(Digest);
Application.ProcessMessages;
...
Function GetTimeInMilliSeconds(theTime : TTime): Int64;
// http://www.delphipages.com/forum/archive/index.php/t-135103.html
var
Hour, Min, Sec, MSec: Word;
begin
DecodeTime(theTime,Hour, Min, Sec, MSec);
Result := (Hour * 3600000) + (Min * 60000) + (Sec * 1000) + MSec;
end;
Example:
Form1.Caption := IntToStr( GetTimeInMilliSeconds(Time) );
I feel that you've made this much more complicated than it needs to be.
Move your file processing code into a separate thread. Within that code, as you finish processing bytes from the file, use InterlockedAdd
to keep a count of the total number of bytes processed.
In your GUI thread, using a timer of some sort, use InterlockedExchange
to read the value that InterlockedAdd
has been modifying and reset it to zero. Then Calculate the amount of time that has passed since the last timer tick. Given the total number of bytes processed within the elapsed time, calculate the bytes per minute and update your GUI.
Since the file operation is on a separate thread, you will not need to worry about how often you update the GUI (although doing so too frequently will be a waste of time).
If you want to calculate the average bytes per minute over the entire operation, don't reset the total byte counter at each tick.