Module apparent.timing

The main module for explicit timing measurement support. See @timed

Expand source code
#  Copyright (c) Arnon Moscona 2023. under Apache2 license
"""The main module for explicit timing measurement support. See @timed"""
import math
from contextlib import AbstractContextManager
from dataclasses import asdict, dataclass
from enum import Enum
from functools import wraps
from time import perf_counter
from typing import Protocol, TypeVar


class Units(Enum):
    MIN = 'min'
    SEC = 'sec'
    MSEC = 'msec'


_CONVERSION_FACTOR = {
    Units.MIN: 1.0 / 60.0,
    Units.SEC: 1.0,
    Units.MSEC: 1000
}


@dataclass
class TimerResults:
    """Results from a timer: basic descriptive statistics (default in seconds).
    This class is generally produced by timers and not instantiated directly by library users"""
    total_time: float
    count: int
    mean: float
    stdevp: float
    min: float
    max: float
    timer_name: str
    units: Units = Units.SEC

    def convert(self, units: Units) -> 'TimerResults':
        """Convert the timer results to the given units"""
        factor = _CONVERSION_FACTOR[units] if self.units == Units.SEC \
            else _CONVERSION_FACTOR[units] / _CONVERSION_FACTOR[self.units]
        return TimerResults(
            total_time=self.total_time * factor,
            count=self.count,
            mean=self.mean * factor,
            stdevp=self.stdevp * factor,
            min=self.min * factor,
            max=self.max * factor,
            timer_name=self.timer_name,
            units=units)

    def dict(self, verbose: bool = True) -> dict:
        """Convert the timer results to a dictionary representation."""
        return asdict(self) if verbose else {'timer_name': self.timer_name,
                                             'total_time': self.total_time,
                                             'count': self.count,
                                             'mean': self.mean,
                                             'units': self.units}

    def round(self, digits: int = 1) -> 'TimerResults':
        """Round the timer results to the given number of digits. Useful for presentation and for comparison."""
        return TimerResults(
            total_time=round(self.total_time, digits),
            count=self.count,
            mean=round(self.mean, digits),
            stdevp=round(self.stdevp, digits),
            min=round(self.min, digits),
            max=round(self.max, digits),
            timer_name=self.timer_name,
            units=self.units)


class RunningVariance:
    """Variance using the shifted data algorithm for unbiased sample variance as described in
    https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Computing_shifted_data"""
    def __init__(self):
        # All variable names are taken from the Wikipedia article to make it easier to follow
        self._k: float = 0  # This will be the first value from the sample (shift value)
        self._n: int = 0  # sample count
        self._ex: float = 0  # sum of differences from shift value
        self._ex2: float = 0  # sum of squared differences from shift value

    def add(self, x: float):
        """Add a value to the running variance"""
        if self._n == 0:
            self._k = x
        self._n += 1
        self._ex += x - self._k
        self._ex2 += (x - self._k) ** 2

    def mean(self) -> float:
        """Return the mean of the added values"""
        return self._k + self._ex / self._n

    def variance(self, population: bool = False) -> float:
        """Computes the variance of the added values. By default, uses sample variance. Can be population variance"""
        return (self._ex2 - self._ex ** 2 / self._n) / (self._n if population else self._n - 1)

    def stdev(self) -> float:
        """Computes the standard deviation of the added values.
        By default, uses sample variance. Can be population variance"""
        return math.sqrt(self.variance())

    def stdevp(self) -> float:
        """Computes the population standard deviation of the added values. """
        return math.sqrt(self.variance(population=True))

    @property
    def n(self):
        """Return the number of values added"""
        return self._n


