Only a couple of days ago I posted about creating a file downloader cmdlet in Windows PowerShell which contained the following little sentence:
One could make this method more complex in order to provide a seconds remaining estimate based on the download speed observed.
One of my readers (dotnetjunkie) was so kind to leave a piece of feedback:
I think you should add transfer speed and estimated remaining time, that would make it even more useful and cooler ! ;)
Well, I couldn't agree more. So I revisited my piece of code I wrote some weeks ago (12th of November to be precise) and added download speed tracking and a seconds remaining indicator.
There are undoubtly different ways to implement such an estimate. My approach is to measure the number of bytes transferred during intervals of approximately 5 seconds (kind of "instant download speed") and to derive the estimated time remaining from this. I'll discuss the code changes in a few steps.
Step 1 - Add a few private members
We need 4 additional members to produce statistics:
- The first one is a Stopwatch to measure elapsed time. We'll poll this counter every time the DownloadProgressChanged event is fired. In case it's above 5 seconds, we update the statistics and restart the counter.
- Secondly, we'll cache an indicator that keeps the current transfer speed as a string of the format n [bytes|KB|MB|GB|...]/sec. This value will be updated every 5 seconds.
- Next, the seconds remaining are kept. It's updated every 5 seconds and during these intervals it just counts down every one second.
- Finally, a transferred bytes indicator is used for calculation of the bytes transferred the last 5 seconds.
Here's the piece of code:
/// <summary>
/// Stopwatch used to measure download speed.
/// </summary>
private Stopwatch sw = new Stopwatch();
/// <summary>
/// Bytes per second indicator (bytes/sec, KB/sec, MB/sec, ...).
/// </summary>
private string bps = null;
/// <summary>
/// Seconds remaining indicator.
/// </summary>
private int secondsRemaining = -1;
/// <summary>
/// Number of bytes already transferred.
/// </summary>
private long transferred = 0;
Step 2 - Let the count begin
In the ProcessRecord method we start our Stopwatch; just that:
//
// Check validity for download. Will throw an exception in case of transport protocol errors.
//
using (clnt.OpenRead(_url)) { }
//
// Start download speed stopwatch.
//
sw.Start();
//
// Download the file asynchronously. Reporting will happen through events on background threads.
//
clnt.DownloadFileAsync(_url, _file);
Step 3 - Calculate stats and report progress
Time for the real stuff. On to the DownloadProgressChanged event handler. When we observe that the Stopwatch has an elapsed time of 5 or more seconds, we'll stop it, update stats and restart it. The code is shown below:
1 /// <summary>
2 /// Reports download progress.
3 /// </summary>
4 private void webClient_DownloadProgressChanged(object sender, DownloadProgressChangedEventArgs e)
5 {
6 //
7 // Update statistics every 5 seconds (approx).
8 //
9 if (sw.Elapsed >= TimeSpan.FromSeconds(5))
10 {
11 sw.Stop();
12
13 //
14 // Calculcate transfer speed.
15 //
16 long bytes = e.BytesReceived - transferred;
17 double bps = bytes * 1000.0 / sw.Elapsed.TotalMilliseconds;
18 this.bps = BpsToString(bps);
19
20 //
21 // Estimated seconds remaining based on the current transfer speed.
22 //
23 secondsRemaining = (int)((e.TotalBytesToReceive - e.BytesReceived) / bps);
24
25 //
26 // Restart stopwatch for next 5 seconds.
27 //
28 transferred = e.BytesReceived;
29 sw.Reset();
30 sw.Start();
31 }
32
33 //
34 // Construct a ProgressRecord with download state information but no completion time estimate (SecondsRemaining < 0).
35 //
36 ProgressRecord pr = new ProgressRecord(0, String.Format("Downloading {0}", _url.ToString(), _file), String.Format("{0} of {1} bytes transferred{2}.", e.BytesReceived, e.TotalBytesToReceive, this.bps != null ? String.Format(" (@ {0})", this.bps) : ""));
37 pr.CurrentOperation = String.Format("Destination file: {0}", _file);
38 pr.SecondsRemaining = secondsRemaining - (int)sw.Elapsed.Seconds;
39 pr.PercentComplete = e.ProgressPercentage;
40
41 //
42 // Report availability of a ProgressRecord item. Will cause the while-loop's body in ProgressRecord to execute.
43 //
44 lock (pr_sync)
45 {
46 this.pr = pr;
47 prog.Set();
48 }
49 }
So, what's going on here. Basically we want to provide a seconds remaining estimate on line 38 and a download speed estimate on line 36. This should be pretty self-explanatory. The real work happens in lines 11 to 30 where the number of bytes transferred in the last 5 seconds are obtained and divided by the expired milliseconds during the last 5 seconds (which should be around 5000 obviously). The rest is maths, except for the BpsToString call as shown below.
Step 4 - A download speed indicator
BpsToString is the method to convert the bytes per second rate to a friendly string representation:
/// <summary>
/// Constructs a download speed indicator string.
/// </summary>
/// <param name="bps">Bytes per second transfer rate.</param>
/// <returns>String represenation of the transfer rate in bytes/sec, KB/sec, MB/sec, etc.</returns>
private string BpsToString(double bps)
{
string[] m = new string[] { "bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB" }; //dreaming of YB/sec
int i = 0;
while (bps >= 0.9 * 1024)
{
bps /= 1024;
i++;
}
return String.Format("{0:0.00} {1}/sec", bps, m[i]);
}
I think the code fragment above is pretty optimistic for what transfer speeds is concerned, but with the expected life time of PowerShell in mind this should be no luxury :-).
Step 5 - The result & code download
This is the result (needless to say the figures are indicative only, it are estimates after all):
And here's the code download link.
Del.icio.us |
Digg It |
Technorati |
Blinklist |
Furl |
reddit |
DotNetKicks