progress: make ETA only consider progress made in the last minute
authorJun Wu <quark@fb.com>
Wed, 27 Sep 2017 15:14:59 -0700
changeset 39812 a667f0ca1d5fa8b80006fc4030ded936b47d3e83
parent 39811 f428c347d32b2f110856c6fb7fec00a5f4cc0c0d
child 39815 98b35921691504a4eaad97c1efac75ad3493d9d2
push id571
push usergszorc@mozilla.com
push dateFri, 29 Sep 2017 23:16:06 +0000
progress: make ETA only consider progress made in the last minute This patch limits the estimate time interval to roughly the last minute (configurable by `estimateinterval`) to be more practical. See the test change for why this is better. .. feature:: Estimated time is more accurate with non-linear progress Differential Revision: https://phab.mercurial-scm.org/D820
mercurial/configitems.py
mercurial/help/config.txt
mercurial/progress.py
tests/test-progress.t
--- a/mercurial/configitems.py
+++ b/mercurial/configitems.py
@@ -354,16 +354,19 @@ coreconfigitem('progress', 'debug',
     default=False,
 )
 coreconfigitem('progress', 'delay',
     default=3,
 )
 coreconfigitem('progress', 'disable',
     default=False,
 )
+coreconfigitem('progress', 'estimateinterval',
+    default=60.0,
+)
 coreconfigitem('progress', 'refresh',
     default=0.1,
 )
 coreconfigitem('progress', 'width',
     default=dynamicdefault,
 )
 coreconfigitem('push', 'pushvars.server',
     default=False,
--- a/mercurial/help/config.txt
+++ b/mercurial/help/config.txt
@@ -1608,16 +1608,20 @@ have a definite end point.
 
 ``delay``
     Number of seconds (float) before showing the progress bar. (default: 3)
 
 ``changedelay``
     Minimum delay before showing a new topic. When set to less than 3 * refresh,
     that value will be used instead. (default: 1)
 
+``estimateinterval``
+    Maximum sampling interval in seconds for speed and estimated time
+    calculation. (default: 60)
+
 ``refresh``
     Time in seconds between refreshes of the progress bar. (default: 0.1)
 
 ``format``
     Format of the progress bar.
 
     Valid entries for the format field are ``topic``, ``bar``, ``number``,
     ``unit``, ``estimate``, ``speed``, and ``item``. ``item`` defaults to the
--- a/mercurial/progress.py
+++ b/mercurial/progress.py
@@ -99,16 +99,18 @@ class progbar(object):
         self.refresh = float(self.ui.config(
             'progress', 'refresh'))
         self.changedelay = max(3 * self.refresh,
                                float(self.ui.config(
                                    'progress', 'changedelay')))
         self.order = self.ui.configlist(
             'progress', 'format',
             default=['topic', 'bar', 'number', 'estimate'])
+        self.estimateinterval = self.ui.configwith(
+            float, 'progress', 'estimateinterval')
 
     def show(self, now, topic, pos, item, unit, total):
         if not shouldprint(self.ui):
             return
         termwidth = self.width()
         self.printed = True
         head = ''
         needprogress = False
@@ -233,16 +235,42 @@ class progbar(object):
             # not a topic change
             or self.curtopic == self.lasttopic
             # it's been long enough we should print anyway
             or now - self.lastprint >= self.changedelay):
             return True
         else:
             return False
 
+    def _calibrateestimate(self, topic, now, pos):
+        '''Adjust starttimes and startvals for topic so ETA works better
+
+        If progress is non-linear (ex. get much slower in the last minute),
+        it's more friendly to only use a recent time span for ETA and speed
+        calculation.
+
+            [======================================>       ]
+                                             ^^^^^^^
+                           estimateinterval, only use this for estimation
+        '''
+        interval = self.estimateinterval
+        if interval <= 0:
+            return
+        elapsed = now - self.starttimes[topic]
+        if elapsed > interval:
+            delta = pos - self.startvals[topic]
+            newdelta = delta * interval / elapsed
+            # If a stall happens temporarily, ETA could change dramatically
+            # frequently. This is to avoid such dramatical change and make ETA
+            # smoother.
+            if newdelta < 0.1:
+                return
+            self.startvals[topic] = pos - newdelta
+            self.starttimes[topic] = now - interval
+
     def progress(self, topic, pos, item='', unit='', total=None):
         now = time.time()
         self._refreshlock.acquire()
         try:
             if pos is None:
                 self.starttimes.pop(topic, None)
                 self.startvals.pop(topic, None)
                 self.topicstates.pop(topic, None)
