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

1import time 

2from collections import deque 

3from functools import wraps 

4from typing import Any, Callable 

5 

6from typing_extensions import Self 

7 

8 

9def time_it(loops: int = 1) -> Callable[..., Any]: 

10 """Decorator to time function execution time and print the results. 

11 

12 #### :params: 

13 

14 `loops`: How many times to execute the decorated function, 

15 starting and stopping the timer before and after each loop.""" 

16 

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 

30 

31 return wrapper 

32 

33 return decorator 

34 

35 

36class _Pauser: 

37 def __init__(self): 

38 self._pause_start = 0 

39 self._pause_total = 0 

40 self._paused = False 

41 

42 @property 

43 def paused(self) -> bool: 

44 """Whether this instance is paused or not.""" 

45 return self._paused 

46 

47 def pause(self): 

48 self._pause_start = time.time() 

49 self._paused = True 

50 

51 def unpause(self): 

52 self._pause_total += time.time() - self._pause_start 

53 self._paused = False 

54 

55 def reset(self): 

56 self._pause_start = 0 

57 self._pause_total = 0 

58 self._paused = False 

59 

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 

66 

67 

68class Timer: 

69 """Simple timer class that tracks total elapsed time 

70 and average time between calls to `start()` and `stop()`.""" 

71 

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. 

78 

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() 

89 

90 @property 

91 def started(self) -> bool: 

92 """Returns whether the timer has been started and is currently running.""" 

93 return self._started 

94 

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 

102 

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) 

107 

108 @property 

109 def average_elapsed(self) -> float: 

110 """Returns the average elapsed time.""" 

111 return self._average_elapsed 

112 

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) 

117 

118 @property 

119 def start_time(self) -> float: 

120 """Returns the timestamp of the last call to `start()`.""" 

121 return self._start_time 

122 

123 @property 

124 def stop_time(self) -> float: 

125 """Returns the timestamp of the last call to `stop()`.""" 

126 return self._stop_time 

127 

128 @property 

129 def history(self) -> deque[float]: 

130 """Returns the history buffer for this timer.""" 

131 return self._history 

132 

133 @property 

134 def is_paused(self) -> bool: 

135 return self._pauser.paused 

136 

137 def start(self: Self) -> Self: 

138 """Start the timer. 

139 

140 Returns this Timer instance so timer start can be chained to Timer creation if desired. 

141 

142 >>> timer = Timer().start()""" 

143 if not self.started: 

144 self._start_time = time.time() 

145 self._started = True 

146 return self 

147 

148 def stop(self): 

149 """Stop the timer. 

150 

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)) 

161 

162 def reset(self): 

163 """Calls stop() then start() for convenience.""" 

164 self.stop() 

165 self.start() 

166 

167 def pause(self): 

168 """Pause the timer.""" 

169 self._pauser.pause() 

170 

171 def unpause(self): 

172 """Unpause the timer.""" 

173 self._pauser.unpause() 

174 

175 @staticmethod 

176 def format_time(num_seconds: float, subsecond_resolution: bool = False) -> str: 

177 """Returns `num_seconds` as a string with units. 

178 

179 #### :params: 

180 

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() 

213 

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