class Timer(AbstractContextManager):
    """A context manager for timing operations. Usually used in conjunction with a timer registry.
    Most of the time you will find yourself using one of the associated function decorators, such as @timed"""
    def __init__(self, name=None):
        if not isinstance(name, str):
            raise TypeError('name must be a string')
        self.name = name
        self.variance: RunningVariance = RunningVariance()
        self.sum = 0
        self._min = math.inf
        self._max = -math.inf
        self.start_time = 0

    def __enter__(self):
        self.start_time = perf_counter()
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        elapsed = (perf_counter() - self.start_time)
        self.variance.add(elapsed)
        self.sum += elapsed
        self._min = min(self._min, elapsed)
        self._max = max(self._max, elapsed)

    @property
    def counter(self) -> int:
        """The number of times the timer has been invoked"""
        return self.variance.n

    @property
    def mean(self) -> float:
        """Mean of all observations. NaN if no observations have been recorded"""
        return self.variance.mean() if self.counter > 0 else math.nan

    @property
    def stdevp(self) -> float:
        """Population standard deviation of all observations. NaN if less than 2 observations have been recorded"""
        return self.variance.stdevp() if self.counter > 2 else math.nan

    @property
    def min(self) -> float:
        """Minimum of all observations. NaN if no observations have been recorded"""
        return self._min if self.counter > 0 else math.nan

    @property
    def max(self) -> float:
        """Maximum of all observations. NaN if no observations have been recorded"""
        return self._max if self.counter > 0 else math.nan

    def results(self, units=Units.SEC) -> TimerResults:
        """TimerResults object packaging the summary of current observations."""
        result = TimerResults(count=self.counter, total_time=self.sum,
                              mean=self.mean, stdevp=self.stdevp,
                              min=self.min, max=self.max,
                              timer_name=self.name)
        return result if units == Units.SEC else result.convert(units)


class TimerRegistry:
    """The default collection for all the timers. Used primarily with the @timed function decorator"""
    _shared: dict[str, Timer] = {}

    @classmethod
    def get(cls, name: str) -> Timer:
        """Returns a named registered timer, whether it already existed or not,
        allowing for easy reuse, e.g. in loops"""
        if name not in cls._shared:
            cls._shared[name] = Timer(name)
        return cls._shared[name]

    @classmethod
    def clear(cls):
        """Clears the entire registry. All timers gone."""
        cls._shared.clear()

    @classmethod
    def names(cls) -> list[str]:
        """Lists all the timer names in the registry.
        Since it is expected that the number of timers will be limited and the whole
        package is not intended for very high scale we return a concrete list rather than an iterator"""
        return list(cls._shared.keys())

    @classmethod
    def timers(cls) -> list[Timer]:
        """Lists all the timers in the registry.
        Since it is expected that the number of timers will be limited and the whole
        package is not intended for very high scale we return a concrete list rather than an iterator"""
        return list(cls._shared.values())

    @classmethod
    def reset(cls, name: str):
        """Resets / deletes a named timer."""
        if name in cls._shared:
            del cls._shared[name]


class TimerRegistryProtocol(Protocol):
    """For users who want to use some other implementation of a timer registry or want to
    use several timer registries for different contexts."""
    def get(self, name: str) -> Timer:
        ...

    def clear(self):
        ...

    def names(self) -> list[str]:
        ...

    def timers(self) -> list[Timer]:
        ...

    def reset(self, name: str):
        ...


TimerRegistryType = TypeVar('TimerRegistryType', TimerRegistryProtocol, TimerRegistry)
"""Any acceptable type for a timer registry."""


class Timed:
    """A simple class to associate a timer registry with the timed decorator. Rarely used."""
    _registry: TimerRegistryType = TimerRegistry

    @classmethod
    def registry(cls) -> TimerRegistryType:
        return cls._registry

    @classmethod
    def set_registry(cls, registry: TimerRegistryType):
        cls._registry = registry


def timed(f):
    """A decorator for timing a function.
    The decorated function will be called with a timer context manager to
    record the time spent in the decorated function.
    The timer registry that will be used for the decorated function is the one in effect
    at the time of the function declaration. The timer name is automatically determined
    from the module and qualified name of the function."""
    function_name = f'{f.__module__}.{f.__qualname__}()'

    @wraps(f)
    def wrapper(*args, **kwargs):
        timer = Timed.registry().get(function_name)
        with timer:
            return f(*args, **kwargs)

    return wrapper

Global variables

var TimerRegistryType

Any acceptable type for a timer registry.

Functions

def timed(f)

A decorator for timing a function. The decorated function will be called with a timer context manager to record the time spent in the decorated function. The timer registry that will be used for the decorated function is the one in effect at the time of the function declaration. The timer name is automatically determined from the module and qualified name of the function.

