Precision Timer API

This module contains the PrecisionTimer class that wraps the bound C++ timer class and provides a high-level API for the timer functionality.

The module is exposed to the highest level of the library (through init-based relative imports). All interactions with the low-level C-API should be carried out through the wrapper class where possible.

class ataraxis_time.precision_timer.timer_class.PrecisionTimer(precision='us')

Bases: object

Provides sub-microsecond-precision interval-timing and blocking / non-blocking delay functionality.

This is a wrapper for a C++ class that uses ‘chrono’ library to interface with the highest available precision clock of the host system. All methods are implemented in C++, which makes them both significantly faster and more precise compared to any pure-python implementation. The class itself operates in nanoseconds, but it automatically converts all inputs and outputs from the user-defined precision to nanoseconds and back.

Notes

The actual clock precision depends on the precision and frequency of the system CPU clock that runs this code. The class methods mostly have fairly minor overheads ~100 ns, but some OS-based methods used in the class (e.g., sleep) may incur longer delays. It is highly advised to test this timer prior to running it in production projects to characterize the overhead associated with using different timer methods.

Use delay_noblock when you want this class to release GIL and delay_block when you do not want this class to release GIL.

_supported_precisions

A tuple of currently supported precision string-options. Used internally for input verification and error-messaging purposes.

_timer

The inner binding-class generated by nanobind to expose C++ API. Since the class has no stubs at the time of writing, the best way to use this library is via the PrecisionTimer (this) wrapper class.

Parameters:

precision (Literal['ns', 'us', 'ms', 's'], default: 'us') – The desired precision of the timer. Accepted values are ‘ns’ (nanoseconds), ‘us’ (microseconds), ‘ms’ (milliseconds) and ‘s’ (seconds). The argument is case-insensitive.

Raises:

ValueError – If the input precision is not one of the accepted options.

__repr__()

Generates and returns a string representation of the PrecisionTimer object.

Return type:

str

delay_block(delay, *, allow_sleep=False)

Delays code execution for the requested period of time while maintaining GIL.

This method is similar to delay_noblock, except it does not release the GIL and, therefore, will prevent any other threads from running during the delay duration. Defaults to using the non-cpu-releasing busy-wait method due to its higher precision compared to the CPU-releasing method.

Notes

Even if sleeping is allowed, the method will only sleep if the duration is long enough to resolve the inherent overhead of the sleep() method (~1 ms). Currently, this means it will not run for nanosecond and microsecond timers.

This functionality does not clash with ‘elapsed time’ functionality of the class. The timer used to delay code execution is different from the timer used to calculate elapsed time.

Parameters:
  • delay (int) – The integer period of time to wait for. The method assumes the delay is given in the same precision units as used by the timer (if the timer uses ‘us’, the method assumes the duration is also in ‘us’).

  • allow_sleep (bool, default: False) – A boolean flag that allows using CPU-releasing sleep() method to suspend execution for durations above 1 millisecond. Defaults to False.

Return type:

None

delay_noblock(delay, *, allow_sleep=False)

Delays code execution for the requested period of time while releasing the GIL.

Use this method to delay code execution while allowing other threads (in multithreaded environments) to run unhindered. Defaults to using the non-cpu-releasing busy-wait method due to its higher precision compared to the CPU-releasing method.

Notes

Even if sleeping is allowed, the method will only sleep if the duration is long enough to resolve the inherent overhead of the sleep() method (~1 ms). Currently, this means it will not run for nanosecond and microsecond timers.

This functionality does not clash with ‘elapsed time’ functionality of the class. The timer used to delay code execution is different from the timer used to calculate elapsed time.

Parameters:
  • delay (int) – The integer period of time to wait for. The method assumes the delay is given in the same precision units as used by the timer (if the timer uses ‘us’, the method assumes the duration is also in ‘us’).

  • allow_sleep (bool, default: False) – A boolean flag that allows using CPU-releasing sleep() method to suspend execution for durations above 1 millisecond. Defaults to False.

Return type:

None

property elapsed: int

Returns the time passed since the last timer checkpoint (instantiation or reset() call), converted to the requested time units.

The time is automatically rounded to the nearest supported integer precision unit.

Notes

This functionality does not clash with delay functionality of the class. The timer used to delay code execution is different from the timer used to calculate elapsed time.

property precision: str

Returns the units currently used by the timer (‘ns’, ‘us’, ‘ms’ or ‘s’).

reset()

