Skip to content

Commit

Permalink
style(add type hints):
Browse files Browse the repository at this point in the history
  • Loading branch information
Zeyan Li 李则言 committed Mar 7, 2024
1 parent d331685 commit acbf712
Show file tree
Hide file tree
Showing 13 changed files with 562 additions and 104 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/pythonpackage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.9]
python-version: [3.9, 3.10, 3.11, 3.12]

steps:
- uses: actions/checkout@v2
Expand All @@ -27,8 +27,8 @@ jobs:
key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements-dev.txt') }}
- name: Install dependencies
run: |
if [ -f requirements-dev.txt ]; then pip install -r requirements-dev.txt; fi
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
pip install -U poetry
poetry install
- name: Test with pytest
run: |
PYTHONPATH=$(realpath .) coverage run -m pytest tests
Expand Down
5 changes: 4 additions & 1 deletion .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .idea/pyprof.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/ruff.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 0 additions & 3 deletions build_and_install.sh

This file was deleted.

467 changes: 467 additions & 0 deletions poetry.lock

Large diffs are not rendered by default.

23 changes: 14 additions & 9 deletions pyprof/prof_proxy.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
from collections import defaultdict
from functools import wraps
from threading import Thread, current_thread
from typing import overload, Callable, Any, Union, Dict, List, Tuple, Optional

from typing_extensions import overload, Callable, Any, Union, Dict, List, Tuple, Optional, TypeVar, ParamSpec, TypeAlias

from .pyprof import Profiler

T = TypeVar('T')
P = ParamSpec('P')
FuncType: TypeAlias = Callable[P, T]


class ProfilerProxy:
active_proxy: Dict[Thread, List[Tuple["ProfilerProxy", "Profiler"]]] = defaultdict(list)

def __init__(
self, name: str, report_printer: Callable[[str], Any] = None, flush=False,
self, name: str, report_printer: Optional[Callable[[str], Any]] = None, flush=False,
min_total_percent: float = 0., min_parent_percent: float = 0.
):
self.name = name
Expand All @@ -24,7 +29,7 @@ def nearest_proxy(cls) -> Optional[Tuple['ProfilerProxy', Profiler]]:
current_stack = cls.active_proxy[current_thread()]
return current_stack[-1] if current_stack else None

def __enter__(self):
def __enter__(self) -> Profiler:
profiler = Profiler(
self.name,
current_profiler(),
Expand All @@ -46,7 +51,7 @@ def __exit__(self, exc_type, exc_val, exc_tb):
)
)

def __call__(self, func: Callable):
def __call__(self, func: FuncType) -> FuncType:
@wraps(func)
def wrapper(*args, **kwargs):
with self:
Expand All @@ -57,24 +62,24 @@ def wrapper(*args, **kwargs):

@overload
def profile(
name: str, *, report_printer=None, flush: bool = False,
arg: str, *, report_printer=None, flush: bool = False,
min_total_percent: float = 0.,
min_parent_percent: float = 0.,
) -> Callable:
) -> ProfilerProxy:
...


@overload
def profile(func: Callable) -> Callable:
def profile(arg: FuncType) -> FuncType:
...