Expand source code
def timed(f):
    """A decorator for timing a function.
    The decorated function will be called with a timer context manager to
    record the time spent in the decorated function.
    The timer registry that will be used for the decorated function is the one in effect
    at the time of the function declaration. The timer name is automatically determined
    from the module and qualified name of the function."""
    function_name = f'{f.__module__}.{f.__qualname__}()'

    @wraps(f)
    def wrapper(*args, **kwargs):
        timer = Timed.registry().get(function_name)
        with timer:
            return f(*args, **kwargs)

    return wrapper

Classes

class RunningVariance

Variance using the shifted data algorithm for unbiased sample variance as described in https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Computing_shifted_data

Expand source code
class RunningVariance:
    """Variance using the shifted data algorithm for unbiased sample variance as described in
    https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Computing_shifted_data"""
    def __init__(self):
        # All variable names are taken from the Wikipedia article to make it easier to follow
        self._k: float = 0  # This will be the first value from the sample (shift value)
        self._n: int = 0  # sample count
        self._ex: float = 0  # sum of differences from shift value
        self._ex2: float = 0  # sum of squared differences from shift value

    def add(self, x: float):
        """Add a value to the running variance"""
        if self._n == 0:
            self._k = x
        self._n += 1
        self._ex += x - self._k
        self._ex2 += (x - self._k) ** 2

    def mean(self) -> float:
        """Return the mean of the added values"""
        return self._k + self._ex / self._n

    def variance(self, population: bool = False) -> float:
        """Computes the variance of the added values. By default, uses sample variance. Can be population variance"""
        return (self._ex2 - self._ex ** 2 / self._n) / (self._n if population else self._n - 1)

    def stdev(self) -> float:
        """Computes the standard deviation of the added values.
        By default, uses sample variance. Can be population variance"""
        return math.sqrt(self.variance())

    def stdevp(self) -> float:
        """Computes the population standard deviation of the added values. """
        return math.sqrt(self.variance(population=True))

    @property
    def n(self):
        """Return the number of values added"""
        return self._n

Instance variables

var n

Return the number of values added

Expand source code
@property
def n(self):
    """Return the number of values added"""
    return self._n

Methods

def add(self, x: float)

Add a value to the running variance

Expand source code
def add(self, x: float):
    """Add a value to the running variance"""
    if self._n == 0:
        self._k = x
    self._n += 1
    self._ex += x - self._k
    self._ex2 += (x - self._k) ** 2
def mean(self) ‑> float

Return the mean of the added values

Expand source code
def mean(self) -> float:
    """Return the mean of the added values"""
    return self._k + self._ex / self._n
def stdev(self) ‑> float

Computes the standard deviation of the added values. By default, uses sample variance. Can be population variance

Expand source code
def stdev(self) -> float:
    """Computes the standard deviation of the added values.
    By default, uses sample variance. Can be population variance"""
    return math.sqrt(self.variance())
def stdevp(self) ‑> float

Computes the population standard deviation of the added values.

Expand source code
def stdevp(self) -> float:
    """Computes the population standard deviation of the added values. """
    return math.sqrt(self.variance(population=True))
def variance(self, population: bool = False) ‑> float

Computes the variance of the added values. By default, uses sample variance. Can be population variance

Expand source code
def variance(self, population: bool = False) -> float:
    """Computes the variance of the added values. By default, uses sample variance. Can be population variance"""
    return (self._ex2 - self._ex ** 2 / self._n) / (self._n if population else self._n - 1)
class Timed

A simple class to associate a timer registry with the timed decorator. Rarely used.

Expand source code
class Timed:
    """A simple class to associate a timer registry with the timed decorator. Rarely used."""
    _registry: TimerRegistryType = TimerRegistry

    @classmethod
    def registry(cls) -> TimerRegistryType:
        return cls._registry

    @classmethod
    def set_registry(cls, registry: TimerRegistryType):
        cls._registry = registry

Static methods

def registry() ‑> ~TimerRegistryType
Expand source code
@classmethod
def registry(cls) -> TimerRegistryType:
    return cls._registry
