How to Get Monotonic Time Durations in Python

How do I get monotonic time durations in python?

That function is simple enough that you can use ctypes to access it:

#!/usr/bin/env python

__all__ = ["monotonic_time"]

import ctypes, os

CLOCK_MONOTONIC_RAW = 4 # see <linux/time.h>

class timespec(ctypes.Structure):
_fields_ = [
('tv_sec', ctypes.c_long),
('tv_nsec', ctypes.c_long)
]

librt = ctypes.CDLL('librt.so.1', use_errno=True)
clock_gettime = librt.clock_gettime
clock_gettime.argtypes = [ctypes.c_int, ctypes.POINTER(timespec)]

def monotonic_time():
t = timespec()
if clock_gettime(CLOCK_MONOTONIC_RAW , ctypes.pointer(t)) != 0:
errno_ = ctypes.get_errno()
raise OSError(errno_, os.strerror(errno_))
return t.tv_sec + t.tv_nsec * 1e-9

if __name__ == "__main__":
print monotonic_time()

scheduling for an exact time with monotonic time

As I said in the comment, your code duplicates the functionality of the sched standard module - so you can as well use solving this problem as a convenient excuse to migrate to it.

That said,

  • what you're supposed to do if system time jumps forward or backward is task-specific.
  • time.monotonic() is designed for cases when you need to do things with set intervals between them regardless of anything
  • So, if your solution is expected to instead react to time jumps by running scheduled tasks sooner or later than it otherwise would, in accordance with the new system time, you have no reason to use monotonic time.

If you wish to do both, then you either need two schedulers, or tasks with timestamps of the two kinds.

In the latter case, the scheduler will need to convert one type to the other (every time it calculates how much to wait/whether to run the next task) - for which time provides no means.

Measuring elapsed time with the Time module

start_time = time.time()
# your code
elapsed_time = time.time() - start_time

You can also write simple decorator to simplify measurement of execution time of various functions:

import time
from functools import wraps

PROF_DATA = {}

def profile(fn):
@wraps(fn)
def with_profiling(*args, **kwargs):
start_time = time.time()

ret = fn(*args, **kwargs)

elapsed_time = time.time() - start_time

if fn.__name__ not in PROF_DATA:
PROF_DATA[fn.__name__] = [0, []]
PROF_DATA[fn.__name__][0] += 1
PROF_DATA[fn.__name__][1].append(elapsed_time)

return ret

return with_profiling

def print_prof_data():
for fname, data in PROF_DATA.items():
max_time = max(data[1])
avg_time = sum(data[1]) / len(data[1])
print "Function %s called %d times. " % (fname, data[0]),
print 'Execution time max: %.3f, average: %.3f' % (max_time, avg_time)

def clear_prof_data():
global PROF_DATA
PROF_DATA = {}

Usage:

@profile
def your_function(...):
...

You can profile more then one function simultaneously. Then to print measurements just call the print_prof_data():

Range of time.clock() in Python

Yes, this is because of the limitations of the underlying system call. The results you'll get really depend on your platform.

On Windows, the system call always returns a signed 64-bit integer (which is the divided by the counter frequency). So it should not wrap on any reasonable time scale.

On Linux the underlying integer is either signed 32-bit or signed 64-bit depending on whether your platform is 32-bit or 64-bit system. It is also further divided by the frequency of the counter so a 32-bit integer could definitely wrap after several hours.

On Linux you should really be using the newer clock_gettime() system call rather than clock(), which shouldn't have this wrap around problem. If you have Python 3.3 or newer it's as simple as:

time.clock_gettime(time.CLOCK_PROCESS_CPUTIME_ID)

If you have an older version of Python, you can either install a C API package like posix_timers to give you clock_gettime(). Check out: https://pypi.python.org/pypi/posix_timers/

Alternatively, you can use ctypes to access it your self. There is a stack overflow question that shows how to do this with a monotonic clock here: How do I get monotonic time durations in python?

The only difference you would have to make is to define CLOCK_PROCESS_CPUTIME_ID = 2 and use that instead of CLOCK_MONOTONIC_RAW.

However, the best answer is probably the one given by jonrsharpe, which is to use timeit or one of the other profiling tools that comes with Python.

High-precision clock in Python