def profile(
arg: Union[str, Callable], *, report_printer=None,
arg: Union[str, FuncType], *, report_printer=None,
flush: bool = False,
min_total_percent: float = 0.,
min_parent_percent: float = 0.,
) -> Callable:
) -> Union[FuncType, ProfilerProxy]:
# work as a context manager
if isinstance(arg, str):
return ProfilerProxy(
Expand Down
56 changes: 45 additions & 11 deletions pyprof/pyprof.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,17 @@
import warnings
from functools import lru_cache, wraps
from io import StringIO
from math import sqrt
from threading import current_thread, Thread, Lock
from typing import Callable, Dict, Set, List

from math import sqrt
from typing_extensions import TypedDict, Callable, Dict, Set, List, Optional, Union, NotRequired


class CachedStatistics(TypedDict):
sorted_elapsed_times: NotRequired[list[float]]
total: NotRequired[float]
average: NotRequired[float]
std: NotRequired[float]


class Profiler:
Expand All @@ -21,7 +29,7 @@ def get(full_path: str) -> "Profiler":
return Profiler._instances[full_path]

@staticmethod
def _generate_full_path(name: str, parent: "Profiler"):
def _generate_full_path(name: str, parent: Union["Profiler", None]):
"""
Format the full path given the current name the parent Profiler
:param name:
Expand All @@ -40,7 +48,7 @@ def _generate_full_path(name: str, parent: "Profiler"):
full_path = f"{parent.full_path if parent is not None else ''}/{name}"
return parent, full_path

def _need_init(self, flush=False):
def _need_init(self, flush=False) -> bool:
"""
1. _initialized is not set
2. flush=True
Expand All @@ -50,8 +58,9 @@ def _need_init(self, flush=False):
"""
if not hasattr(self, '_initialized') or flush:
return True
return False

def __init__(self, name: str = "", parent: "Profiler" = None, flush=False):
def __init__(self, name: str = "", parent: Optional["Profiler"] = None, flush=False):
"""
If the Profiler is inited before, then the __init__ will be skipped
:param name:
Expand All @@ -75,8 +84,8 @@ def __init__(self, name: str = "", parent: "Profiler" = None, flush=False):
_._destroy()
self._children: Set["Profiler"] = set()

self._elapsed_times = []
self._cached_statistics = {}
self._elapsed_times: list[float] = []
self._cached_statistics: CachedStatistics = {}

self._tics: Dict[Thread, float] = {}
self._initialized = time.perf_counter()
Expand All @@ -88,7 +97,7 @@ def _destroy(self):
_._destroy()
self._children = set()

def __new__(cls, name: str = "", parent: "Profiler" = None, flush=False):
def __new__(cls, name: str = "", parent: Optional["Profiler"] = None, flush=False):
parent, full_path = Profiler._generate_full_path(name, parent)
if full_path not in Profiler._instances:
Profiler._instances[full_path] = super(Profiler, cls).__new__(cls)
Expand Down Expand Up @@ -129,7 +138,7 @@ def __fill_parent_times_if_not_triggered(profiler: 'Profiler', elapsed_time: flo
profiler._cached_statistics = {}
Profiler.__fill_parent_times_if_not_triggered(profiler._parent, elapsed_time)

def toc(self):
def toc(self) -> None:
"""
Record the difference between the most recent tic and clean the tic
:return:
Expand Down Expand Up @@ -182,6 +191,10 @@ def total(self) -> float:
self._cached_statistics['total'] = sum(self.times)
return self._cached_statistics['total']

@property
def total_seconds(self) -> float:
return self.total

def tail(self, percentile: float) -> float:
if self.count == 0:
return 0
Expand All @@ -196,6 +209,10 @@ def average(self) -> float:
self._cached_statistics['average'] = sum(self.times) / self.count
return self._cached_statistics['average']

@property
def mean(self) -> float:
return self.average

@property
def standard_deviation(self) -> float:
if self.count == 0:
Expand All @@ -204,24 +221,39 @@ def standard_deviation(self) -> float:
self._cached_statistics['std'] = sqrt(sum([(_ - self.average) ** 2 for _ in self.times]) / self.count)
return self._cached_statistics['std']

@property
def std(self) -> float:
return self.standard_deviation

@property
def min_time(self) -> float:
if self.count == 0:
return 0
return self.sorted_times[0]

@property
def min(self) -> float:
return self.min_time

@property
def max_time(self) -> float:
if self.count == 0:
return 0
return self.sorted_times[-1]

@property
def max(self) -> float:
return self.max_time

def report(self, full_path_width=None, min_total_percent: float = 0, min_parent_percent: float = 0) -> str:
if full_path_width is not None:
full_path_width = full_path_width
else:
full_path_width = self._max_children_full_path_length()
total_percent = self.total / max(_root_profiler.total, 1e-4) * 100
if _root_profiler is None:
total_percent = 100.
else:
total_percent = self.total / max(_root_profiler.total, 1e-4) * 100
if self._parent is not None:
parent_percent = self.total / max(self._parent.total, 1e-4) * 100
else:
Expand Down Expand Up @@ -293,12 +325,14 @@ def clean():


def report(min_total_percent: float = 0., min_parent_percent: float = 0.) -> str:
if _root_profiler is None:
return ""
body = _root_profiler.report(min_total_percent=min_total_percent, min_parent_percent=min_parent_percent)
return f'{_root_profiler.report_header()}{body}'


# noinspection PyTypeChecker
_root_profiler = None # type: Profiler
_root_profiler: Union[Profiler, None] = None
clean()

__all__ = ["Profiler", "clean", "report"]
22 changes: 22 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[tool.poetry]
name = "pyprof"
version = "0.1.0"
description = "This package focus on build time profiler for python functions and snippets."
authors = ["Zeyan Li 李则言 <lizeyan.42@bytedance.com>"]
license = "MIT"
readme = "README.md"

[tool.poetry.dependencies]
python = ">=3.9"


[tool.poetry.group.dev.dependencies]
coverage = ">=5.5,<6.0"
pytest = "^8.0.2"
coveralls = "^3.3.1"
numpy = "^1.26.4"
mypy = "^1.8.0"

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
7 changes: 0 additions & 7 deletions requirements-dev.txt

This file was deleted.

Empty file removed requirements.txt
Empty file.
67 changes: 0 additions & 67 deletions setup.py

This file was deleted.

2 changes: 0 additions & 2 deletions upload_pypi.sh

This file was deleted.

0 comments on commit acbf712

Please sign in to comment.