Coverage for /Users/martin/prj/git/benchman_pre/src/benchman/timings.py: 35%
62 statements
« prev ^ index » next coverage.py v7.6.4, created at 2024-12-24 08:16 +0100
« prev ^ index » next coverage.py v7.6.4, created at 2024-12-24 08:16 +0100
1# (c) 2024 Martin Wendt; see https://github.com/mar10/benchman
2# Licensed under the MIT license: https://www.opensource.org/licenses/mit-license.php
3""" """
4# ruff: noqa: T201, T203 `print` found
6import logging
7import time
8import timeit
9from dataclasses import dataclass
10from textwrap import dedent
11from typing import Any, Optional
13from benchman.util import byte_number_string, format_time
15logger = logging.getLogger("benchman.timings")
18class Timing:
19 def __init__(self, name: str) -> None:
20 self.name = name
21 self.start = time.monotonic()
22 self.elap = None
24 def __repr__(self):
25 if self.elap is None:
26 elap = time.monotonic() - self.start
27 return f"Timing<{self.name}> Running since {elap}..."
28 return f"Timing<{self.name}> took {self.elap}."
30 def __enter__(self):
31 self.start = time.monotonic()
32 return self
34 def __exit__(self, exc_type, exc_val, exc_tb):
35 self.elap = time.monotonic() - self.start
36 # print(f"{self}")
39@dataclass
40class TimingsResult:
41 name: str
42 iterations: int
43 repeat: int
44 timings: list[float]
46 def __str__(self):
47 best = min(self.timings)
48 return "{}: {:,d} loop{}, best of {:,}: {} per loop ({} per sec.)".format(
49 self.name,
50 self.iterations,
51 "" if self.iterations == 1 else "s",
52 self.repeat,
53 format_time(best),
54 byte_number_string(self.iterations / best),
55 )
57 def __repr__(self):
58 best = min(self.timings)
59 return (
60 f"TimingsResult<{self.name}, {self.iterations} loops, "
61 f"best of {self.repeat}: {best:.3f} sec. per loop>"
62 )
65def run_timings(
66 name: str,
67 stmt: str,
68 *,
69 #: A setup statement to execute before the main statement.
70 setup: str = "pass",
71 #: Verbosity level (0: quiet, 1: normal, 2: verbose)
72 verbose: int = 0,
73 #: Number of times to repeat the test.
74 repeat: int = 5, # timeit.default_repeat
75 #: Number of loops to run.
76 #: If 0, `timeit` will determine the iterations automatically.
77 iterations: int = 0,
78 #: A dict containing the global variables.
79 globals: Optional[dict[str, Any]] = None,
80 #: Use `time.process_time` instead of `time.monotonic` for measuring CPU time.
81 process_time: bool = False,
82) -> TimingsResult:
83 """Taken from Python `timeit.main()` module."""
84 timer = timeit.default_timer
85 if process_time:
86 timer = time.process_time
87 precision = 4 if verbose > 0 else 3
89 stmt = dedent(stmt).strip()
90 if isinstance(setup, str):
91 setup = dedent(setup).strip()
93 t = timeit.Timer(stmt, setup, timer, globals=globals)
95 if iterations == 0:
96 # determine iterations so that 0.2 <= total time < 2.0
97 callback = None # type: ignore
98 if verbose > 0:
100 def callback(iterations: int, time_taken: float):
101 msg = "{num} loop{s} -> {secs:.{prec}g} secs"
102 plural = iterations != 1
103 print(
104 msg.format(
105 num=iterations,
106 s="s" if plural else "",
107 secs=time_taken,
108 prec=precision,
109 )
110 )
112 iterations, _ = t.autorange(callback)
114 if verbose:
115 print()
117 raw_timings = t.repeat(repeat, iterations)
119 timings = [dt / iterations for dt in raw_timings]
121 best = min(timings)
122 worst = max(timings)
123 if worst >= best * 4:
124 import warnings
126 warnings.warn_explicit(
127 f"The test results of {name} are likely unreliable. "
128 f"The worst time ({format_time(worst)}) was more than four times "
129 f"slower than the best time ({format_time(best)}).",
130 UserWarning,
131 "",
132 0,
133 )
134 return TimingsResult(
135 name=name,
136 iterations=iterations,
137 repeat=repeat,
138 timings=timings,
139 )