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

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 

5 

6import logging 

7import time 

8import timeit 

9from dataclasses import dataclass 

10from textwrap import dedent 

11from typing import Any, Optional 

12 

13from benchman.util import byte_number_string, format_time 

14 

15logger = logging.getLogger("benchman.timings") 

16 

17 

18class Timing: 

19 def __init__(self, name: str) -> None: 

20 self.name = name 

21 self.start = time.monotonic() 

22 self.elap = None 

23 

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}." 

29 

30 def __enter__(self): 

31 self.start = time.monotonic() 

32 return self 

33 

34 def __exit__(self, exc_type, exc_val, exc_tb): 

35 self.elap = time.monotonic() - self.start 

36 # print(f"{self}") 

37 

38 

39@dataclass 

40class TimingsResult: 

41 name: str 

42 iterations: int 

43 repeat: int 

44 timings: list[float] 

45 

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 ) 

56 

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 ) 

63 

64 

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 

88 

89 stmt = dedent(stmt).strip() 

90 if isinstance(setup, str): 

91 setup = dedent(setup).strip() 

92 

93 t = timeit.Timer(stmt, setup, timer, globals=globals) 

94 

95 if iterations == 0: 

96 # determine iterations so that 0.2 <= total time < 2.0 

97 callback = None # type: ignore 

98 if verbose > 0: 

99 

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 ) 

111 

112 iterations, _ = t.autorange(callback) 

113 

114 if verbose: 

115 print() 

116 

117 raw_timings = t.repeat(repeat, iterations) 

118 

119 timings = [dt / iterations for dt in raw_timings] 

120 

121 best = min(timings) 

122 worst = max(timings) 

123 if worst >= best * 4: 

124 import warnings 

125 

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 )