-
Notifications
You must be signed in to change notification settings - Fork 2
/
simplestats.py
136 lines (106 loc) · 3.34 KB
/
simplestats.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
"""Collect some simple performance statistics"""
import atexit
import threading
import time
from copy import copy
from dataclasses import dataclass
from sys import stderr
from typing import Callable, ParamSpec, TextIO, TypeVar
_lock = threading.Lock()
class Timer:
"""
Collect performance statistics for a named block of code.
- Auto start/stop via `with`:
```python
with Timer("my code"):
# code to time
```
- Manual start/stop:
```python
t = Timer("my code")
t.start()
# code to time
t.stop()
```
"""
@dataclass
class Stats:
"""Statistics tracked for each timer"""
name: str
count: int = 0
elapsed_ns: int = 0
# Database of all timer statistics
_db: dict[str, Stats] = {}
@classmethod
def clear(cls) -> None:
"""Clear the timer statistics"""
with _lock:
cls._db.clear()
@classmethod
def dump(cls, out: TextIO = stderr) -> None:
"""Dump the timer statistics"""
with _lock:
if not cls._db:
return # No timers have been used
name_size = max(len(t.name) for t in cls._db.values()) + 1
header = f"{'Name':<{name_size}} {'Count':>8} {'Avg':>8} {'Total':>10}"
print(header, file=out)
print("-" * len(header), file=out)
for t in cls._db.values():
elapsed_s = t.elapsed_ns / 1000000000
print(
f"{t.name+":":<{name_size}} {t.count:>8}"
+ f" {elapsed_s/t.count:>8.3f}s {elapsed_s:>10.3f}s",
file=out,
)
@classmethod
def stats(cls, name: str) -> Stats:
"""Get the statistics for a named timer"""
with _lock:
stats = cls._db.get(name) or cls.Stats(name)
return copy(stats)
def _save(self) -> None:
with _lock:
stats = Timer._db.get(self.name) or Timer.Stats(self.name)
stats.count += 1
stats.elapsed_ns += self.elapsed_ns
Timer._db[self.name] = stats
def __init__(self, name: str, autostart: bool = False):
self.name = name
self._start = None
self.elapsed_ns = 0
if autostart:
self.start()
def start(self):
"""Start the timer"""
self._start = time.perf_counter_ns()
def stop(self):
"""Stop the timer"""
stop = time.perf_counter_ns()
if self._start is not None:
self.elapsed_ns += stop - self._start
self._start = None
self._save()
def __enter__(self):
self.start()
return self
def __exit__(self, *args):
self.stop()
_P = ParamSpec("_P")
_T = TypeVar("_T")
def measure_function(func: Callable[_P, _T]) -> Callable[_P, _T]:
"""
Decorator to measure the time spent in a function.
The timer will be automatically named after the decorated function.
```python
@measure_function
def my_function():
# code to time
```
"""
def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _T:
with Timer(func.__qualname__):
return func(*args, **kwargs)
return wrapper
# Automatically dump the timer statistics to stderr when the program exits
atexit.register(Timer.dump)