Coverage for pyTooling / Stopwatch / __init__.py: 85%

208 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-01-08 23:46 +0000

1# ==================================================================================================================== # 

2# _____ _ _ ____ _ _ _ # 

3# _ __ _ |_ _|__ ___ | (_)_ __ __ _ / ___|| |_ ___ _ ____ ____ _| |_ ___| |__ # 

4# | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | \___ \| __/ _ \| '_ \ \ /\ / / _` | __/ __| '_ \ # 

5# | |_) | |_| || | (_) | (_) | | | | | | (_| |_ ___) | || (_) | |_) \ V V / (_| | || (__| | | | # 

6# | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____/ \__\___/| .__/ \_/\_/ \__,_|\__\___|_| |_| # 

7# |_| |___/ |___/ |_| # 

8# ==================================================================================================================== # 

9# Authors: # 

10# Patrick Lehmann # 

11# # 

12# License: # 

13# ==================================================================================================================== # 

14# Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # 

15# # 

16# Licensed under the Apache License, Version 2.0 (the "License"); # 

17# you may not use this file except in compliance with the License. # 

18# You may obtain a copy of the License at # 

19# # 

20# http://www.apache.org/licenses/LICENSE-2.0 # 

21# # 

22# Unless required by applicable law or agreed to in writing, software # 

23# distributed under the License is distributed on an "AS IS" BASIS, # 

24# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 

25# See the License for the specific language governing permissions and # 

26# limitations under the License. # 

27# # 

28# SPDX-License-Identifier: Apache-2.0 # 

29# ==================================================================================================================== # 

30# 

31""" 

32A stopwatch to measure execution times. 

33 

34.. hint:: 

35 

36 See :ref:`high-level help <COMMON/Stopwatch>` for explanations and usage examples. 

37""" 

38 

39from datetime import datetime 

40from time import perf_counter_ns 

41from types import TracebackType 

42from typing import List, Optional as Nullable, Iterator, Tuple, Type, Self 

43 

44try: 

45 from pyTooling.Decorators import export, readonly 

46 from pyTooling.MetaClasses import SlottedObject 

47 from pyTooling.Exceptions import ToolingException 

48 from pyTooling.Platform import CurrentPlatform 

49except (ImportError, ModuleNotFoundError): # pragma: no cover 

50 print("[pyTooling.Stopwatch] Could not import from 'pyTooling.*'!") 

51 

52 try: 

53 from Decorators import export, readonly 

54 from MetaClasses import SlottedObject 

55 from Exceptions import ToolingException 

56 from Platform import CurrentPlatform 

57 except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover 

58 print("[pyTooling.Stopwatch] Could not import directly!") 

59 raise ex 

60 

61 

62@export 

63class StopwatchException(ToolingException): 

64 """This exception is caused by wrong usage of the stopwatch.""" 

65 

66 

67@export 

68class ExcludeContextManager: 

69 """ 

70 A stopwatch context manager for excluding certain time spans from measurement. 

71 

72 While a normal stopwatch's embedded context manager (re)starts the stopwatch on every *enter* event and pauses the 

73 stopwatch on every *exit* event, this context manager pauses on *enter* events and restarts on every *exit* event. 

74 """ 

75 _stopwatch: "Stopwatch" #: Reference to the stopwatch. 

76 

77 def __init__(self, stopwatch: "Stopwatch") -> None: 

78 """ 

79 Initializes an excluding context manager. 

80 

81 :param stopwatch: Reference to the stopwatch. 

82 """ 

83 self._stopwatch = stopwatch 

84 

85 def __enter__(self) -> Self: 

86 """ 

87 Enter the context and pause the stopwatch. 

88 

89 :returns: Excluding stopwatch context manager instance. 

90 """ 

91 self._stopwatch.Pause() 

92 

93 return self 

94 

95 def __exit__( 

96 self, 

97 exc_type: Nullable[Type[BaseException]] = None, 

98 exc_val: Nullable[BaseException] = None, 

99 exc_tb: Nullable[TracebackType] = None 

100 ) -> Nullable[bool]: 

101 """ 

102 Exit the context and restart stopwatch. 

103 

104 :param exc_type: Exception type 

105 :param exc_val: Exception instance 

106 :param exc_tb: Exception's traceback. 

107 :returns: ``None`` 

108 """ 

109 self._stopwatch.Resume() 

110 

111 

112@export 

113class Stopwatch(SlottedObject): 