def set_registry(registry: ~TimerRegistryType)
Expand source code
@classmethod
def set_registry(cls, registry: TimerRegistryType):
    cls._registry = registry
class Timer (name=None)

A context manager for timing operations. Usually used in conjunction with a timer registry. Most of the time you will find yourself using one of the associated function decorators, such as @timed

Expand source code
class Timer(AbstractContextManager):
    """A context manager for timing operations. Usually used in conjunction with a timer registry.
    Most of the time you will find yourself using one of the associated function decorators, such as @timed"""
    def __init__(self, name=None):
        if not isinstance(name, str):
            raise TypeError('name must be a string')
        self.name = name
        self.variance: RunningVariance = RunningVariance()
        self.sum = 0
        self._min = math.inf
        self._max = -math.inf
        self.start_time = 0

    def __enter__(self):
        self.start_time = perf_counter()
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        elapsed = (perf_counter() - self.start_time)
        self.variance.add(elapsed)
        self.sum += elapsed
        self._min = min(self._min, elapsed)
        self._max = max(self._max, elapsed)

    @property
    def counter(self) -> int:
        """The number of times the timer has been invoked"""
        return self.variance.n

    @property
    def mean(self) -> float:
        """Mean of all observations. NaN if no observations have been recorded"""
        return self.variance.mean() if self.counter > 0 else math.nan

    @property
    def stdevp(self) -> float:
        """Population standard deviation of all observations. NaN if less than 2 observations have been recorded"""
        return self.variance.stdevp() if self.counter > 2 else math.nan

    @property
    def min(self) -> float:
        """Minimum of all observations. NaN if no observations have been recorded"""
        return self._min if self.counter > 0 else math.nan

    @property
    def max(self) -> float:
        """Maximum of all observations. NaN if no observations have been recorded"""
        return self._max if self.counter > 0 else math.nan

    def results(self, units=Units.SEC) -> TimerResults:
        """TimerResults object packaging the summary of current observations."""
        result = TimerResults(count=self.counter, total_time=self.sum,
                              mean=self.mean, stdevp=self.stdevp,
                              min=self.min, max=self.max,
                              timer_name=self.name)
        return result if units == Units.SEC else result.convert(units)

Ancestors

  • contextlib.AbstractContextManager
  • abc.ABC

Instance variables

var counter : int

The number of times the timer has been invoked

Expand source code
@property
def counter(self) -> int:
    """The number of times the timer has been invoked"""
    return self.variance.n
var max : float

Maximum of all observations. NaN if no observations have been recorded

Expand source code
@property
def max(self) -> float:
    """Maximum of all observations. NaN if no observations have been recorded"""
    return self._max if self.counter > 0 else math.nan
var mean : float

Mean of all observations. NaN if no observations have been recorded

Expand source code
@property
def mean(self) -> float:
    """Mean of all observations. NaN if no observations have been recorded"""
    return self.variance.mean() if self.counter > 0 else math.nan
var min : float

Minimum of all observations. NaN if no observations have been recorded

Expand source code
@property
def min(self) -> float:
    """Minimum of all observations. NaN if no observations have been recorded"""
    return self._min if self.counter > 0 else math.nan
var stdevp : float

Population standard deviation of all observations. NaN if less than 2 observations have been recorded

Expand source code
@property
def stdevp(self) -> float:
    """Population standard deviation of all observations. NaN if less than 2 observations have been recorded"""
    return self.variance.stdevp() if self.counter > 2 else math.nan

Methods

def results(self, units=Units.SEC) ‑> TimerResults

TimerResults object packaging the summary of current observations.

Expand source code
def results(self, units=Units.SEC) -> TimerResults:
    """TimerResults object packaging the summary of current observations."""
    result = TimerResults(count=self.counter, total_time=self.sum,
                          mean=self.mean, stdevp=self.stdevp,
                          min=self.min, max=self.max,
                          timer_name=self.name)
    return result if units == Units.SEC else result.convert(units)
class TimerRegistry

The default collection for all the timers. Used primarily with the @timed function decorator

