]> sjero.net Git - wget/blobdiff - src/progress.c
[svn] Update progress code to use higher timer resolution.
[wget] / src / progress.c
index 4bf8541b3bdb7157a2fede69e7395687b51f1890..0e6267965047b0809618a53c3848050cbc65e9cc 100644 (file)
@@ -1,5 +1,5 @@
 /* Download progress.
-   Copyright (C) 2001 Free Software Foundation, Inc.
+   Copyright (C) 2001, 2002 Free Software Foundation, Inc.
 
 This file is part of GNU Wget.
 
@@ -15,7 +15,17 @@ GNU General Public License for more details.
 
 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>
 
@@ -30,6 +40,9 @@ Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.  */
 #ifdef HAVE_UNISTD_H
 # include <unistd.h>
 #endif
+#ifdef HAVE_SIGNAL_H
+# include <signal.h>
+#endif
 
 #include "wget.h"
 #include "progress.h"
@@ -38,22 +51,22 @@ Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.  */
 
 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[] = {
@@ -61,7 +74,7 @@ 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.  */
@@ -159,7 +172,7 @@ progress_create (long initial, long total)
    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);
 }
@@ -168,7 +181,7 @@ progress_update (void *progress, long howmuch, long 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);
 }
@@ -185,7 +198,7 @@ struct dot_progress {
 
   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. */
@@ -247,7 +260,7 @@ print_percentage (long bytes, long expected)
 }
 
 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));
@@ -257,7 +270,7 @@ print_download_speed (struct dot_progress *dp, long bytes, long dltime)
 /* 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;
@@ -297,7 +310,7 @@ dot_update (void *progress, long howmuch, long dltime)
 /* 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;
@@ -400,6 +413,15 @@ dot_set_params (const char *params)
 
 static int screen_width = DEFAULT_SCREEN_WIDTH;
 
+/* Size of the download speed history ring. */
+#define DLSPEED_HISTORY_SIZE 30
+
+/* The minimum time length of a history sample.  By default, each
+   sample is at least 100ms long, which means that, over the course of
+   30 samples, "current" download speed spans at least 3s into the
+   past.  */
+#define DLSPEED_SAMPLE_MIN 100
+
 struct bar_progress {
   long initial_length;         /* how many bytes have been downloaded
                                   previously. */
@@ -407,17 +429,49 @@ struct bar_progress {
                                   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 *
@@ -427,9 +481,17 @@ bar_create (long initial, long total)
 
   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");
@@ -440,11 +502,13 @@ bar_create (long initial, long total)
   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
@@ -454,34 +518,35 @@ bar_update (void *progress, long howmuch, long dltime)
        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);
@@ -492,26 +557,117 @@ bar_finish (void *progress, long dltime)
   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 0.1s intervals and stores the
+   recorded samples in a FIFO history ring.  The ring stores no more
+   than 30 intervals, hence the history covers the period of at least
+   three seconds and at most 30 reads into the past.  This method
+   should produce good results for both very fast and very slow
+   downloads.
+
+   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 0.1s 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 very 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;
@@ -530,12 +686,7 @@ create_image (struct bar_progress *bp, long dltime)
       p += 4;
     }
   else
-    {
-      *p++ = ' ';
-      *p++ = ' ';
-      *p++ = ' ';
-      *p++ = ' ';
-    }
+    APPEND_LITERAL ("    ");
 
   /* The progress bar: "[====>      ]" */
   if (progress_size && bp->total_length > 0)
@@ -589,69 +740,82 @@ create_image (struct bar_progress *bp, long dltime)
       ++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);
+      long bytes = hist->total_bytes + bp->recent_bytes;
+      double tm = hist->total_time + dl_total_time - bp->recent_start;
+      double dlrate = calc_rate (bytes, tm, &units);
       sprintf (p, " %7.2f%s", dlrate, 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)
+  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 (dl_total_time - bp->last_eta_time < 900
+         && bp->last_eta_value != 0)
+       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';
 }
@@ -662,14 +826,17 @@ create_image (struct bar_progress *bp, long dltime)
 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"))
@@ -677,10 +844,19 @@ bar_set_params (const char *params)
 
   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)
     {
@@ -698,10 +874,13 @@ bar_set_params (const char *params)
     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