114 """ 

115 The stopwatch implements a solution to measure and collect timings. 

116 

117 The time measurement can be started, paused, resumed and stopped. More over, split times can be taken too. The 

118 measurement is based on :func:`time.perf_counter_ns`. Additionally, starting and stopping is preserved as absolute 

119 time via :meth:`datetime.datetime.now`. 

120 

121 Every split time taken is a time delta to the previous operation. These are preserved in an internal sequence of 

122 splits. This sequence includes time deltas of activity and inactivity. Thus, a running stopwatch can be split as well 

123 as a paused stopwatch. 

124 

125 The stopwatch can also be used in a :ref:`with-statement <with>`, because it implements the :ref:`context manager protocol <context-managers>`. 

126 """ 

127 

128 _name: Nullable[str] 

129 _preferPause: bool 

130 

131 _beginTime: Nullable[datetime] 

132 _endTime: Nullable[datetime] 

133 _startTime: Nullable[int] 

134 _resumeTime: Nullable[int] 

135 _pauseTime: Nullable[int] 

136 _stopTime: Nullable[int] 

137 _totalTime: Nullable[int] 

138 _splits: List[Tuple[float, bool]] 

139 

140 _excludeContextManager: ExcludeContextManager 

141 

142 def __init__(self, name: str = None, started: bool = False, preferPause: bool = False) -> None: 

143 """ 

144 Initializes the fields of the stopwatch. 

145 

146 If parameter ``started`` is set to true, the stopwatch will immediately start. 

147 

148 :param name: Optional name of the stopwatch. 

149 :param preferPause: Optional setting, if __exit__(...) in a contex should prefer pause or stop behavior. 

150 :param started: Optional flag, if the stopwatch should be started immediately. 

151 """ 

152 self._name = name 

153 self._preferPause = preferPause 

154 

155 self._endTime = None 

156 self._pauseTime = None 

157 self._stopTime = None 

158 self._totalTime = None 

159 self._splits = [] 

160 

161 self._excludeContextManager = None 

162 

163 if started is False: 163 ↛ 168line 163 didn't jump to line 168 because the condition on line 163 was always true

164 self._beginTime = None 

165 self._startTime = None 

166 self._resumeTime = None 

167 else: 

168 self._beginTime = datetime.now() 

169 self._resumeTime = self._startTime = perf_counter_ns() 

170 

171 def Start(self) -> None: 

172 """ 

173 Start the stopwatch. 

174 

175 A stopwatch can only be started once. There is no restart or reset operation provided. 

176 

177 :raises StopwatchException: If stopwatch was already started. 

178 :raises StopwatchException: If stopwatch was already started and stopped. 

179 """ 

180 if self._startTime is not None: 

181 raise StopwatchException("Stopwatch was already started.") 

182 if self._stopTime is not None: 182 ↛ 183line 182 didn't jump to line 183 because the condition on line 182 was never true

183 raise StopwatchException("Stopwatch was already used (started and stopped).") 

184 

185 self._beginTime = datetime.now() 

186 self._resumeTime = self._startTime = perf_counter_ns() 

187 

188 def Split(self) -> float: 

189 """ 

190 Take a split time and return the time delta to the previous stopwatch operation. 

191 

192 The stopwatch needs to be running to take a split time. See property :data:`IsRunning` to check if the stopwatch 

193 is running and the split operation is possible. |br| 

194 Depending on the previous operation, the time delta will be: 

195 

196 * the duration from start operation to the first split. 

197 * the duration from last resume to this split. 

198 

199 :returns: Duration in seconds since last stopwatch operation 

200 :raises StopwatchException: If stopwatch was not started or resumed. 

201 """ 

202 pauseTime = perf_counter_ns() 

203 

204 if self._resumeTime is None: 

205 raise StopwatchException("Stopwatch was not started or resumed.") 

206 

207 diff = (pauseTime - self._resumeTime) / 1e9 

208 self._splits.append((diff, True)) 

209 self._resumeTime = pauseTime 

210 

211 return diff 

212 

213 def Pause(self) -> float: 

214 """ 

215 Pause the stopwatch and return the time delta to the previous stopwatch operation. 

216 

217 The stopwatch needs to be running to pause it. See property :data:`IsRunning` to check if the stopwatch is running 

218 and the pause operation is possible. |br| 

219 Depending on the previous operation, the time delta will be: 

220 

221 * the duration from start operation to the first pause. 

222 * the duration from last resume to this pause. 

223 

224 :returns: Duration in seconds since last stopwatch operation 

225 :raises StopwatchException: If stopwatch was not started or resumed. 

226 """ 

227 self._pauseTime = perf_counter_ns() 

228 

229 if self._resumeTime is None: 

230 raise StopwatchException("Stopwatch was not started or resumed.") 

231 

