import time
import typing
from functools import wraps
from typing import Optional, Union, Callable
if typing.TYPE_CHECKING:
from jina.logging.logger import JinaLogger
from jina import __windows__
from jina.helper import get_readable_size, get_readable_time, colored, get_rich_console
from rich.progress import (
Progress,
Task,
BarColumn,
TimeRemainingColumn,
SpinnerColumn,
TimeElapsedColumn,
TextColumn,
ProgressColumn,
TaskID,
)
from rich.text import Text
from rich.table import Column
from rich.console import Console
[docs]def used_memory(unit: int = 1024 * 1024 * 1024) -> float:
"""
Get the memory usage of the current process.
:param unit: Unit of the memory, default in Gigabytes.
:return: Memory usage of the current process.
"""
if __windows__:
# TODO: windows doesn't include `resource` module
return 0
import resource
return resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / unit
[docs]def used_memory_readable() -> str:
"""
Get the memory usage of the current process in a human-readable format.
:return: Memory usage of the current process.
"""
return get_readable_size(used_memory(1))
[docs]def profiling(func):
"""
Create the Decorator to mark a function for profiling. The time and memory usage will be recorded and printed.
Example:
.. highlight:: python
.. code-block:: python
@profiling
def foo():
print(1)
:param func: function to be profiled
:return: arguments wrapper
"""
from jina.logging.predefined import default_logger
@wraps(func)
def arg_wrapper(*args, **kwargs):
start_t = time.perf_counter()
start_mem = used_memory(unit=1)
r = func(*args, **kwargs)
elapsed = time.perf_counter() - start_t
end_mem = used_memory(unit=1)
# level_prefix = ''.join('-' for v in inspect.stack() if v and v.index is not None and v.index >= 0)
level_prefix = ''
mem_status = f'memory Δ {get_readable_size(end_mem - start_mem)} {get_readable_size(start_mem)} -> {get_readable_size(end_mem)}'
default_logger.info(
f'{level_prefix} {func.__qualname__} time: {elapsed}s {mem_status}'
)
return r
return arg_wrapper
[docs]class ProgressBar(Progress):
"""
A progress bar made with rich.
Example:
.. highlight:: python
.. code-block:: python
with ProgressBar(100, 'loop') as p_bar:
for i in range(100):
do_busy()
p_bar.update()
"""
def __init__(
self,
description: str = 'Working...',
total_length: Optional[float] = None,
message_on_done: Optional[Union[str, Callable[..., str]]] = None,
columns: Optional[Union[str, ProgressColumn]] = None,
disable: bool = False,
console: Optional[Console] = None,
**kwargs,
):
"""Init a custom progress bar based on rich. This is the default progress bar of jina if you want to customize
it you should probably just use a rich `Progress` and add your custom column and task
:param description: description of your task ex : 'Working...'
:param total_length: the number of steps
:param message_on_done: The message that you want to print at the end of your task. It can either be a string to
be formatted with task (ex '{task.completed}') task or a function which take task as input (ex : lambda task : f'{task.completed}'
:param columns: If you want to customize the column of the progress bar. Note that you should probably directly use
rich Progress object than overwriting these columns parameters.
:param total_length: disable the progress bar
.. # noqa: DAR202
.. # noqa: DAR101
.. # noqa: DAR003
"""
def _default_message_on_done(task):
return f'{task.completed} steps done in {get_readable_time(seconds=task.finished_time)}'
columns = columns or [
SpinnerColumn(),
_OnDoneColumn(f'DONE', description, 'progress.description'),
BarColumn(complete_style='green', finished_style='yellow'),
TimeElapsedColumn(),
'[progress.percentage]{task.percentage:>3.0f}%',
TextColumn('ETA:', style='progress.remaining'),
TimeRemainingColumn(),
_OnDoneColumn(
message_on_done if message_on_done else _default_message_on_done
),
]
if not console:
console = get_rich_console()
super().__init__(*columns, console=console, disable=disable, **kwargs)
self.task_id = self.add_task(
'Working...', total=total_length if total_length else 100.0
)
[docs] def update(
self,
task_id: Optional[TaskID] = None,
advance: float = 1,
*args,
**kwargs,
):
"""Update the progress bar
:param task_id: the task to update
:param advance: Add a value to main task.completed
.. # noqa: DAR202
.. # noqa: DAR101
.. # noqa: DAR003
"""
if not task_id:
super().update(self.task_id, advance=advance, *args, **kwargs)
else:
super().update(task_id, advance=advance, *args, **kwargs)
class _OnDoneColumn(ProgressColumn):
"""Renders custom on done for jina progress bar."""
def __init__(
self,
text_on_done_format: Union[str, Callable],
text_init_format: str = '',
style: Optional[str] = None,
table_column: Optional[Column] = None,
):
"""
Create a ProgressBar column with a final message
Example:
.. highlight:: python
.. code-block:: python
def on_done(task):
return f'{task.completed} steps done in {task.finished_time:.0f} seconds'
column = _OnDoneColumn(text_on_done_format=on_done) # functional
column = _OnDoneColumn(
text_on_done_format='{task.completed} steps done in {task.finished_time:.0f} seconds'
) # formatting
:param text_on_done_format: message_on_done
:param text_init_format: string to be formatted with task or a function which take task as input
:param style: rich style for the Text
:param table_column: rich table column
"""
super().__init__(table_column)
self.text_on_done_format = text_on_done_format
self.text_init_format = text_init_format
self.style = style
def render(self, task: 'Task') -> Text:
if task.finished_time:
if callable(self.text_on_done_format):
return Text(self.text_on_done_format(task), style=self.style)
else:
return Text(
self.text_on_done_format.format(task=task), style=self.style
)
else:
return Text(self.text_init_format.format(task=task), style=self.style)
[docs]class TimeContext:
"""Timing a code snippet with a context manager."""
time_attrs = ['years', 'months', 'days', 'hours', 'minutes', 'seconds']
def __init__(self, task_name: str, logger: 'JinaLogger' = None):
"""
Create the context manager to timing a code snippet.
:param task_name: The context/message.
:param logger: Use existing logger or use naive :func:`print`.
Example:
.. highlight:: python
.. code-block:: python
with TimeContext('loop'):
do_busy()
"""
self.task_name = task_name
self._logger = logger
self.duration = 0
def __enter__(self):
self.start = time.perf_counter()
self._enter_msg()
return self
def _enter_msg(self):
if self._logger:
self._logger.info(self.task_name + '...')
else:
print(self.task_name, end=' ...\t', flush=True)
def __exit__(self, typ, value, traceback):
self.duration = self.now()
self.readable_duration = get_readable_time(seconds=self.duration)
self._exit_msg()
[docs] def now(self) -> float:
"""
Get the passed time from start to now.
:return: passed time
"""
return time.perf_counter() - self.start
def _exit_msg(self):
if self._logger:
self._logger.info(
f'{self.task_name} takes {self.readable_duration} ({self.duration:.2f}s)'
)
else:
print(
colored(
f'{self.task_name} takes {self.readable_duration} ({self.duration:.2f}s)'
),
flush=True,
)