track optimization

This commit is contained in:
Will McGugan 2020-07-26 12:14:44 +01:00
parent 847a38ecd4
commit 59eee8930b
5 changed files with 87 additions and 16 deletions

View file

@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [4.1.0] - 2020-07-26
### Changed
- Optimized progress.track for very quick iterations
- Force default size of 80x25 if get_terminal_size reports size of 0,0
## [4.0.0] - 2020-07-23 ## [4.0.0] - 2020-07-23
Major version bump for a breaking change to `Text.stylize signature`, which corrects a minor but irritating API wart. The style now comes first and the `start` and `end` offsets default to the entire text. This allows for `text.stylize_all(style)` to be replaced with `text.stylize(style)`. The `start` and `end` offsets now support negative indexing, so `text.stylize("bold", -1)` makes the last character bold. Major version bump for a breaking change to `Text.stylize signature`, which corrects a minor but irritating API wart. The style now comes first and the `start` and `end` offsets default to the entire text. This allows for `text.stylize_all(style)` to be replaced with `text.stylize(style)`. The `start` and `end` offsets now support negative indexing, so `text.stylize("bold", -1)` makes the last character bold.

View file

@ -2,7 +2,7 @@
name = "rich" name = "rich"
homepage = "https://github.com/willmcgugan/rich" homepage = "https://github.com/willmcgugan/rich"
documentation = "https://rich.readthedocs.io/en/latest/" documentation = "https://rich.readthedocs.io/en/latest/"
version = "4.0.0" version = "4.1.0"
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
authors = ["Will McGugan <willmcgugan@gmail.com>"] authors = ["Will McGugan <willmcgugan@gmail.com>"]
license = "MIT" license = "MIT"

View file