232 diff = (self._pauseTime - self._resumeTime) / 1e9 

233 self._splits.append((diff, True)) 

234 self._resumeTime = None 

235 

236 return diff 

237 

238 def Resume(self) -> float: 

239 """ 

240 Resume the stopwatch and return the time delta to the previous pause operation. 

241 

242 The stopwatch needs to be paused to resume it. See property :data:`IsPaused` to check if the stopwatch is paused 

243 and the resume operation is possible. |br| 

244 The time delta will be the duration from last pause to this resume. 

245 

246 :returns: Duration in seconds since last pause operation 

247 :raises StopwatchException: If stopwatch was not paused. 

248 """ 

249 self._resumeTime = perf_counter_ns() 

250 

251 if self._pauseTime is None: 

252 raise StopwatchException("Stopwatch was not paused.") 

253 

254 diff = (self._resumeTime - self._pauseTime) / 1e9 

255 self._splits.append((diff, False)) 

256 self._pauseTime = None 

257 

258 return diff 

259 

260 def Stop(self) -> float: 

261 """ 

262 Stop the stopwatch and return the time delta to the previous stopwatch operation. 

263 

264 The stopwatch needs to be started to stop it. See property :data:`IsStarted` to check if the stopwatch was started 

265 and the stop operation is possible. |br| 

266 Depending on the previous operation, the time delta will be: 

267 

268 * the duration from start operation to the stop operation. 

269 * the duration from last resume to the stop operation. 

270 

271 :returns: Duration in seconds since last stopwatch operation 

272 :raises StopwatchException: If stopwatch was not started. 

273 :raises StopwatchException: If stopwatch was already stopped. 

274 """ 

275 self._stopTime = perf_counter_ns() 

276 self._endTime = datetime.now() 

277 

278 if self._startTime is None: 

279 raise StopwatchException("Stopwatch was never started.") 

280 if self._totalTime is not None: 280 ↛ 281line 280 didn't jump to line 281 because the condition on line 280 was never true

281 raise StopwatchException("Stopwatch was already stopped.") 

282 

283 if len(self._splits) == 0: # was never paused 

284 diff = (self._stopTime - self._startTime) / 1e9 

285 elif self._resumeTime is None: # is paused 

286 diff = (self._stopTime - self._pauseTime) / 1e9 

287 self._splits.append((diff, False)) 

288 else: # is running 

289 diff = (self._stopTime - self._resumeTime) / 1e9 

290 self._splits.append((diff, True)) 

291 

292 self._pauseTime = None 

293 self._resumeTime = None 

294 self._totalTime = self._stopTime - self._startTime 

295 

296 # FIXME: why is this unused? 

297 beginEndDiff = self._endTime - self._beginTime 

298 

299 return diff 

300 

301 @readonly 

302 def Name(self) -> Nullable[str]: 

303 """ 

304 Read-only property returning the name of the stopwatch. 

305 

306 :return: Name of the stopwatch. 

307 """ 

308 return self._name 

309 

310 @readonly 

311 def IsStarted(self) -> bool: 

312 """ 

313 Read-only property returning the IsStarted state of the stopwatch. 

314 

315 :return: True, if stopwatch was started. 

316 """ 

317 return self._startTime is not None and self._stopTime is None 

318 

319 @readonly 

320 def IsRunning(self) -> bool: 

321 """ 

322 Read-only property returning the IsRunning state of the stopwatch. 

323 

324 :return: True, if stopwatch was started and is currently not paused. 

325 """ 

326 return self._startTime is not None and self._resumeTime is not None 

327 

328 @readonly 

329 def IsPaused(self) -> bool: 

330 """ 

331 Read-only property returning the IsPaused state of the stopwatch. 

332 

333 :return: True, if stopwatch was started and is currently paused. 

334 """ 

335 return self._startTime is not None and self._pauseTime is not None 

336 

337 @readonly 

338 def IsStopped(self) -> bool: 

339 """ 

340 Read-only property returning the IsStopped state of the stopwatch. 

341 

342 :return: True, if stopwatch was stopped. 

343 """ 

344 return self._stopTime is not None 

345 

346 @readonly 

347 def StartTime(self) -> Nullable[datetime]: 

348 """ 

349 Read-only property returning the absolute time when the stopwatch was started. 

350 

351 :return: The time when the stopwatch was started, otherwise None. 

352 """ 

353 return self._beginTime 

354 

355 @readonly 

356 def StopTime(self) -> Nullable[datetime]: 

357 """ 

358 Read-only property returning the absolute time when the stopwatch was stopped. 

359 

360 :return: The time when the stopwatch was stopped, otherwise None. 

361 """ 

