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/),
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
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"
homepage = "https://github.com/willmcgugan/rich"
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"
authors = ["Will McGugan <willmcgugan@gmail.com>"]
license = "MIT"

View file

@ -386,12 +386,12 @@ LEGACY_WINDOWS_SUBSTITUTIONS = {
@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
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)
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(
(width - self.legacy_windows) if self._width is None else self._width,
height if self._height is None else self._height,

View file

@ -6,7 +6,7 @@ from datetime import timedelta
import io
from math import ceil
import sys
from time import monotonic
from time import monotonic, perf_counter
from threading import Event, RLock, Thread
from typing import (
Any,
@ -53,6 +53,36 @@ ProgressType = TypeVar("ProgressType")
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(
sequence: Union[Sequence[ProgressType], Iterable[ProgressType]],
description="Working...",
@ -66,6 +96,7 @@ def track(
complete_style: StyleType = "bar.complete",
finished_style: StyleType = "bar.finished",
pulse_style: StyleType = "bar.pulse",
update_period: float = 0.02,
) -> Iterable[ProgressType]:
"""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".
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".
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:
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)
advance = progress.advance
with progress:
for completed, value in enumerate(sequence, 1):
yield value
progress.update(task_id, completed=completed)
for values in iter_track(
sequence, update_period=update_period if auto_refresh else 0
):
yield from values
advance(task_id, len(values))
if not progress.auto_refresh:
progress.refresh()
@ -390,13 +425,15 @@ class Task:
"""Optional[float]: Get the estimated speed in steps per second."""
if self.start_time is None:
return None
progress = list(self._progress)
progress = self._progress
if not progress:
return None
total_time = progress[-1].timestamp - progress[0].timestamp
if total_time == 0:
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
return speed
@ -611,6 +648,7 @@ class Progress(JupyterMixin, RenderHook):
total: int = None,
task_id: Optional[TaskID] = None,
description="Working...",
update_period: float = 0.05,
) -> Iterable[ProgressType]:
"""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).
task_id: (TaskID): Task to track. Default is new task.
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:
Iterable[ProgressType]: An iterable of values taken from the provided sequence.
@ -638,9 +677,14 @@ class Progress(JupyterMixin, RenderHook):
else:
self.update(task_id, total=task_total)
with self:
for completed, value in enumerate(sequence, 1):
yield value
self.update(task_id, completed=completed)
advance = self.advance
for values in iter_track(
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:
"""Start a task.
@ -716,9 +760,12 @@ class Progress(JupyterMixin, RenderHook):
old_sample_time = current_time - self.speed_estimate_period
_progress = task._progress
popleft = _progress.popleft
while _progress and _progress[0].timestamp < old_sample_time:
_progress.popleft()
task._progress.append(ProgressSample(current_time, update_completed))
popleft()
while len(_progress) > 10:
popleft()
_progress.append(ProgressSample(current_time, update_completed))
if refresh:
self.refresh()
@ -729,7 +776,21 @@ class Progress(JupyterMixin, RenderHook):
task_id (TaskID): ID of task.
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:
"""Refresh (render) the progress information."""