import time
from typing import Tuple, Any, Callable
import aerosandbox.numpy as np
[docs]class Timer(object):
"""
A context manager for timing things. Use it like this:
with Timer("My timer"): # You can optionally give it a name
# Do stuff
Results are printed to stdout. You can access the runtime (in seconds) directly by instantiating the object:
>>> t = Timer("My timer")
>>> t.tic()
>>> # Do stuff
>>> print(t.toc())
Nested timers are also supported. For example, this code:
>>> with Timer("a"):
>>> with Timer("b"):
>>> with Timer("c"):
>>> f()
prints the following console output:
[a] Timing...
[b] Timing...
[c] Timing...
[c] Elapsed: 100 msec
[b] Elapsed: 100 msec
[a] Elapsed: 100 msec
"""
[docs] number_running: int = 0 # The number of Timers currently running
def __init__(self,
name: str = None
):
self.name: str = name
self.runtime: float = np.nan
[docs] def __repr__(self):
return f"{self.__class__.__name__}: " + (
"Running..."
if np.isnan(self.runtime) else
f"Finished, elapsed: ({self._format_time(self.runtime)})"
)
@staticmethod
[docs] def _print(self, s: str, number_running_mod: int = 0):
header = "\t" * (self.__class__.number_running - 1 + number_running_mod)
if self.name:
header += f"[{self.name}] "
print(header + s)
[docs] def tic(self):
self.__class__.number_running += 1
self._print("Timing...")
self.t_start = time.perf_counter_ns()
[docs] def __enter__(self):
self.tic()
[docs] def toc(self) -> float:
self.t_end = time.perf_counter_ns()
self.__class__.number_running -= 1
self.runtime = (self.t_end - self.t_start) / 1e9
self._print(
f"Elapsed: {self._format_time(self.runtime)}",
number_running_mod=1
)
return self.runtime
[docs] def __exit__(self, type, value, traceback):
self.toc()
[docs]def time_function(
func: Callable,
repeats: int = None,
desired_runtime: float = None,
runtime_reduction=np.min,
) -> Tuple[float, Any]:
"""
Runs a given callable and tells you how long it took to run, in seconds. Also returns the result of the function
(if any), for good measure.
Args:
func: The function to run. Should take no arguments; use a lambda function or functools.partial if you need
to pass arguments.
repeats: The number of times to run the function. If None, runs until desired_runtime is met.
desired_runtime: The desired runtime of the function, in seconds. If None, runs until repeats is met.
runtime_reduction: A function that takes in a list of runtimes and returns a reduced value. For example,
np.min will return the minimum runtime, np.mean will return the mean runtime, etc. Default is np.min.
Returns: A Tuple of (time_taken, result).
- time_taken is a float of the time taken to run the function, in seconds.
- result is the result of the function, if any.
"""
if (repeats is not None) and (desired_runtime is not None):
raise ValueError("You can't specify both repeats and desired_runtime!")
def time_function_once():
start_ns = time.perf_counter_ns()
result = func()
return (
(time.perf_counter_ns() - start_ns) / 1e9,
result
)
runtimes = []
t, result = time_function_once()
if t == 0:
t = 1e-2
else:
runtimes.append(t)
if (desired_runtime is not None) and (repeats is None):
repeats = int(desired_runtime // t) - 1
# print(f"Running {func.__name__} {repeats} times to get a desired runtime of {desired_runtime} seconds.")
if repeats is None:
repeats = 0
for _ in range(repeats):
t, _ = time_function_once()
if t != 0:
runtimes.append(t)
if len(runtimes) == 0:
runtimes = [0.]
return (
runtime_reduction(runtimes),
result
)
if __name__ == '__main__':
[docs] def f():
time.sleep(0.1)
print(time_function(f, desired_runtime=1))
with Timer("a") as a:
with Timer("b") as b:
with Timer("c") as c:
f()
t = Timer()
t.tic()
time.sleep(0.1)
print(t.toc())