362 return self._endTime 

363 

364 @readonly 

365 def HasSplitTimes(self) -> bool: 

366 """ 

367 Read-only property checking if split times have been taken. 

368 

369 :return: True, if split times have been taken. 

370 """ 

371 return len(self._splits) > 1 

372 

373 @readonly 

374 def SplitCount(self) -> int: 

375 """ 

376 Read-only property returning the number of split times. 

377 

378 :return: Number of split times. 

379 """ 

380 return len(self._splits) 

381 

382 @readonly 

383 def ActiveCount(self) -> int: 

384 """ 

385 Read-only property returning the number of active split times. 

386 

387 :return: Number of active split times. 

388 

389 .. warning:: 

390 

391 This won't include all activities, unless the stopwatch got stopped. 

392 """ 

393 if self._startTime is None: 393 ↛ 394line 393 didn't jump to line 394 because the condition on line 393 was never true

394 return 0 

395 

396 return len(list(t for t, a in self._splits if a is True)) 

397 

398 @readonly 

399 def InactiveCount(self) -> int: 

400 """ 

401 Read-only property returning the number of active split times. 

402 

403 :return: Number of active split times. 

404 

405 .. warning:: 

406 

407 This won't include all inactivities, unless the stopwatch got stopped. 

408 """ 

409 if self._startTime is None: 409 ↛ 410line 409 didn't jump to line 410 because the condition on line 409 was never true

410 return 0 

411 

412 return len(list(t for t, a in self._splits if a is False)) 

413 

414 @readonly 

415 def Activity(self) -> float: 

416 """ 

417 Read-only property returning the duration of all active split times. 

418 

419 If the stopwatch is currently running, the duration since start or last resume operation will be included. 

420 

421 :return: Duration of all active split times in seconds. If the stopwatch was never started, the return value will 

422 be 0.0. 

423 """ 

424 if self._startTime is None: 424 ↛ 425line 424 didn't jump to line 425 because the condition on line 424 was never true

425 return 0.0 

426 

427 currentDiff = 0.0 if self._resumeTime is None else ((perf_counter_ns() - self._resumeTime) / 1e9) 

428 return sum(t for t, a in self._splits if a is True) + currentDiff 

429 

430 @readonly 

431 def Inactivity(self) -> float: 

432 """ 

433 Read-only property returning the duration of all inactive split times. 

434 

435 If the stopwatch is currently paused, the duration since last pause operation will be included. 

436 

437 :return: Duration of all inactive split times in seconds. If the stopwatch was never started, the return value will 

438 be 0.0. 

439 """ 

440 if self._startTime is None: 440 ↛ 441line 440 didn't jump to line 441 because the condition on line 440 was never true

441 return 0.0 

442 

443 currentDiff = 0.0 if self._pauseTime is None else ((perf_counter_ns() - self._pauseTime) / 1e9) 

444 return sum(t for t, a in self._splits if a is False) + currentDiff 

445 

446 @readonly 

447 def Duration(self) -> float: 

448 """ 

449 Read-only property returning the duration from start operation to stop operation. 

450 

451 If the stopwatch is not yet stopped, the duration from start to now is returned. 

452 

453 :return: Duration since stopwatch was started in seconds. If the stopwatch was never started, the return value will 

454 be 0.0. 

455 """ 

456 if self._startTime is None: 456 ↛ 457line 456 didn't jump to line 457 because the condition on line 456 was never true

457 return 0.0 

458 

459 return ((perf_counter_ns() - self._startTime) if self._stopTime is None else self._totalTime) / 1e9 

460 

461 @readonly 

462 def Exclude(self) -> ExcludeContextManager: 

463 """ 

464 Return an *exclude* context manager for the stopwatch instance. 

465 

466 :returns: An excluding context manager. 

467 """ 

468 if self._excludeContextManager is None: 

469 excludeContextManager = ExcludeContextManager(self) 

470 self._excludeContextManager = excludeContextManager 

471 

472 return excludeContextManager 

473 

474 def __enter__(self) -> Self: 

475 """ 

476 Implementation of the :ref:`context manager protocol's <context-managers>` ``__enter__(...)`` method. 

477 

478 An unstarted stopwatch will be started. A paused stopwatch will be resumed. 

479 

480 :return: The stopwatch itself. 

481 """ 

482 if self._startTime is None: # start stopwatch 

483 self._beginTime = datetime.now() 

484 self._resumeTime = self._startTime = perf_counter_ns() 

485 elif self._pauseTime is not None: # resume after pause 

486 self._resumeTime = perf_counter_ns() 