Resets the timer by re-basing it to count time relative to the call-time of this method.

Return type:

None

set_precision(precision)

Changes the precision used by the timer to the input string-option.

This method allows reusing the same timer instance for different precisions, which is frequently more efficient than initializing multiple timers or re-initializing a class with a new precision.

Parameters:

precision (Literal['ns', 'us', 'ms', 's']) – The desired precision of the timer. Accepted values are ‘ns’ (nanoseconds), ‘us’ (microseconds), ‘ms’ (milliseconds) and ‘s’ (seconds). The argument is case-insensitive.

Raises:

ValueError – If the input precision is not one of the accepted options.

Return type:

None

property supported_precisions: tuple[str, str, str, str]

Returns a tuple that stores all currently supported time unit options.

Timer Benchmark

This module contains the benchmark() function used to assess the performance of the PrecisionTimer class on the intended host-system.

To improve user-experience, installing the library automatically generates a shorthand ‘benchmark_timer’ command that allows calling the benchmark() method from the CLI without using the short (ataraxis_time.benchmark) or the full method path (ataraxis_time.precision_timer.timer_benchmark.benchmark).

Calling benchmark_timer with –help argument displays the list of command line arguments that can be used to configure the behavior of the benchmark. The default benchmark arguments are designed to offer a high-confidence result without excessive time expenditures, but may not be optimal for all users. The CLI options are also listed in the online API documentation.

benchmark-timer

This function is used to benchmark the PrecisionTimer class performance for the caller host system.

It is highly advised to use this function to evaluate the precision and performance of the timer for each intended host system, as these parameters vary for each tested OS and Platform combination. Additionally, the performance of the timer may be affected by the overall system utilization and particular use-patterns.

Notes:

This command is accessible from a CLI interface via a shorthand benchmark_timer command, following library installation.

benchmark-timer [OPTIONS]

Options

-ic, --interval-cycles <interval_cycles>

Number of times to repeat the interval benchmark for each of the tested precisions. Example: -ic 60

-id, --interval-delay <interval_delay>

The interval duration, in seconds, to use during the interval benchmark for each of the tested precisions. Example: -id 1

-dc, --delay-cycles <delay_cycles>

Number of times to repeat the delay benchmark (blocking and non-blocking) for each of the tested precisions. Expects a space-separated sequence in the order of: ns, us, ms, s. Example: -dc 1000 1000 1000 60

-dd, --delay-durations <delay_durations>

The delay duration, in precision-units, to use during the delay benchmark (blocking and non-blocking) for each of the tested precisions. Expects a space-separated sequence in the order of: ns, us, ms, s. Example: -dd 500 5 2 1

Helper Functions

This module contains helper functions used to work with date and time data.

These functions are included as convenience methods that are expected to be frequently used both together with and independently of the PrecisionTimer class. Unlike PrecisionTimer class, they are not expected to be actively used in real-time runtimes and are implemented using pure-python API where possible.

ataraxis_time.time_helpers.helper_functions.convert_time(time, from_units, to_units, *, convert_output=True)

Converts the input time value(s) from the original units to the requested units.

Supports conversion in the range from days to nanoseconds and uses numpy under-the-hood to optimize runtime speed. Since the function always converts input data to numpy arrays, it can be configured to return data using either numpy or python formats. If the data can be returned as a scalar, it will be returned as a scalar, even if the input was iterable (e.g.: a one-element list).

Notes

While this function accepts numpy arrays, it expects them to be one-dimensional. To pass a multidimensional numpy array through this function, first flatten the array into one dimension.

The conversion uses 3 decimal places rounding, which may introduce inaccuracies in some cases.

Parameters:
  • time (Union[int, float, list[int | float], tuple[int | float], signedinteger[Any], unsignedinteger[Any], floating[Any], ndarray[Any, dtype[signedinteger[Any] | unsignedinteger[Any] | floating[Any]]]]) – A scalar Python or numpy numeric time-value to convert. Alternatively, can be Python or numpy iterable that contains float-convertible numeric values. Input numpy arrays have to be one-dimensional.

  • from_units (Literal['ns', 'us', 'ms', 's', 'm', 'h', 'd']) – The units used by the input data. Valid options are: ‘ns’ (nanoseconds), ‘us’ (microseconds), ‘ms’ (milliseconds), ‘s’ (seconds), ‘m’ (minutes), ‘h’ (hours), ‘d’ (days).

  • to_units (Literal['ns', 'us', 'ms', 's', 'm', 'h', 'd']) – The units to convert the input data to. Uses the same options as from_units.

  • convert_output (bool, default: True) – Determines whether to convert output to a Python scalar / iterable type or to return it as a numpy type.