@ -386,12 +386,12 @@ LEGACY_WINDOWS_SUBSTITUTIONS = {
@overload @overload
def get_safe_box(box: None, legacy_windows: bool) -> None: # pragma: no cover def get_safe_box(box: None, legacy_windows: bool) -> None:
... ...
@overload @overload
def get_safe_box(box: Box, legacy_windows: bool) -> Box: # pragma: no cover def get_safe_box(box: Box, legacy_windows: bool) -> Box:
... ...

View file

@ -517,6 +517,9 @@ class Console:
return ConsoleDimensions(80, 25) return ConsoleDimensions(80, 25)
width, height = shutil.get_terminal_size() width, height = shutil.get_terminal_size()
# get_terminal_size can report 0, 0 if run from psuedo-terminal
width = width or 80
height = height or 25
return ConsoleDimensions( return ConsoleDimensions(
(width - self.legacy_windows) if self._width is None else self._width, (width - self.legacy_windows) if self._width is None else self._width,
height if self._height is None else self._height, height if self._height is None else self._height,

View file

@ -6,7 +6,7 @@ from datetime import timedelta
import io import io
from math import ceil from math import ceil
import sys import sys
from time import monotonic from time import monotonic, perf_counter
from threading import Event, RLock, Thread from threading import Event, RLock, Thread
from typing import ( from typing import (
Any, Any,
@ -53,6 +53,36 @@ ProgressType = TypeVar("ProgressType")
GetTimeCallable = Callable[[], float] GetTimeCallable = Callable[[], float]
def iter_track(
values: ProgressType, update_period: float = 0.05
) -> Iterable[List[ProgressType]]:
"""Break a sequence in to chunks based on time.
Args:
values (ProgressType): An iterable of values.
Returns:
Iterable[List[ProgressType]]: An iterable containing lists of values from sequence.
"""
output: List[ProgressType] = []
append = output.append
get_time = perf_counter
period_size = 1
for value in values:
append(value)
if len(output) >= period_size:
start_time = get_time()
yield output
time_taken = get_time() - start_time
del output[:]
if abs(time_taken - update_period) > 0.2 * update_period:
period_size = period_size * (1.5 if time_taken < update_period else 0.8)
if output:
yield output
def track( def track(
sequence: Union[Sequence[ProgressType], Iterable[ProgressType]], sequence: Union[Sequence[ProgressType], Iterable[ProgressType]],
description="Working...", description="Working...",
@ -66,6 +96,7 @@ def track(
complete_style: StyleType = "bar.complete", complete_style: StyleType = "bar.complete",
finished_style: StyleType = "bar.finished", finished_style: StyleType = "bar.finished",
pulse_style: StyleType = "bar.pulse", pulse_style: StyleType = "bar.pulse",
update_period: float = 0.02,
) -> Iterable[ProgressType]: ) -> Iterable[ProgressType]:
"""Track progress by iterating over a sequence. """Track progress by iterating over a sequence.
@ -80,7 +111,8 @@ def track(
style (StyleType, optional): Style for the bar background. Defaults to "bar.back". style (StyleType, optional): Style for the bar background. Defaults to "bar.back".
complete_style (StyleType, optional): Style for the completed bar. Defaults to "bar.complete". complete_style (StyleType, optional): Style for the completed bar. Defaults to "bar.complete".
finished_style (StyleType, optional): Style for a finished bar. Defaults to "bar.done". finished_style (StyleType, optional): Style for a finished bar. Defaults to "bar.done".
pulse_style (StyleType, optional): Style for pulsing bars. Defaults to "bar.pulse". pulse_style (StyleType, optional): Style for pulsing bars. Defaults to "bar.pulse".
update_period (float, optional): Minimum time (in seconds) between calls to update(). Defaults to 0.05.
Returns: Returns:
Iterable[ProgressType]: An iterable of the values in the sequence. Iterable[ProgressType]: An iterable of the values in the sequence.
@ -120,10 +152,13 @@ def track(
) )
task_id = progress.add_task(description, total=task_total) task_id = progress.add_task(description, total=task_total)
advance = progress.advance
with progress: with progress:
for completed, value in enumerate(sequence, 1): for values in iter_track(
yield value sequence, update_period=update_period if auto_refresh else 0
progress.update(task_id, completed=completed) ):
yield from values
advance(task_id, len(values))
if not progress.auto_refresh: if not progress.auto_refresh:
progress.refresh() progress.refresh()
@ -390,13 +425,15 @@ class Task:
"""Optional[float]: Get the estimated speed in steps per second.""" """Optional[float]: Get the estimated speed in steps per second."""
if self.start_time is None: if self.start_time is None:
return None return None
progress = list(self._progress) progress = self._progress
if not progress: if not progress:
return None return None
total_time = progress[-1].timestamp - progress[0].timestamp total_time = progress[-1].timestamp - progress[0].timestamp
if total_time == 0: if total_time == 0:
return None return None
total_completed = sum(sample.completed for sample in progress[1:]) iter_progress = iter(progress)
next(iter_progress)
total_completed = sum(sample.completed for sample in iter_progress)
speed = total_completed / total_time speed = total_completed / total_time
return speed return speed
@ -611,6 +648,7 @@ class Progress(JupyterMixin, RenderHook):
total: int = None, total: int = None,
task_id: Optional[TaskID] = None, task_id: Optional[TaskID] = None,
description="Working...", description="Working...",
update_period: float = 0.05,
) -> Iterable[ProgressType]: ) -> Iterable[ProgressType]:
"""Track progress by iterating over a sequence. """Track progress by iterating over a sequence.
@ -619,6 +657,7 @@ class Progress(JupyterMixin, RenderHook):
total: (int, optional): Total number of steps. Default is len(sequence). total: (int, optional): Total number of steps. Default is len(sequence).
task_id: (TaskID): Task to track. Default is new task. task_id: (TaskID): Task to track. Default is new task.
description: (str, optional): Description of task, if new task is created. description: (str, optional): Description of task, if new task is created.
update_period (float, optional): Minimum time (in seconds) between calls to update(). Defaults to 0.05.
Returns: Returns:
Iterable[ProgressType]: An iterable of values taken from the provided sequence. Iterable[ProgressType]: An iterable of values taken from the provided sequence.
@ -638,9 +677,14 @@ class Progress(JupyterMixin, RenderHook):
else: else:
self.update(task_id, total=task_total) self.update(task_id, total=task_total)
with self: with self:
for completed, value in enumerate(sequence, 1): advance = self.advance
yield value for values in iter_track(
self.update(task_id, completed=completed) sequence, update_period=update_period if self.auto_refresh else 0
):
yield from values
advance(task_id, len(values))
if not self.auto_refresh:
progress.refresh()
def start_task(self, task_id: TaskID) -> None: def start_task(self, task_id: TaskID) -> None:
"""Start a task. """Start a task.
@ -716,9 +760,12 @@ class Progress(JupyterMixin, RenderHook):
old_sample_time = current_time - self.speed_estimate_period old_sample_time = current_time - self.speed_estimate_period
_progress = task._progress _progress = task._progress
popleft = _progress.popleft
while _progress and _progress[0].timestamp < old_sample_time: while _progress and _progress[0].timestamp < old_sample_time:
_progress.popleft() popleft()
task._progress.append(ProgressSample(current_time, update_completed)) while len(_progress) > 10:
popleft()
_progress.append(ProgressSample(current_time, update_completed))
if refresh: if refresh:
self.refresh() self.refresh()
@ -729,7 +776,21 @@ class Progress(JupyterMixin, RenderHook):
task_id (TaskID): ID of task. task_id (TaskID): ID of task.
advance (float): Number of steps to advance. Default is 1. advance (float): Number of steps to advance. Default is 1.
""" """
self.update(task_id, advance=advance) current_time = self.get_time()
with self._lock:
task = self._tasks[task_id]
completed_start = task.completed
task.completed += advance
update_completed = task.completed - completed_start
old_sample_time = current_time - self.speed_estimate_period
_progress = task._progress
popleft = _progress.popleft
while _progress and _progress[0].timestamp < old_sample_time:
popleft()
while len(_progress) > 10:
popleft()
_progress.append(ProgressSample(current_time, update_completed))
def refresh(self) -> None: def refresh(self) -> None:
"""Refresh (render) the progress information.""" """Refresh (render) the progress information."""