487 

488 diff = (self._resumeTime - self._pauseTime) / 1e9 

489 self._splits.append((diff, False)) 

490 self._pauseTime = None 

491 elif self._resumeTime is not None: # is running? 491 ↛ 492line 491 didn't jump to line 492 because the condition on line 491 was never true

492 raise StopwatchException("Stopwatch is currently running and can not be started/resumed again.") 

493 elif self._stopTime is not None: # is stopped? 493 ↛ 496line 493 didn't jump to line 496 because the condition on line 493 was always true

494 raise StopwatchException(f"Stopwatch was already stopped.") 

495 else: 

496 raise StopwatchException(f"Internal error.") 

497 

498 return self 

499 

500 def __exit__( 

501 self, 

502 exc_type: Nullable[Type[BaseException]] = None, 

503 exc_val: Nullable[BaseException] = None, 

504 exc_tb: Nullable[TracebackType] = None 

505 ) -> Nullable[bool]: 

506 """ 

507 Implementation of the :ref:`context manager protocol's <context-managers>` ``__exit__(...)`` method. 

508 

509 A running stopwatch will be paused or stopped depending on the configured ``preferPause`` behavior. 

510 

511 :param exc_type: Exception type, otherwise None. 

512 :param exc_val: Exception object, otherwise None. 

513 :param exc_tb: Exception's traceback, otherwise None. 

514 :returns: True, if exceptions should be suppressed. 

515 """ 

516 if self._startTime is None: # never started? 516 ↛ 517line 516 didn't jump to line 517 because the condition on line 516 was never true

517 raise StopwatchException("Stopwatch was never started.") 

518 elif self._stopTime is not None: 518 ↛ 519line 518 didn't jump to line 519 because the condition on line 518 was never true

519 raise StopwatchException("Stopwatch was already stopped.") 

520 elif self._resumeTime is not None: # pause or stop 520 ↛ 537line 520 didn't jump to line 537 because the condition on line 520 was always true

521 if self._preferPause: 

522 self._pauseTime = perf_counter_ns() 

523 diff = (self._pauseTime - self._resumeTime) / 1e9 

524 self._splits.append((diff, True)) 

525 self._resumeTime = None 

526 else: 

527 self._stopTime = perf_counter_ns() 

528 self._endTime = datetime.now() 

529 

530 diff = (self._stopTime - self._resumeTime) / 1e9 

531 self._splits.append((diff, True)) 

532 

533 self._pauseTime = None 

534 self._resumeTime = None 

535 self._totalTime = self._stopTime - self._startTime 

536 else: 

537 raise StopwatchException("Stopwatch was not resumed.") 

538 

539 def __len__(self) -> int: 

540 """ 

541 Implementation of ``len(...)`` to return the number of split times. 

542 

543 :return: Number of split times. 

544 """ 

545 return len(self._splits) 

546 

547 def __getitem__(self, index: int) -> Tuple[float, bool]: 

548 """ 

549 Implementation of ``split = object[i]`` to return the i-th split time. 

550 

551 :param index: Index to access the i-th split time. 

552 :return: i-th split time as a tuple of: |br| 

553 (1) delta time to the previous stopwatch operation and |br| 

554 (2) a boolean indicating if the split was an activity (true) or inactivity (false). 

555 :raises KeyError: If index *i* doesn't exist. 

556 """ 

557 return self._splits[index] 

558 

559 def __iter__(self) -> Iterator[Tuple[float, bool]]: 

560 """ 

561 Return an iterator of tuples to iterate all split times. 

562 

563 If the stopwatch is not stopped yet, the last split won't be included. 

564 

565 :return: Iterator of split time tuples of: |br| 

566 (1) delta time to the previous stopwatch operation and |br| 

567 (2) a boolean indicating if the split was an activity (true) or inactivity (false). 

568 """ 

569 return self._splits.__iter__() 

570 

571 def __str__(self) -> str: 

572 """ 

573 Returns the stopwatch's state and its measured time span. 

574 

575 :returns: The string equivalent of the stopwatch. 

576 """ 

577 name = f" {self._name}" if self._name is not None else "" 

578 if self.IsStopped: 

579 return f"Stopwatch{name} (stopped): {self._beginTime} -> {self._endTime}: {self._totalTime}" 

580 elif self.IsRunning: 

581 return f"Stopwatch{name} (running): {self._beginTime} -> now: {self.Duration}" 

582 elif self.IsPaused: 

583 return f"Stopwatch{name} (paused): {self._beginTime} -> now: {self.Duration}" 

584 else: 

585 return f"Stopwatch{name}: not started"