Expand source code
class TimerRegistry:
    """The default collection for all the timers. Used primarily with the @timed function decorator"""
    _shared: dict[str, Timer] = {}

    @classmethod
    def get(cls, name: str) -> Timer:
        """Returns a named registered timer, whether it already existed or not,
        allowing for easy reuse, e.g. in loops"""
        if name not in cls._shared:
            cls._shared[name] = Timer(name)
        return cls._shared[name]

    @classmethod
    def clear(cls):
        """Clears the entire registry. All timers gone."""
        cls._shared.clear()

    @classmethod
    def names(cls) -> list[str]:
        """Lists all the timer names in the registry.
        Since it is expected that the number of timers will be limited and the whole
        package is not intended for very high scale we return a concrete list rather than an iterator"""
        return list(cls._shared.keys())

    @classmethod
    def timers(cls) -> list[Timer]:
        """Lists all the timers in the registry.
        Since it is expected that the number of timers will be limited and the whole
        package is not intended for very high scale we return a concrete list rather than an iterator"""
        return list(cls._shared.values())

    @classmethod
    def reset(cls, name: str):
        """Resets / deletes a named timer."""
        if name in cls._shared:
            del cls._shared[name]

Static methods

def clear()

Clears the entire registry. All timers gone.

Expand source code
@classmethod
def clear(cls):
    """Clears the entire registry. All timers gone."""
    cls._shared.clear()
def get(name: str) ‑> Timer

Returns a named registered timer, whether it already existed or not, allowing for easy reuse, e.g. in loops

Expand source code
@classmethod
def get(cls, name: str) -> Timer:
    """Returns a named registered timer, whether it already existed or not,
    allowing for easy reuse, e.g. in loops"""
    if name not in cls._shared:
        cls._shared[name] = Timer(name)
    return cls._shared[name]
def names() ‑> list[str]

Lists all the timer names in the registry. Since it is expected that the number of timers will be limited and the whole package is not intended for very high scale we return a concrete list rather than an iterator

Expand source code
@classmethod
def names(cls) -> list[str]:
    """Lists all the timer names in the registry.
    Since it is expected that the number of timers will be limited and the whole
    package is not intended for very high scale we return a concrete list rather than an iterator"""
    return list(cls._shared.keys())
def reset(name: str)

Resets / deletes a named timer.

Expand source code
@classmethod
def reset(cls, name: str):
    """Resets / deletes a named timer."""
    if name in cls._shared:
        del cls._shared[name]
def timers() ‑> list[Timer]

Lists all the timers in the registry. Since it is expected that the number of timers will be limited and the whole package is not intended for very high scale we return a concrete list rather than an iterator

Expand source code
@classmethod
def timers(cls) -> list[Timer]:
    """Lists all the timers in the registry.
    Since it is expected that the number of timers will be limited and the whole
    package is not intended for very high scale we return a concrete list rather than an iterator"""
    return list(cls._shared.values())
class TimerRegistryProtocol (*args, **kwargs)

For users who want to use some other implementation of a timer registry or want to use several timer registries for different contexts.

Expand source code
class TimerRegistryProtocol(Protocol):
    """For users who want to use some other implementation of a timer registry or want to
    use several timer registries for different contexts."""
    def get(self, name: str) -> Timer:
        ...

    def clear(self):
        ...

    def names(self) -> list[str]:
        ...

    def timers(self) -> list[Timer]:
        ...

    def reset(self, name: str):
        ...

Ancestors

  • typing.Protocol
  • typing.Generic

Methods

def clear(self)
Expand source code
def clear(self):
    ...
def get(self, name: str) ‑> Timer
Expand source code
def get(self, name: str) -> Timer:
    ...
def names(self) ‑> list[str]
Expand source code
def names(self) -> list[str]:
    ...
def reset(self, name: str)
Expand source code
def reset(self, name: str):
    ...
def timers(self) ‑> list[Timer]
Expand source code
def timers(self) -> list[Timer]:
    ...
class TimerResults (total_time: float, count: int, mean: float, stdevp: float, min: float, max: float, timer_name: str, units: Units = Units.SEC)

Results from a timer: basic descriptive statistics (default in seconds). This class is generally produced by timers and not instantiated directly by library users