The standard time.time() function provides sub-second precision, though that precision varies by platform. For Linux and Mac precision is +- 1 microsecond or 0.001 milliseconds. Python on Windows uses +- 16 milliseconds precision due to clock implementation problems due to process interrupts. The timeit module can provide higher resolution if you're measuring execution time.

>>> import time
>>> time.time() #return seconds from epoch
1261367718.971009

Python 3.7 introduces new functions to the time module that provide higher resolution:

>>> import time
>>> time.time_ns()
1530228533161016309
>>> time.time_ns() / (10 ** 9) # convert to floating-point seconds
1530228544.0792289

How to get monotonic part of time.Time in Go

A Go time.Time stores 2 timestamps:

  • Wall clock
  • Monotonic duration since process start (optional, via time.Now)

m=+0.007725255 represents the monotonic duration since the start of the process (when present in a time.Time).

Go calculates this offset by recording time.startNano timestamp during initialisation (not public). time.Now uses startNano to calculate the monotonic duration stored in time.Time. There is no simple public API to directly retrieve this value since it should never be needed.

In practice, you should simply subtract 2 timestamps generated via time.Now in your current process and the result will be the monotonic duration. If you need to know the duration since process startup you should record a startup timestamp during initalisation.

Example:

package main

import (
"errors"
"fmt"
"math"
"strconv"
"strings"
"time"
)

func main() {
t0 := time.Now()
fmt.Println("...example event...")
time.Sleep(time.Millisecond)
t1 := time.Now()
fmt.Println("Event start:", t0)
fmt.Println("Event completed:", t1)

fmt.Println("=== Not recommended ===")
offsetT0, _ := monoOffset(t0)
fmt.Println("Parsed start offset:", offsetT0)
startNano, _ := calculateStartNano()
fmt.Println("Calculate start offset via startNano: ", t0.Sub(startNano))

fmt.Println("=== Recommended ===")
fmt.Println("Example event duration:", t1.Sub(t0))
fmt.Println("Time since startup", time.Since(t0))
}

// You should never need anything below here (code smell).

func monoOffset(t time.Time) (time.Duration, error) {
// Recommend strings.Cut on Go1.18+.
parts := strings.Split(t.String(), " m=")
if len(parts) != 2 {
return 0, errors.New("missing monotonic offset")
}

seconds, err := strconv.ParseFloat(parts[1], 64)
if err != nil {
return 0, err
}

nanos := math.Round(seconds * 1e9)
return time.Duration(nanos), nil
}

func calculateStartNano() (time.Time, error) {
now := time.Now()
offset, err := monoOffset(now)
if err != nil {
return time.Time{}, err
}
return now.Add(-offset), nil
}

Outputs:

...example event...
Event start: 2022-04-16 16:54:25.088159496 +1000 AEST m=+0.000079273
Event completed: 2022-04-16 16:54:25.089438935 +1000 AEST m=+0.001358685
=== Not recommended ===
Parsed start offset : 79.273µs
Calculate start offset via startNano: 79.273µs
=== Recommended ===
Example event duration: 1.279412ms
Time since startup 2.016789ms

Measuring elapsed time in python

You can use perf_counter function of time module in Python Standard Library:

from datetime import timedelta
from time import perf_counter

startTime = perf_counter()
CallYourFunc()
finishedTime = perf_counter()
duration = timedelta(seconds=(finishedTime - startTime))

How to calculate the time interval between two time strings

Yes, definitely datetime is what you need here. Specifically, the datetime.strptime() method, which parses a string into a datetime object.

from datetime import datetime
s1 = '10:33:26'
s2 = '11:15:49' # for example
FMT = '%H:%M:%S'
tdelta = datetime.strptime(s2, FMT) - datetime.strptime(s1, FMT)

That gets you a timedelta object that contains the difference between the two times. You can do whatever you want with that, e.g. converting it to seconds or adding it to another datetime.

This will return a negative result if the end time is earlier than the start time, for example s1 = 12:00:00 and s2 = 05:00:00. If you want the code to assume the interval crosses midnight in this case (i.e. it should assume the end time is never earlier than the start time), you can add the following lines to the above code:

if tdelta.days < 0:
tdelta = timedelta(
days=0,
seconds=tdelta.seconds,
microseconds=tdelta.microseconds
)

(of course you need to include from datetime import timedelta somewhere). Thanks to J.F. Sebastian for pointing out this use case.



Related Topics



Leave a reply



Submit