Repository: pallets/click

Issue:
Avoid rendering on highly frequent updates in progressbar
I am using a progressbar in a loop with short iterations, which negatively affects performance. It would be good to supply a chunk size such that the heavy part of an update is only performed every nth time. 

Currently I am using a small wrapper around it:

```
class ChunkProgressbar:
    def __init__(self, iterator, chunk_size=100, enabled=True, **kwargs):
        self.iterator = iterator
        self.chunk_size = chunk_size
        self.bar = progressbar(iterator, **kwargs)
        self.missed_updates = 0

    def __enter__(self):
        if self.bar is not None:
            self.bar.__enter__()
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.bar.update(self.missed_updates)
        self.bar.__exit__(exc_type, exc_val, exc_tb)

    def __iter__(self):
        for index, elem in enumerate(self.iterator):
            yield elem
            self.missed_updates += 1
            if index % self.chunk_size == self.chunk_size - 1:
                self.bar.update(self.missed_updates)
                self.missed_updates = 0
```

One could also throttle the rendering based on timing information (like the eta calculation), but that might be costly again due to timing calls or mess with heterogeneous iteration times.



Discussion:

Resolving patch:
diff --git a/src/click/_termui_impl.py b/src/click/_termui_impl.py
--- a/src/click/_termui_impl.py
+++ b/src/click/_termui_impl.py
@@ -62,6 +62,7 @@ def __init__(
         label=None,
         file=None,
         color=None,
+        update_min_steps=1,
         width=30,
     ):
         self.fill_char = fill_char
@@ -77,6 +78,8 @@ def __init__(
             file = _default_text_stdout()
         self.file = file
         self.color = color
+        self.update_min_steps = update_min_steps
+        self._completed_intervals = 0
         self.width = width
         self.autowidth = width == 0
 
@@ -290,13 +293,23 @@ def update(self, n_steps, current_item=None):
         :param current_item: Optional item to set as ``current_item``
             for the updated position.
 
-        .. versionadded:: 8.0
+        .. versionchanged:: 8.0
             Added the ``current_item`` optional parameter.
+
+        .. versionchanged:: 8.0
+            Only render when the number of steps meets the
+            ``update_min_steps`` threshold.
         """
-        self.make_step(n_steps)
-        if current_item is not None:
-            self.current_item = current_item
-        self.render_progress()
+        self._completed_intervals += n_steps
+
+        if self._completed_intervals >= self.update_min_steps:
+            self.make_step(self._completed_intervals)
+
+            if current_item is not None:
+                self.current_item = current_item
+
+            self.render_progress()
+            self._completed_intervals = 0
 
     def finish(self):
         self.eta_known = 0
diff --git a/src/click/termui.py b/src/click/termui.py
--- a/src/click/termui.py
+++ b/src/click/termui.py
@@ -299,6 +299,7 @@ def progressbar(
     width=36,
     file=None,
     color=None,
+    update_min_steps=1,
 ):
     """This function creates an iterable context manager that can be used
     to iterate over something while showing a progress bar.  It will
@@ -353,12 +354,6 @@ def progressbar(
                 archive.extract()
                 bar.update(archive.size, archive)
 
-    .. versionadded:: 2.0
-
-    .. versionadded:: 4.0
-       Added the `color` parameter.  Added a `update` method to the
-       progressbar object.
-
     :param iterable: an iterable to iterate over.  If not provided the length
                      is required.
     :param length: the number of items to iterate over.  By default the
@@ -397,6 +392,17 @@ def progressbar(
                   default is autodetection.  This is only needed if ANSI
                   codes are included anywhere in the progress bar output
                   which is not the case by default.
+    :param update_min_steps: Render only when this many updates have
+        completed. This allows tuning for very fast iterators.
+
+    .. versionadded:: 8.0
+       Added the ``update_min_steps`` parameter.
+
+    .. versionchanged:: 4.0
+        Added the ``color`` parameter. Added the ``update`` method to
+        the object.
+
+    .. versionadded:: 2.0
     """
     from ._termui_impl import ProgressBar
 
@@ -416,6 +422,7 @@ def progressbar(
         label=label,
         width=width,
         color=color,
+        update_min_steps=update_min_steps,
     )
 
 


Test patch:
diff --git a/tests/test_termui.py b/tests/test_termui.py
--- a/tests/test_termui.py
+++ b/tests/test_termui.py
@@ -344,6 +344,16 @@ def cli():
     assert "Custom 4" in lines[2]
 
 
+def test_progress_bar_update_min_steps(runner):
+    bar = _create_progress(update_min_steps=5)
+    bar.update(3)
+    assert bar._completed_intervals == 3
+    assert bar.pos == 0
+    bar.update(2)
+    assert bar._completed_intervals == 0
+    assert bar.pos == 5
+
+
 @pytest.mark.parametrize("key_char", ("h", "H", "é", "À", " ", "字", "àH", "àR"))
 @pytest.mark.parametrize("echo", [True, False])
 @pytest.mark.skipif(not WIN, reason="Tests user-input using the msvcrt module.")