/* Download progress.
- Copyright (C) 2001 Free Software Foundation, Inc.
+ Copyright (C) 2001, 2002 Free Software Foundation, Inc.
This file is part of GNU Wget.
You should have received a copy of the GNU General Public License
along with Wget; if not, write to the Free Software
-Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */
+Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+In addition, as a special exception, the Free Software Foundation
+gives permission to link the code of its release of Wget with the
+OpenSSL project's "OpenSSL" library (or with modified versions of it
+that use the same license as the "OpenSSL" library), and distribute
+the linked executables. You must obey the GNU General Public License
+in all respects for all of the code used other than "OpenSSL". If you
+modify this file, you may extend this exception to your version of the
+file, but you are not obligated to do so. If you do not wish to do
+so, delete this exception statement from your version. */
#include <config.h>
#ifdef HAVE_UNISTD_H
# include <unistd.h>
#endif
+#ifdef HAVE_SIGNAL_H
+# include <signal.h>
+#endif
#include "wget.h"
#include "progress.h"
struct progress_implementation {
char *name;
- void *(*create) (long, long);
- void (*update) (void *, long, long);
- void (*finish) (void *, long);
- void (*set_params) (const char *);
+ void *(*create) PARAMS ((long, long));
+ void (*update) PARAMS ((void *, long, double));
+ void (*finish) PARAMS ((void *, double));
+ void (*set_params) PARAMS ((const char *));
};
/* Necessary forward declarations. */
static void *dot_create PARAMS ((long, long));
-static void dot_update PARAMS ((void *, long, long));
-static void dot_finish PARAMS ((void *, long));
+static void dot_update PARAMS ((void *, long, double));
+static void dot_finish PARAMS ((void *, double));
static void dot_set_params PARAMS ((const char *));
static void *bar_create PARAMS ((long, long));
-static void bar_update PARAMS ((void *, long, long));
-static void bar_finish PARAMS ((void *, long));
+static void bar_update PARAMS ((void *, long, double));
+static void bar_finish PARAMS ((void *, double));
static void bar_set_params PARAMS ((const char *));
static struct progress_implementation implementations[] = {
{ "bar", bar_create, bar_update, bar_finish, bar_set_params }
};
static struct progress_implementation *current_impl;
-int current_impl_locked;
+static int current_impl_locked;
/* Progress implementation used by default. Can be overriden in
wgetrc or by the fallback one. */
char *colon = strchr (name, ':');
int namelen = colon ? colon - name : strlen (name);
- for (i = 0; i < ARRAY_SIZE (implementations); i++, pi++)
+ for (i = 0; i < countof (implementations); i++, pi++)
if (!strncmp (pi->name, name, namelen))
return 1;
return 0;
colon = strchr (name, ':');
namelen = colon ? colon - name : strlen (name);
- for (i = 0; i < ARRAY_SIZE (implementations); i++, pi++)
+ for (i = 0; i < countof (implementations); i++, pi++)
if (!strncmp (pi->name, name, namelen))
{
current_impl = pi;
time in milliseconds since the beginning of the download. */
void
-progress_update (void *progress, long howmuch, long dltime)
+progress_update (void *progress, long howmuch, double dltime)
{
current_impl->update (progress, howmuch, dltime);
}
PROGRESS object, the further use of which is not allowed. */
void
-progress_finish (void *progress, long dltime)
+progress_finish (void *progress, double dltime)
{
current_impl->finish (progress, dltime);
}
int rows; /* number of rows printed so far */
int dots; /* number of dots printed in this row */
- long last_timer_value;
+ double last_timer_value;
};
/* Dot-progress backend for progress_create. */
static void *
dot_create (long initial, long total)
{
- struct dot_progress *dp = xmalloc (sizeof (struct dot_progress));
-
- memset (dp, 0, sizeof (*dp));
-
+ struct dot_progress *dp = xnew0 (struct dot_progress);
dp->initial_length = initial;
dp->total_length = total;
}
static void
-print_download_speed (struct dot_progress *dp, long bytes, long dltime)
+print_download_speed (struct dot_progress *dp, long bytes, double dltime)
{
logprintf (LOG_VERBOSE, " %s",
retr_rate (bytes, dltime - dp->last_timer_value, 1));
/* Dot-progress backend for progress_update. */
static void
-dot_update (void *progress, long howmuch, long dltime)
+dot_update (void *progress, long howmuch, double dltime)
{
struct dot_progress *dp = progress;
int dot_bytes = opt.dot_bytes;
/* Dot-progress backend for progress_finish. */
static void
-dot_finish (void *progress, long dltime)
+dot_finish (void *progress, double dltime)
{
struct dot_progress *dp = progress;
int dot_bytes = opt.dot_bytes;
static int screen_width = DEFAULT_SCREEN_WIDTH;
+/* Size of the download speed history ring. */
+#define DLSPEED_HISTORY_SIZE 20
+
+/* The minimum time length of a history sample. By default, each
+ sample is at least 150ms long, which means that, over the course of
+ 20 samples, "current" download speed spans at least 3s into the
+ past. */
+#define DLSPEED_SAMPLE_MIN 150
+
struct bar_progress {
long initial_length; /* how many bytes have been downloaded
previously. */
download finishes */
long count; /* bytes downloaded so far */
- long last_update; /* time of the last screen update. */
+ double last_screen_update; /* time of the last screen update,
+ measured since the beginning of
+ download. */
- int width; /* screen width at the time the
- progress gauge was created. */
+ int width; /* screen width we're using at the
+ time the progress gauge was
+ created. this is different from
+ the screen_width global variable in
+ that the latter can be changed by a
+ signal. */
char *buffer; /* buffer where the bar "image" is
stored. */
-
- int tick;
+ int tick; /* counter used for drawing the
+ progress bar where the total size
+ is not known. */
+
+ /* The following variables (kept in a struct for namespace reasons)
+ keep track of recent download speeds. See bar_update() for
+ details. */
+ struct bar_progress_hist {
+ int pos;
+ long times[DLSPEED_HISTORY_SIZE];
+ long bytes[DLSPEED_HISTORY_SIZE];
+
+ /* The sum of times and bytes respectively, maintained for
+ efficiency. */
+ long total_time;
+ long total_bytes;
+ } hist;
+
+ double recent_start; /* timestamp of beginning of current
+ position. */
+ long recent_bytes; /* bytes downloaded so far. */
+
+ /* create_image() uses these to make sure that ETA information
+ doesn't flash. */
+ double last_eta_time; /* time of the last update to download
+ speed and ETA, measured since the
+ beginning of download. */
+ long last_eta_value;
};
-static void create_image PARAMS ((struct bar_progress *, long));
+static void create_image PARAMS ((struct bar_progress *, double));
static void display_image PARAMS ((char *));
static void *
bar_create (long initial, long total)
{
- struct bar_progress *bp = xmalloc (sizeof (struct bar_progress));
+ struct bar_progress *bp = xnew0 (struct bar_progress);
- memset (bp, 0, sizeof (*bp));
+ /* In theory, our callers should take care of this pathological
+ case, but it can sometimes happen. */
+ if (initial > total)
+ total = initial;
bp->initial_length = initial;
bp->total_length = total;
- bp->width = screen_width;
+
+ /* - 1 because we don't want to use the last screen column. */
+ bp->width = screen_width - 1;
+ /* + 1 for the terminating zero. */
bp->buffer = xmalloc (bp->width + 1);
logputs (LOG_VERBOSE, "\n");
return bp;
}
+static void update_speed_ring PARAMS ((struct bar_progress *, long, double));
+
static void
-bar_update (void *progress, long howmuch, long dltime)
+bar_update (void *progress, long howmuch, double dltime)
{
struct bar_progress *bp = progress;
- int force_update = 0;
+ int force_screen_update = 0;
bp->count += howmuch;
if (bp->total_length > 0
adjust bp->total_length to the new reality, so that the code in
create_image() that depends on total size being smaller or
equal to the expected size doesn't abort. */
- bp->total_length = bp->count + bp->initial_length;
+ bp->total_length = bp->initial_length + bp->count;
- if (screen_width != bp->width)
+ update_speed_ring (bp, howmuch, dltime);
+
+ if (screen_width - 1 != bp->width)
{
- bp->width = screen_width;
+ bp->width = screen_width - 1;
bp->buffer = xrealloc (bp->buffer, bp->width + 1);
+ force_screen_update = 1;
}
- if (dltime - bp->last_update < 200 && !force_update)
- /* Don't update more often than every half a second. */
+ if (dltime - bp->last_screen_update < 200 && !force_screen_update)
+ /* Don't update more often than five times per second. */
return;
- bp->last_update = dltime;
-
create_image (bp, dltime);
display_image (bp->buffer);
+ bp->last_screen_update = dltime;
}
static void
-bar_finish (void *progress, long dltime)
+bar_finish (void *progress, double dltime)
{
struct bar_progress *bp = progress;
- if (dltime == 0)
- /* If the download was faster than the granularity of the timer,
- fake some output so that we don't get the ugly "----.--" rate
- at the download finish. */
- dltime = 1;
+ if (bp->total_length > 0
+ && bp->count + bp->initial_length > bp->total_length)
+ /* See bar_update() for explanation. */
+ bp->total_length = bp->initial_length + bp->count;
create_image (bp, dltime);
display_image (bp->buffer);
xfree (bp);
}
+/* This code attempts to maintain the notion of a "current" download
+ speed, over the course of no less than 3s. (Shorter intervals
+ produce very erratic results.)
+
+ To do so, it samples the speed in 150ms intervals and stores the
+ recorded samples in a FIFO history ring. The ring stores no more
+ than 20 intervals, hence the history covers the period of at least
+ three seconds and at most 20 reads into the past. This method
+ should produce reasonable results for downloads ranging from very
+ slow to very fast.
+
+ The idea is that for fast downloads, we get the speed over exactly
+ the last three seconds. For slow downloads (where a network read
+ takes more than 150ms to complete), we get the speed over a larger
+ time period, as large as it takes to complete thirty reads. This
+ is good because slow downloads tend to fluctuate more and a
+ 3-second average would be too erratic. */
+
static void
-create_image (struct bar_progress *bp, long dltime)
+update_speed_ring (struct bar_progress *bp, long howmuch, double dltime)
+{
+ struct bar_progress_hist *hist = &bp->hist;
+ double recent_age = dltime - bp->recent_start;
+
+ /* Update the download count. */
+ bp->recent_bytes += howmuch;
+
+ /* For very small time intervals, we return after having updated the
+ "recent" download count. When its age reaches or exceeds minimum
+ sample time, it will be recorded in the history ring. */
+ if (recent_age < DLSPEED_SAMPLE_MIN)
+ return;
+
+ /* Store "recent" bytes and download time to history ring at the
+ position POS. */
+
+ /* To correctly maintain the totals, first invalidate existing data
+ (least recent in time) at this position. */
+ hist->total_time -= hist->times[hist->pos];
+ hist->total_bytes -= hist->bytes[hist->pos];
+
+ /* Now store the new data and update the totals. */
+ hist->times[hist->pos] = recent_age;
+ hist->bytes[hist->pos] = bp->recent_bytes;
+ hist->total_time += recent_age;
+ hist->total_bytes += bp->recent_bytes;
+
+ /* Start a new "recent" period. */
+ bp->recent_start = dltime;
+ bp->recent_bytes = 0;
+
+ /* Advance the current ring position. */
+ if (++hist->pos == DLSPEED_HISTORY_SIZE)
+ hist->pos = 0;
+
+#if 0
+ /* Sledgehammer check to verify that the totals are accurate. */
+ {
+ int i;
+ double sumt = 0, sumb = 0;
+ for (i = 0; i < DLSPEED_HISTORY_SIZE; i++)
+ {
+ sumt += hist->times[i];
+ sumb += hist->bytes[i];
+ }
+ assert (sumt == hist->total_time);
+ assert (sumb == hist->total_bytes);
+ }
+#endif
+}
+
+#define APPEND_LITERAL(s) do { \
+ memcpy (p, s, sizeof (s) - 1); \
+ p += sizeof (s) - 1; \
+} while (0)
+
+#ifndef MAX
+# define MAX(a, b) ((a) >= (b) ? (a) : (b))
+#endif
+
+static void
+create_image (struct bar_progress *bp, double dl_total_time)
{
char *p = bp->buffer;
long size = bp->initial_length + bp->count;
- /* The progress bar should look like this:
- xx% [=======> ] nn.nnn rrK/s ETA 00:00
-
- Calculate its geometry:
+ char *size_legible = legible (size);
+ int size_legible_len = strlen (size_legible);
- "xx% " or "100%" - percentage - 4 chars exactly
- "[]" - progress bar decorations - 2 chars exactly
- " n,nnn,nnn,nnn" - downloaded bytes - 14 or less chars
- " 1012.56K/s" - dl rate - 11 chars exactly
- " ETA xx:xx:xx" - ETA - 13 or less chars
+ struct bar_progress_hist *hist = &bp->hist;
- "=====>..." - progress bar content - the rest
+ /* The progress bar should look like this:
+ xx% [=======> ] nn,nnn 12.34K/s ETA 00:00
+
+ Calculate the geometry. The idea is to assign as much room as
+ possible to the progress bar. The other idea is to never let
+ things "jitter", i.e. pad elements that vary in size so that
+ their variance does not affect the placement of other elements.
+ It would be especially bad for the progress bar to be resized
+ randomly.
+
+ "xx% " or "100%" - percentage - 4 chars
+ "[]" - progress bar decorations - 2 chars
+ " nnn,nnn,nnn" - downloaded bytes - 12 chars or very rarely more
+ " 1012.56K/s" - dl rate - 11 chars
+ " ETA xx:xx:xx" - ETA - 13 chars
+
+ "=====>..." - progress bar - the rest
*/
- int progress_size = screen_width - (4 + 2 + 14 + 11 + 13);
+ int dlbytes_size = 1 + MAX (size_legible_len, 11);
+ int progress_size = bp->width - (4 + 2 + dlbytes_size + 11 + 13);
if (progress_size < 5)
progress_size = 0;
p += 4;
}
else
- {
- *p++ = ' ';
- *p++ = ' ';
- *p++ = ' ';
- *p++ = ' ';
- }
+ APPEND_LITERAL (" ");
- /* The progress bar: "[====> ]" */
+ /* The progress bar: "[====> ]" or "[++==> ]". */
if (progress_size && bp->total_length > 0)
{
- double fraction = (double)size / bp->total_length;
- int dlsz = (int)(fraction * progress_size);
+ /* Size of the initial portion. */
+ int insz = (double)bp->initial_length / bp->total_length * progress_size;
+
+ /* Size of the downloaded portion. */
+ int dlsz = (double)size / bp->total_length * progress_size;
+
char *begin;
+ int i;
assert (dlsz <= progress_size);
+ assert (insz <= dlsz);
*p++ = '[';
begin = p;
+ /* Print the initial portion of the download with '+' chars, the
+ rest with '=' and one '>'. */
+ for (i = 0; i < insz; i++)
+ *p++ = '+';
+ dlsz -= insz;
if (dlsz > 0)
{
- /* Draw dlsz-1 '=' chars and one arrow char. */
- while (dlsz-- > 1)
+ for (i = 0; i < dlsz - 1; i++)
*p++ = '=';
*p++ = '>';
}
while (p - begin < progress_size)
*p++ = ' ';
-
*p++ = ']';
}
else if (progress_size)
++bp->tick;
}
- /* " 1,234,567" */
- /* If there are 7 or less digits (9 because of "legible" comas),
- print the number in constant space. This will prevent the rest
- of the line jerking at the beginning of download, but without
- assigning maximum width in all cases. */
- sprintf (p, " %9s", legible (size));
+ /* " 234,567,890" */
+ sprintf (p, " %-11s", legible (size));
p += strlen (p);
/* " 1012.45K/s" */
- if (dltime && bp->count)
+ if (hist->total_time && hist->total_bytes)
{
static char *short_units[] = { "B/s", "K/s", "M/s", "G/s" };
int units = 0;
- double dlrate = calc_rate (bp->count, dltime, &units);
- sprintf (p, " %7.2f%s", dlrate, short_units[units]);
+ /* Calculate the download speed using the history ring and
+ recent data that hasn't made it to the ring yet. */
+ long dlquant = hist->total_bytes + bp->recent_bytes;
+ double dltime = hist->total_time + (dl_total_time - bp->recent_start);
+ double dlspeed = calc_rate (dlquant, dltime, &units);
+ sprintf (p, " %7.2f%s", dlspeed, short_units[units]);
p += strlen (p);
}
else
- {
- strcpy (p, " --.--K/s");
- p += 11;
- }
+ APPEND_LITERAL (" --.--K/s");
- /* " ETA xx:xx:xx" */
- if (bp->total_length > 0 && bp->count > 0)
+ /* " ETA xx:xx:xx"; wait for three seconds before displaying the ETA.
+ That's because the ETA value needs a while to become
+ reliable. */
+ if (bp->total_length > 0 && dl_total_time > 3000)
{
- int eta, eta_hrs, eta_min, eta_sec;
- double tm_sofar = (double)dltime / 1000;
- long bytes_remaining = bp->total_length - size;
-
- eta = (int) (tm_sofar * bytes_remaining / bp->count);
+ long eta;
+ int eta_hrs, eta_min, eta_sec;
+
+ /* Don't change the value of ETA more than approximately once
+ per second; doing so would cause flashing without providing
+ any value to the user. */
+ if (bp->total_length != size
+ && bp->last_eta_value != 0
+ && dl_total_time - bp->last_eta_time < 900)
+ eta = bp->last_eta_value;
+ else
+ {
+ /* Calculate ETA using the average download speed to predict
+ the future speed. If you want to use a speed averaged
+ over a more recent period, replace dl_total_time with
+ hist->total_time and bp->count with hist->total_bytes.
+ I found that doing that results in a very jerky and
+ ultimately unreliable ETA. */
+ double time_sofar = (double)dl_total_time / 1000;
+ long bytes_remaining = bp->total_length - size;
+ eta = (long) (time_sofar * bytes_remaining / bp->count);
+ bp->last_eta_value = eta;
+ bp->last_eta_time = dl_total_time;
+ }
eta_hrs = eta / 3600, eta %= 3600;
eta_min = eta / 60, eta %= 60;
eta_sec = eta;
- /*printf ("\neta: %d, %d %d %d\n", eta, eta_hrs, eta_min, eta_sec);*/
- /*printf ("\n%ld %f %ld %ld\n", dltime, tm_sofar, bytes_remaining, bp->count);*/
-
- *p++ = ' ';
- *p++ = 'E';
- *p++ = 'T';
- *p++ = 'A';
- *p++ = ' ';
-
if (eta_hrs > 99)
- /* Bogus value, for whatever reason. We must avoid overflow. */
- sprintf (p, "--:--");
- else if (eta_hrs > 0)
- sprintf (p, "%d:%02d:%02d", eta_hrs, eta_min, eta_sec);
+ goto no_eta;
+
+ if (eta_hrs == 0)
+ {
+ /* Hours not printed: pad with three spaces. */
+ APPEND_LITERAL (" ");
+ sprintf (p, " ETA %02d:%02d", eta_min, eta_sec);
+ }
else
- sprintf (p, "%02d:%02d", eta_min, eta_sec);
+ {
+ if (eta_hrs < 10)
+ /* Hours printed with one digit: pad with one space. */
+ *p++ = ' ';
+ sprintf (p, " ETA %d:%02d:%02d", eta_hrs, eta_min, eta_sec);
+ }
p += strlen (p);
}
else if (bp->total_length > 0)
{
- strcpy (p, " ETA --:--");
- p += 10;
+ no_eta:
+ APPEND_LITERAL (" ");
}
- assert (p - bp->buffer <= screen_width);
+ assert (p - bp->buffer <= bp->width);
- while (p < bp->buffer + screen_width)
+ while (p < bp->buffer + bp->width)
*p++ = ' ';
*p = '\0';
}
static void
display_image (char *buf)
{
+ int old = log_set_save_context (0);
logputs (LOG_VERBOSE, "\r");
logputs (LOG_VERBOSE, buf);
+ log_set_save_context (old);
}
static void
bar_set_params (const char *params)
{
int sw;
+ char *term = getenv ("TERM");
if (params
&& 0 == strcmp (params, "force"))
if ((opt.lfilename
#ifdef HAVE_ISATTY
+ /* The progress bar doesn't make sense if the output is not a
+ TTY -- when logging to file, it is better to review the
+ dots. */
|| !isatty (fileno (stderr))
#else
1
#endif
+ /* Normally we don't depend on terminal type because the
+ progress bar only uses ^M to move the cursor to the
+ beginning of line, which works even on dumb terminals. But
+ Jamie Zawinski reports that ^M and ^H tricks don't work in
+ Emacs shell buffers, and only make a mess. */
+ || (term && 0 == strcmp (term, "emacs"))
)
&& !current_impl_locked)
{
screen_width = sw;
}
+#ifdef SIGWINCH
RETSIGTYPE
progress_handle_sigwinch (int sig)
{
int sw = determine_screen_width ();
if (sw && sw >= MINIMUM_SCREEN_WIDTH)
screen_width = sw;
+ signal (SIGWINCH, progress_handle_sigwinch);
}
+#endif