Return type:

float | tuple[float] | ndarray[Any, dtype[float64]] | float64

Returns:

The converted time in the requested units using either python ‘float’ or numpy ‘float64’ format. The returned data will be a scalar, if possible. If not, it will be a tuple (when the function is configured to return Python types) or a numpy array (when the function is configured to return numpy types).

Raises:
  • TypeError – If ‘time’ argument is not of a valid type. If time contains elements that are not float-convertible.

  • ValueError – If ‘from_units’ or ‘to_units’ argument is not set to a valid time-option. If time is a multidimensional numpy array.

ataraxis_time.time_helpers.helper_functions.get_timestamp(time_separator='-')

Gets the current date and time (to seconds) and formats it into year-month-day-hour-minute-second string.

This utility method can be used to quickly time-stamp events and should be decently fast as it links to a C-extension under the hood.

Parameters:

time_separator (str, default: '-') – The separator to use to separate the components of the time-string. Defaults to hyphens “-“.

Notes

Hyphen-separation is supported by the majority of modern OSes and, therefore, the default separator should be safe for most use cases. That said, the method does not evaluate the separator for compatibility with the OS-reserved symbols and treats it as a generic string to be inserted between time components. Therefore, it is advised to make sure that the separator is a valid string given your OS and Platform combination.

Return type:

str

Returns:

The ‘year-month-day-hour-minute-second’ string that uses the input timer-separator to separate time-components.

Raises:

TypeError – If the time_separator argument is not a string.

C++ Timer Extension

The C++ extension module that defines and implements the CPrecisionTimer class.

Description:

This module instantiates the CPrecisionTimer class using the fastest system clock available through the ‘chrono’ library, which allows the timer to resolve sub-microsecond timer-intervals on sufficiently fast CPUs. The use of the ‘chrono’ library offers multiplatform support, so this module works on Windows, OSX and Linux.

Dependencies:

  • nanobind/nanobind.h: For nanobind-based binding to Python.

  • nanobind/stl/string.h: To enable working with python string arguments.

  • chrono: To work with system-exposed time sources.

  • thread: To control GIL-locking behavior of noblock methods.

Note

This module is bound to python using (nanobind) project and is designed to be further wrapped with a pure-python PrecisionTimer wrapper instantiated by the init.py of the python module. The binding code is stored in the same file as source code (at the end of this file).

Functions

NB_MODULE(precision_timer_ext, m)

The nanobind module that binds (exposes) the CPrecisionTimer class to the Python API.

This modules wraps the CPrecisionTimer class and exposes it to Python via it’s API.

Note

The module is available as ‘precision_timer_ext’ and has to be properly bound to a python package via CMake configuration. Each method exposed to Python API below uses the names given as the first argument to each ‘def’ method.

class CPrecisionTimer

Provides methods for sub-microsecond-precise interval timing and blocking and non-blocking code execution delays.

Note

The performance of this class scales with the OS and the state of the host system. ‘Chrono’ library provides a high_resolution_clock, which is automatically set to the highest resolution clock of the host OS (in Python, a very similar approach is used by the perf_counter_ns() function from ‘time’ standard library). Additionally, all method calls have a certain overhead associated with them (especially the sleep_for() method that has to wait for the scheduler for at least 1 ms on the tested system). The busier the system is, the longer the overhead. Therefore, it is highly advisable to benchmark the timer if your application has very tight timing constraints and / or experiences resource limitations.

Public Functions

inline explicit CPrecisionTimer(const std::string &precision = "us")

Instantiates the CPrecisionTimer class using the requested precision.

Parameters:

precision – The precision of the timer. This controls the units used by the timer for all inputs and outputs, which simplifies class usage as all time conversions are done automatically. Supported values are: ‘ns’ (nanoseconds), ‘us’ (microseconds), ‘ms’ (milliseconds), and ‘s’ (seconds). Defaults to ‘us’.

inline ~CPrecisionTimer()

Destroys the CPrecisionTimer class.

Currently an explicit destructor is not strictly required, but it is still defined with potential future-use in mind.

inline void Reset()

Resets the timer by replacing the timer reference with the time-value at the time of this method call.

Call this method before executing the code which you want to time. When elapsed() method is used, it returns the elapsed time relative to the last reset() method call or class instantiation (whichever happened last).

