Source code for stopwatch.stopwatch

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Stopwatch class for timing portions of python code
"""
# Created on Sun Feb 28 20:00:59 2021

__author__ = "Hrishikesh Terdalkar"

###############################################################################

import time
import logging
from collections import defaultdict
from collections.abc import Iterable
from dataclasses import dataclass

###############################################################################
# Constants

STATE_ACTIVE = "state_active"
STATE_INACTIVE = "state_inactive"
STATE_PAUSE = "state_pause"

ACTION_START = "action_start"
ACTION_STOP = "action_stop"
ACTION_PAUSE = "action_pause"
ACTION_RESUME = "action_resume"
ACTION_TICK = "action_tick"

ACTIONS = [ACTION_START, ACTION_STOP, ACTION_PAUSE, ACTION_RESUME, ACTION_TICK]

###############################################################################


[docs] @dataclass class Tick: id: int name: str time: float duration: float action: str
###############################################################################
[docs] class Stopwatch: """ Stopwatch Instance A typical lifecycle of the stopwatch: [creation] --> [start] --> [tick, pause, resume] --> [stop] """ def __init__(self, precision=None): self.__state = STATE_INACTIVE self.__ticks = [] self.__index_name = defaultdict(list) self.__index_action = defaultdict(list) self.precision = precision self.logger = logging.getLogger( f"{__name__}.{self.__class__.__name__}" ) # ----------------------------------------------------------------------- # def __perform_tick(self, name=None, action=ACTION_TICK): """Record a tick without any checks""" tick_time = time.perf_counter() - self.__start_time if self.precision: tick_time = round(tick_time, self.precision) if action == ACTION_START: tick_time = 0 tick_id = len(self.__ticks) if action is None or action not in ACTIONS: action = ACTION_TICK self.__index_name[name].append(tick_id) self.__index_action[action].append(tick_id) if tick_id > 0: last_tick = self.__ticks[-1] duration = tick_time - last_tick.time else: duration = 0 self.__ticks.append( Tick( id=tick_id, name=name, duration=duration, time=tick_time, action=action, ) ) # ----------------------------------------------------------------------- # # Actions
[docs] def start(self): """Start the stopwatch""" if self.__state == STATE_INACTIVE: self.__state = STATE_ACTIVE self.__ticks = [] self.__index_name = defaultdict(list) self.__index_action = defaultdict(list) self.__start_time = time.perf_counter() self.__perform_tick(action=ACTION_START) return True else: self.logger.warning("Stopwatch is already active.") return None
[docs] def tick(self, name=None): """ Record a tick Returns ------- Time since the last tick """ if self.__state == STATE_ACTIVE: if name is not None: name = str(name) self.__perform_tick(name=name) return self.last() else: self.logger.warning("Failed to record tick.") return None
[docs] def pause(self): """ Pause the stopwatch (Ticks are not recorded until resumed) """ if self.__state == STATE_ACTIVE: self.__state = STATE_PAUSE self.__perform_tick(action=ACTION_PAUSE) return True else: self.logger.warning("Failed to pause.") return None
[docs] def resume(self): """ Resume Returns ------- Time for which the instance was paused """ if self.__state == STATE_PAUSE: self.__state = STATE_ACTIVE self.__perform_tick(action=ACTION_RESUME) return self.last() else: self.logger.warning("Failed to resume.") return None
[docs] def stop(self): """ Stops the stopwatch Returns ------- Total time (including pause-time) """ if self.__state != STATE_INACTIVE: self.__state = STATE_INACTIVE self.__perform_tick(action=ACTION_STOP) return self.time_active else: self.logger.warning("Stopwatch is already inactive.") return None
# ----------------------------------------------------------------------- # # Calculated Properties
[docs] def get_time_paused(self, start_idx=0, end_idx=-1): """Get pause-time between different ticks""" pause_time = 0 pause_start = 0 pause_end = 0 _end_idx = end_idx + 1 ticks = ( self.__ticks[start_idx:_end_idx] if _end_idx else self.__ticks[start_idx:] ) for tick in ticks: if tick.action == ACTION_PAUSE: pause_start = tick.time if tick.action == ACTION_RESUME: if pause_start: pause_end = tick.time pause_time += pause_end - pause_start return pause_time
time_paused = property(get_time_paused) @property def time_active(self): return self.get_time_elapsed(exclude_pause=True) @property def time_total(self): return self.get_time_elapsed(exclude_pause=False) # ----------------------------------------------------------------------- #
[docs] def get_time_elapsed( self, start_key: int or str = 0, end_key: int or str = -1, exclude_pause: bool = True, ): """ Get time elapsed between different ticks If there are multiple matches for start or end ticks, the following two ticks are selected: - the chronologically first matching tick for start - the chronologically last matching tick for end Parameters ---------- start_key: int or str exclude_pause: bool If True, pause-time is not counted. The default is True. Returns ------- Total runtime (with or without pause-time) """ if not self.__ticks: return 0 start_matches = self.get_ticks(start_key) end_matches = self.get_ticks(end_key) if not start_matches: self.logger.warning( f"No matching start tick found for '{start_key}'." ) return None elif not end_matches: self.logger.warning(f"No matching end tick found for '{end_key}'.") return None else: start_tick = start_matches[0] start_idx = start_tick.id end_tick = end_matches[-1] end_idx = end_tick.id pause_time = ( self.get_time_paused(start_idx, end_idx) if exclude_pause else 0 ) return end_tick.time - start_tick.time - pause_time
time_elapsed = property(get_time_elapsed) # ----------------------------------------------------------------------- #
[docs] def get_ticks(self, key=None): id_match_ticks = [] name_match_ticks = [] ticks = [] total_ticks = len(self.__ticks) if key is None: key = [] if isinstance(key, str) or not isinstance(key, Iterable): key = [key] search_keys = set() for k in key: try: int_k = int(k) int_conversion = True search_keys.add(int_k) if int_k < 0: search_keys.add(total_ticks + int_k) except Exception: int_conversion = False try: str_k = str(k) str_conversion = True search_keys.add(str_k) except Exception: str_conversion = False if not int_conversion and not str_conversion: self.logger.warning( f"Ignored search key {k} ({type(k)}) ." ) for tick_idx, tick in enumerate(self.__ticks): if tick_idx in search_keys: id_match_ticks.append(tick) if tick.name in search_keys: name_match_ticks.append(tick) ticks.append(tick) if search_keys: ticks = id_match_ticks + name_match_ticks return ticks
# ----------------------------------------------------------------------- #
[docs] def last(self): """Return the time duration of the last tick i.e. time between the last two ticks""" if len(self.__ticks): return self.__ticks[-1].duration else: return 0
[docs] def current(self): """Return the time elapsed since the last tick""" if self.__state != STATE_INACTIVE: if self.__ticks: current_time = time.perf_counter() - self.__start_time return current_time - self.__ticks[-1].time else: return 0 else: self.logger.warning("Stopwatch is inactive.") return None
# ----------------------------------------------------------------------- # def __enter__(self): self.start() return self def __exit__(self, exception_type, exception_value, traceback): self.stop() # ----------------------------------------------------------------------- # def __repr__(self): return ( f"<{self.__class__.__name__}: " f"({self.__state}), " f"{len(self.__ticks)} ticks, " f"time_paused: {self.time_paused:.2f} sec, " f"time_active: {self.time_active:.2f} sec>" )
# ----------------------------------------------------------------------- # ###############################################################################
[docs] def main(): t = Stopwatch() t.start() print("Started ..") time.sleep(0.24) print(f"t.tick(): {t.tick():.4f} seconds") time.sleep(0.48) print(f"t.tick(): {t.tick():.4f} seconds") time.sleep(0.16) print(f"t.tick('Named Tick-1'): {t.tick('Named Tick-1'):.4f} seconds") t.pause() print("Paused ..") time.sleep(0.12) t.resume() print("Resumed ..") print(f"t.last(): {t.last():.4f} seconds") time.sleep(0.12) print(f"t.tick(): {t.tick():.4f} seconds") time.sleep(0.12) print(f"t.tick('Named Tick-2'): {t.tick('Named Tick-2'):.4f} seconds") t.stop() print("Timer stopped.") print("---") print(f"Total pause: {t.time_paused:.2f} seconds.") print(f"Total runtime: {t.time_active:.2f} seconds.") print(f"Total time: {t.time_total:.2f} seconds.") tij = t.get_time_elapsed(start_key="Named Tick-1", end_key="Named Tick-2") print(f"Time between 'Named Tick-1' and 'Named Tick-2': {tij:.4f}") return t
if __name__ == "__main__": t = main()