Expand source code
@dataclass
class TimerResults:
    """Results from a timer: basic descriptive statistics (default in seconds).
    This class is generally produced by timers and not instantiated directly by library users"""
    total_time: float
    count: int
    mean: float
    stdevp: float
    min: float
    max: float
    timer_name: str
    units: Units = Units.SEC

    def convert(self, units: Units) -> 'TimerResults':
        """Convert the timer results to the given units"""
        factor = _CONVERSION_FACTOR[units] if self.units == Units.SEC \
            else _CONVERSION_FACTOR[units] / _CONVERSION_FACTOR[self.units]
        return TimerResults(
            total_time=self.total_time * factor,
            count=self.count,
            mean=self.mean * factor,
            stdevp=self.stdevp * factor,
            min=self.min * factor,
            max=self.max * factor,
            timer_name=self.timer_name,
            units=units)

    def dict(self, verbose: bool = True) -> dict:
        """Convert the timer results to a dictionary representation."""
        return asdict(self) if verbose else {'timer_name': self.timer_name,
                                             'total_time': self.total_time,
                                             'count': self.count,
                                             'mean': self.mean,
                                             'units': self.units}

    def round(self, digits: int = 1) -> 'TimerResults':
        """Round the timer results to the given number of digits. Useful for presentation and for comparison."""
        return TimerResults(
            total_time=round(self.total_time, digits),
            count=self.count,
            mean=round(self.mean, digits),
            stdevp=round(self.stdevp, digits),
            min=round(self.min, digits),
            max=round(self.max, digits),
            timer_name=self.timer_name,
            units=self.units)

Class variables

var count : int
var max : float
var mean : float
var min : float
var stdevp : float
var timer_name : str
var total_time : float
var unitsUnits

Methods

def convert(self, units: Units) ‑> TimerResults

Convert the timer results to the given units

Expand source code
def convert(self, units: Units) -> 'TimerResults':
    """Convert the timer results to the given units"""
    factor = _CONVERSION_FACTOR[units] if self.units == Units.SEC \
        else _CONVERSION_FACTOR[units] / _CONVERSION_FACTOR[self.units]
    return TimerResults(
        total_time=self.total_time * factor,
        count=self.count,
        mean=self.mean * factor,
        stdevp=self.stdevp * factor,
        min=self.min * factor,
        max=self.max * factor,
        timer_name=self.timer_name,
        units=units)
def dict(self, verbose: bool = True) ‑> dict

Convert the timer results to a dictionary representation.

Expand source code
def dict(self, verbose: bool = True) -> dict:
    """Convert the timer results to a dictionary representation."""
    return asdict(self) if verbose else {'timer_name': self.timer_name,
                                         'total_time': self.total_time,
                                         'count': self.count,
                                         'mean': self.mean,
                                         'units': self.units}
def round(self, digits: int = 1) ‑> TimerResults

Round the timer results to the given number of digits. Useful for presentation and for comparison.

Expand source code
def round(self, digits: int = 1) -> 'TimerResults':
    """Round the timer results to the given number of digits. Useful for presentation and for comparison."""
    return TimerResults(
        total_time=round(self.total_time, digits),
        count=self.count,
        mean=round(self.mean, digits),
        stdevp=round(self.stdevp, digits),
        min=round(self.min, digits),
        max=round(self.max, digits),
        timer_name=self.timer_name,
        units=self.units)
class Units (*args, **kwds)

Create a collection of name/value pairs.

Example enumeration:

>>> class Color(Enum):
...     RED = 1
...     BLUE = 2
...     GREEN = 3

Access them by:

  • attribute access::
>>> Color.RED
<Color.RED: 1>
  • value lookup:
>>> Color(1)
<Color.RED: 1>
  • name lookup:
>>> Color['RED']
<Color.RED: 1>

Enumerations can be iterated over, and know how many members they have:

>>> len(Color)
3
>>> list(Color)
[<Color.RED: 1>, <Color.BLUE: 2>, <Color.GREEN: 3>]

Methods can be added to enumerations, and members can have their own attributes – see the documentation for details.

Expand source code
class Units(Enum):
    MIN = 'min'
    SEC = 'sec'
    MSEC = 'msec'

Ancestors

  • enum.Enum

Class variables

var MIN
var MSEC
var SEC