inline int64_t Elapsed()

Obtains the current value of the monotonic clock and uses it to calculate how much time (using precision-units) has elapsed since the last reset() method call or class instantiation.

Note

Unless the class reference point is re-established using reset() method, the class will use the same reference time for all elapsed() method calls. The time has no inherent meaning other than relative to the reference point.

Returns:

int64_t The elapsed time in the requested precision.

inline void DelayNoblock(int64_t duration, bool allow_sleep = false) const

Releases GIL and blocks (delays) in-place for the specified number of time-units (depends on used precision).

This method is used to execute arbitrary delays while releasing GIL to enable other threads to run while blocking. By default, this method uses a busy-wait approach where the thread constantly checks elapsed time until the exit condition is encountered. Optionally, the method can be allowed to use sleep_for() for durations above 1 millisecond (sleep_for only has ms precision), which will release the CPU.

Note

While the timer supports sub-microsecond precision, the minimum precision on the tested system was ~200 ns and that incurred a static overhead of ~100 ns to setup and tear-down the timer. It is likely that the overhead and precision will be worse for most other systems.

Warning

If sleeping is allowed, there is an overhead of up to 1 ms due to scheduling on Windows. Unix schedulers so far seem considerably better, but may still experience overheads.

Parameters:
  • duration – The time to block for. Uses the same units as the precision parameter.

  • allow_sleep – A boolean flag that determines whether the method should use sleep for delay durations above 1 millisecond. Sleep may be beneficial in some cases as it reduces the CPU load at the expense of a significantly larger overhead compared to default busy-wait approach.

inline void DelayBlock(int64_t duration, bool allow_sleep = false) const

Similar to DelayNoblock() method, but this method does NOT release the GIL, preventing other threads from running, as it blocks (delays) in-place for the specified number of time-units (depends on used precision).

This method is used to execute arbitrary delays while maintaining GIL to prevent other threads from running. By default, this method uses a busy-wait approach where the thread constantly checks elapsed time until the exit condition is encountered. Optionally, the method can be allowed to use sleep_for() for durations above 1 millisecond (sleep_for only has ms precision), which will release the CPU.

Note

While the timer supports sub-microsecond precision, the minimum precision on the tested system was ~200 ns and that incurred a static overhead of ~100 ns to setup and tear-down he timer. It is likely that the overhead and precision will be worse for most other systems.

Warning

If sleeping is allowed, there is an overhead of up to 1 ms due to scheduling on Windows. Unix schedulers so far seem considerably better, but may still experience overheads.

Parameters:
  • duration – The time to block for. Uses the same units as the precision parameter.

  • allow_sleep – A boolean flag that determines whether the method should use sleep for delay durations above 1 millisecond. Sleep may be beneficial in some cases as it reduces the CPU load at the expense of a significantly larger overhead compared to default busy-wait approach.

inline void SetPrecision(const std::string &precision)

Changes the precision of the timer class to the requested units.

This method can be used to dynamically change the precision of the class without re-instantiating the class during runtime, improving overall runtime speeds.

Parameters:

precision – The new precision to set the timer to. Supported values are: ‘ns’ (nanoseconds), ‘us’ (microseconds), ms’ (milliseconds), and ‘s’ (seconds).’

inline std::string GetPrecision() const

Returns the current precision (time-units) of the timer.

Returns:

std::string The current precision of the timer (‘ns’, ‘us’, ‘ms’, or ‘s’).

Private Functions

inline int64_t ConvertToPrecision(int64_t nanoseconds) const

Converts the input value from nanoseconds to the chosen precision units.

This method is currently used by the Elapsed() method to convert elapsed time from nanoseconds (used by the class) to the desired precision (requested by the user). However, it is a general converter that may be used by other methods in the future.

Parameters:

nanoseconds – The value in nanoseconds to be converted to the desired precision.

Returns:

int64_t The converted time-value rounded to the whole number.

Private Members

std::chrono::high_resolution_clock::time_point _start_time

Stores the reference value used to calculate elapsed time.

std::string _precision

Stores the string-option that describes the units used for inputs and outputs.

nanoseconds _precision_duration

Stores the conversion factor that is assigned based on the chosen _precision option. It is used to convert the input duration values (for delay methods) to nanoseconds and the output duration values from nanoseconds to the chosen precision units.

namespace literals

Provides the ability to work with Python literal string-options.

namespace chrono

Provides the binding for various clock-related operations.