@@ -263,14 +291,15 @@ class progbar(object):
                         self.lasttopic = None
             else:
                 if topic not in self.topics:
                     self.starttimes[topic] = now
                     self.startvals[topic] = pos
                     self.topics.append(topic)
                 self.topicstates[topic] = pos, item, unit, total
                 self.curtopic = topic
+                self._calibrateestimate(topic, now, pos)
                 if now - self.lastprint >= self.refresh and self.topics:
                     if self._oktoprint(now):
                         self.lastprint = now
                         self.show(now, topic, *self.topicstates[topic])
         finally:
             self._refreshlock.release()
--- a/tests/test-progress.t
+++ b/tests/test-progress.t
@@ -255,27 +255,27 @@ Non-linear progress:
   loop [=>                                      ]  1/20 6m21s\r (no-eol) (esc)
   loop [===>                                    ]  2/20 6m01s\r (no-eol) (esc)
   loop [=====>                                  ]  3/20 5m41s\r (no-eol) (esc)
   loop [=======>                                ]  4/20 5m21s\r (no-eol) (esc)
   loop [=========>                              ]  5/20 5m01s\r (no-eol) (esc)
   loop [===========>                            ]  6/20 4m41s\r (no-eol) (esc)
   loop [=============>                          ]  7/20 4m21s\r (no-eol) (esc)
   loop [===============>                        ]  8/20 4m01s\r (no-eol) (esc)
-  loop [================>                      ]  9/20 13m27s\r (no-eol) (esc)
-  loop [==================>                    ] 10/20 19m21s\r (no-eol) (esc)
-  loop [====================>                  ] 11/20 22m39s\r (no-eol) (esc)
-  loop [======================>                ] 12/20 24m01s\r (no-eol) (esc)
-  loop [========================>              ] 13/20 23m53s\r (no-eol) (esc)
-  loop [==========================>            ] 14/20 19m09s\r (no-eol) (esc)
-  loop [============================>          ] 15/20 15m01s\r (no-eol) (esc)
-  loop [==============================>        ] 16/20 11m21s\r (no-eol) (esc)
-  loop [=================================>      ] 17/20 8m04s\r (no-eol) (esc)
-  loop [===================================>    ] 18/20 5m07s\r (no-eol) (esc)
-  loop [=====================================>  ] 19/20 2m27s\r (no-eol) (esc)
+  loop [================>                      ]  9/20 25m40s\r (no-eol) (esc)
+  loop [===================>                    ] 10/20 1h06m\r (no-eol) (esc)
+  loop [=====================>                  ] 11/20 1h13m\r (no-eol) (esc)
+  loop [=======================>                ] 12/20 1h07m\r (no-eol) (esc)
+  loop [========================>              ] 13/20 58m19s\r (no-eol) (esc)
+  loop [===========================>            ] 14/20 7m09s\r (no-eol) (esc)
+  loop [=============================>          ] 15/20 3m38s\r (no-eol) (esc)
+  loop [===============================>        ] 16/20 2m15s\r (no-eol) (esc)
+  loop [=================================>      ] 17/20 1m27s\r (no-eol) (esc)
+  loop [====================================>     ] 18/20 52s\r (no-eol) (esc)
+  loop [======================================>   ] 19/20 25s\r (no-eol) (esc)
                                                               \r (no-eol) (esc)
 
 Time estimates should not fail when there's no end point:
   $ hg -y loop -- -4
   \r (no-eol) (esc)
   loop [ <=>                                              ] 2\r (no-eol) (esc)
   loop [  <=>                                             ] 3\r (no-eol) (esc)
                                                               \r (no-eol) (esc)