Coverage for src\noiftimer\noiftimer.py: 99%
126 statements
« prev ^ index » next coverage.py v7.2.2, created at 2024-02-16 17:59 -0600
« prev ^ index » next coverage.py v7.2.2, created at 2024-02-16 17:59 -0600
1import time
2from collections import deque
3from functools import wraps
4from typing import Any, Callable
6from typing_extensions import Self
9def time_it(loops: int = 1) -> Callable[..., Any]:
10 """Decorator to time function execution time and print the results.
12 #### :params:
14 `loops`: How many times to execute the decorated function,
15 starting and stopping the timer before and after each loop."""
17 def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
18 @wraps(func)
19 def wrapper(*args: Any, **kwargs: Any) -> Any:
20 timer = Timer(loops)
21 result = None
22 for _ in range(loops):
23 timer.start()
24 result = func(*args, **kwargs)
25 timer.stop()
26 print(
27 f"{func.__name__} {'average ' if loops > 1 else ''}execution time: {timer.average_elapsed_str}"
28 )
29 return result
31 return wrapper
33 return decorator
36class _Pauser:
37 def __init__(self):
38 self._pause_start = 0
39 self._pause_total = 0
40 self._paused = False
42 @property
43 def paused(self) -> bool:
44 """Whether this instance is paused or not."""
45 return self._paused
47 def pause(self):
48 self._pause_start = time.time()
49 self._paused = True
51 def unpause(self):
52 self._pause_total += time.time() - self._pause_start
53 self._paused = False
55 def reset(self):
56 self._pause_start = 0
57 self._pause_total = 0
58 self._paused = False
60 @property
61 def pause_total(self) -> float:
62 if self._paused:
63 return self._pause_total + (time.time() - self._pause_start)
64 else:
65 return self._pause_total
68class Timer:
69 """Simple timer class that tracks total elapsed time
70 and average time between calls to `start()` and `stop()`."""
72 def __init__(
73 self, averaging_window_length: int = 10, subsecond_resolution: bool = True
74 ):
75 """
76 #### :params:
77 * `averaging_window_length`: Number of start/stop cycles to calculate the average elapsed time with.
79 * `subsecond_resolution`: Whether to print formatted time strings with subsecond resolution or not.
80 """
81 self._start_time = time.time()
82 self._stop_time = self.start_time
83 self._elapsed = 0
84 self._average_elapsed = 0
85 self._history: deque[float] = deque([], averaging_window_length)
86 self._started: bool = False
87 self.subsecond_resolution = subsecond_resolution
88 self._pauser = _Pauser()
90 @property
91 def started(self) -> bool:
92 """Returns whether the timer has been started and is currently running."""
93 return self._started
95 @property
96 def elapsed(self) -> float:
97 """Returns the currently elapsed time."""
98 if self._started:
99 return time.time() - self._start_time - self._pauser.pause_total
100 else:
101 return self._elapsed
103 @property
104 def elapsed_str(self) -> str:
105 """Returns the currently elapsed time as a formatted string."""
106 return self.format_time(self.elapsed, self.subsecond_resolution)
108 @property
109 def average_elapsed(self) -> float:
110 """Returns the average elapsed time."""
111 return self._average_elapsed
113 @property
114 def average_elapsed_str(self) -> str:
115 """Returns the average elapsed time as a formatted string."""
116 return self.format_time(self._average_elapsed, self.subsecond_resolution)
118 @property
119 def start_time(self) -> float:
120 """Returns the timestamp of the last call to `start()`."""
121 return self._start_time
123 @property
124 def stop_time(self) -> float:
125 """Returns the timestamp of the last call to `stop()`."""
126 return self._stop_time
128 @property
129 def history(self) -> deque[float]:
130 """Returns the history buffer for this timer."""
131 return self._history
133 @property
134 def is_paused(self) -> bool:
135 return self._pauser.paused
137 def start(self: Self) -> Self:
138 """Start the timer.
140 Returns this Timer instance so timer start can be chained to Timer creation if desired.
142 >>> timer = Timer().start()"""
143 if not self.started:
144 self._start_time = time.time()
145 self._started = True
146 return self
148 def stop(self):
149 """Stop the timer.
151 Calculates elapsed time and average elapsed time."""
152 if self.started:
153 self._stop_time = time.time()
154 self._started = False
155 self._elapsed = (
156 self._stop_time - self._start_time - self._pauser.pause_total
157 )
158 self._pauser.reset()
159 self._history.append(self._elapsed)
160 self._average_elapsed = sum(self._history) / (len(self._history))
162 def reset(self):
163 """Calls stop() then start() for convenience."""
164 self.stop()
165 self.start()
167 def pause(self):
168 """Pause the timer."""
169 self._pauser.pause()
171 def unpause(self):
172 """Unpause the timer."""
173 self._pauser.unpause()
175 @staticmethod
176 def format_time(num_seconds: float, subsecond_resolution: bool = False) -> str:
177 """Returns `num_seconds` as a string with units.
179 #### :params:
181 `subsecond_resolution`: Include milliseconds and microseconds with the output.
182 """
183 microsecond = 0.000001
184 millisecond = 0.001
185 second = 1
186 seconds_per_minute = 60
187 seconds_per_hour = 3600
188 seconds_per_day = 86400
189 seconds_per_week = 604800
190 seconds_per_month = 2419200
191 seconds_per_year = 29030400
192 time_units = [
193 (seconds_per_year, "y"),
194 (seconds_per_month, "mn"),
195 (seconds_per_week, "w"),
196 (seconds_per_day, "d"),
197 (seconds_per_hour, "h"),
198 (seconds_per_minute, "m"),
199 (second, "s"),
200 (millisecond, "ms"),
201 (microsecond, "us"),
202 ]
203 if not subsecond_resolution:
204 time_units = time_units[:-2]
205 time_string = ""
206 for time_unit in time_units:
207 unit_amount, num_seconds = divmod(num_seconds, time_unit[0])
208 if unit_amount > 0:
209 time_string += f"{int(unit_amount)}{time_unit[1]} "
210 if time_string == "":
211 return f"<1{time_units[-1][1]}"
212 return time_string.strip()
214 @property
215 def stats(self) -> str:
216 """Returns a string stating the currently elapsed time and the average elapsed time."""
217 return f"elapsed time: {self.elapsed_str}\naverage elapsed time: {self.average_elapsed_str}"