From 51b61fa439f1898e2a35fb67ec8626b75163b5ee Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Sun, 15 Apr 2012 11:46:01 +0200 Subject: [PATCH] PEP 418: Cameron Simpson rewrote the text of his alternative --- pep-0418.txt | 58 +++++- pep-0418/clockutils.py | 408 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 457 insertions(+), 9 deletions(-) create mode 100644 pep-0418/clockutils.py diff --git a/pep-0418.txt b/pep-0418.txt index 69fe09238..8a05c8bb0 100644 --- a/pep-0418.txt +++ b/pep-0418.txt @@ -2,7 +2,7 @@ PEP: 418 Title: Add monotonic time, performance counter and process time functions Version: $Revision$ Last-Modified: $Date$ -Author: Jim Jewett , Victor Stinner +Author: Cameron Simpson , Jim Jewett , Victor Stinner Status: Draft Type: Standards Track Content-Type: text/x-rst @@ -432,20 +432,45 @@ Issues: else? -One function choosing the clock from a list of constraints ----------------------------------------------------------- +Choosing the clock from a list of constraints +--------------------------------------------- -``time.get_clock(*flags)`` with the following flags: +The PEP as proposed offers a few new clocks, but their guarentees +are deliberately loose in order to offer useful clocks on different +platforms. This inherently embeds policy in the calls, and the +caller must thus choose a policy. + +The "choose a clock" approach suggests an additional API to let +callers implement their own policy if necessary +by making most platform clocks available and letting the caller pick amongst them. +The PEP's suggested clocks are still expected to be available for the common +simple use cases. + +To do this two facilities are needed: +an enumeration of clocks, and metadata on the clocks to enable the user to +evaluate their suitability. + +The primary interface is a function make simple choices easy: +the caller can use ``time.get_clock(*flags)`` with some combination of flags. +This include at least: * time.MONOTONIC: clock cannot go backward -* time.STEADY: clock rate is steady and the clock is not adjusted +* time.STEADY: clock rate is steady +* time.ADJUSTED: clock may be adjusted, for example by NTP * time.HIGHRES: clock with the highest precision -time.get_clock() returns None if the clock is found and so calls can -be chained using the or operator. Example:: +It returns a clock object with a .now() method returning the current time. +The clock object is annotated with metadata describing the clock feature set; +its .flags field will contain at least all the requested flags. - get_time = time.get_clock(time.MONOTONIC) or time.get_clock(time.STEADY) or time.time() - t = get_time() +time.get_clock() returns None if no matching clock is found and so calls can +be chained using the or operator. Example of a simple policy decision:: + + T = get_clock(MONOTONIC) or get_clock(STEADY) or get_clock() + t = T.now() + +The available clocks always at least include a wrapper for ``time.time()``, +so a final call with no flags can always be used to obtain a working clock. Example of flags of system clocks: @@ -455,6 +480,21 @@ Example of flags of system clocks: * CLOCK_MONOTONIC_RAW: MONOTONIC | STEADY * gettimeofday(): (no flag) +The clock objects contain other metadata including the clock flags +with additional feature flags above those listed above, the name +of the underlying OS facility, and clock precisions. + +``time.get_clock()`` still chooses a single clock; an enumeration +facility is also required. +The most obvious method is to offer ``time.get_clocks()`` with the +same signature as ``time.get_clock()``, but returning a sequence +of all clocks matching the requested flags. +Requesting no flags would thus enumerate all available clocks, +allowing the caller to make an arbitrary choice amongst them based +on their metadata. + +Example partial implementation: +`clockutils.py `_. One function with a flag: time.monotonic(fallback=True) ------------------------------------------------------- diff --git a/pep-0418/clockutils.py b/pep-0418/clockutils.py new file mode 100644 index 000000000..9aad79a4e --- /dev/null +++ b/pep-0418/clockutils.py @@ -0,0 +1,408 @@ +#!/usr/bin/python +# +# Framework to present system clocks by feature, intended to avoid +# the library-as-policy pitfalls of the discussion around PEP 418. +# +# My 2c: +# http://www.gossamer-threads.com/lists/python/dev/977474#977474 +# http://www.gossamer-threads.com/lists/python/dev/977495#977495 +# or: +# http://www.mail-archive.com/python-dev@python.org/msg66174.html +# http://www.mail-archive.com/python-dev@python.org/msg66179.html +# - Cameron Simpson 02apr2012 +# + +from collections import namedtuple +from time import time +import os + +# the module exposing OS clock features +_time = os + +HIGHRES = 0x01 # high resolution +MONOTONIC = 0x02 # never goes backwards +STEADY = 0x04 # never steps; implies MONOTONIC +ADJUSTED = 0x08 # may be adjusted, for example by NTP +WALLCLOCK = 0x10 # tracks real world time, will usually be ADJUSTED too +RUNTIME = 0x20 # track system run time - stops when system suspended +SYNTHETIC = 0x40 # a synthetic clock, computed from other clocks + +def get_clock(flags=0, clocklist=None): + ''' Return a clock based on the supplied `flags`. + The returned clock shall have all the requested flags. + If no clock matches, return None. + ''' + for clock in get_clocks(flags=flags, clocklist=clocklist): + return clock + return None + +def get_clocks(flags=0, clocklist=None): + ''' Yield all clocks matching the supplied `flags`. + The returned clocks shall have all the requested flags. + ''' + if clocklist is None: + clocklist = ALL_CLOCKS + for clock in clocklist: + if clock.flags & flags == flags: + yield clock.factory() + +def monotonic_clock(other_flags=0): + ''' Return a monotonic clock, preferably high resolution. + ''' + return get_clock(MONOTONIC|HIGHRES|other_flags, MONOTONIC_CLOCKS) \ + or get_clock(MONOTONIC|other_flags, MONOTONIC_CLOCKS) + +def steady_clock(other_flags=0): + ''' Return a steady clock, preferably high resolution. + ''' + return get_clock(STEADY|HIGHRES|other_flags, STEADY_CLOCKS) \ + or get_clock(STEADY|other_flags, STEADY_CLOCKS) + +def highres_clock(other_flags=0): + ''' Return a high resolution clock, preferably steady. + ''' + return get_clock(HIGHRES|STEADY|other_flags, HIGHRES_CLOCKS) \ + or get_clock(HIGHRES|other_flags, HIGHRES_CLOCKS) + +_global_monotonic = None + +def monotonic(): + ''' Return the current time according to the default monotonic clock. + ''' + global _global_monotonic + if _global_monotonic is None: + _global_monotonic = monotonic_clock() + if _global_monotonic is None: + raise RunTimeError("no monotonic clock available") + return _global_monotonic.now() + +_global_hires = None + +def highres(): + ''' Return the current time according to the default high resolution clock. + ''' + global _global_hires + if _global_hires is None: + _global_hires = highres() + if _global_hires is None: + raise RunTimeError("no highres clock available") + return _global_hires.now() + +_global_steady = None + +def steady(): + ''' Return the current time according to the default steady clock. + ''' + global _global_steady + if _global_steady is None: + _global_steady = steady() + if _global_steady is None: + raise RunTimeError("no steady clock available") + return _global_steady.now() + +class _Clock_Flags(int): + ''' An int with human friendly str() and repr() for clock flags. + ''' + + _flag_names = ( + 'HIGHRES', + 'MONOTONIC', + 'STEADY', + 'ADJUSTED', + 'WALLCLOCK', + 'RUNTIME', + 'SYNTHETIC', + ) + + def __str__(self): + f = self + G = globals() + names = [] + for name in _Clock_Flags._flag_names: + n = G[name] + if f & n: + names.append(name) + f &= ~n + if f: + names.append('0x%02x' % f) + return '|'.join(names) if names else '0' + + def __repr__(self): + return '<%s %02x %s>' % (self.__class__.__name__, self, self) + +# now assemble the list of platform clocks + +class _Clock(object): + ''' A _Clock is the private base class of clock objects. + A clock has the following mandatory attributes: + .flags Feature flags describing the clock. + A clock may have the following optional attributes: + .epoch If present, the offset from time.time()'s epoch + of this clock's epoch(). Not all clocks have epochs; some + measure elapsed time from an unknown point and only the + difference in two measurements is useful. + .resolution + The resolution of the underlying clock facility's + reporting units. The clock can never be more precise than + this value. The actual accuracy of the reported time may + vary with adjustments and the real accuracy of the + underlying OS clock facility (which in turn may be + dependent on the precision of some hardware clock). + A clock must also supply the following methods: + .now() Report the current time in seconds, a float. + ''' + def __init__(self): + ''' Set instance attributes from class attributes, suitable to + singleton clocks. + ''' + klass = self.__class__ + self.flags = _Clock_Flags(klass.flags) + for attr in 'epoch', 'resolution': + try: + attrval = getattr(klass, attr) + except AttributeError: + pass + else: + setattr(self, attr, attrval) + + def __repr__(self): + props = [self.__class__.__name__] + for attr in sorted( [ attr for attr in dir(self) + if attr + and attr[0].isalpha() + and attr not in ('now',)] ): + props.append("%s=%s" % (attr, getattr(self, attr))) + return "<" + " ".join(props) + ">" + +ClockEntry = namedtuple('ClockEntry', 'flags factory') + +ALL_CLOCKS = [] + +def _SingletonClockEntry( klass ): + ''' Construct a ClockEntry for a Singleton class, typical for system clocks. + ''' + klock = klass() + return ClockEntry( klass.flags, lambda: klock ) + +# always provide good old time.time() +# provide no flags - this is a fallback - totally implementation agnostic +class _TimeDotTimeClock(_Clock): + ''' A clock made from time.time(). + ''' + flags = 0 + epoch = 0 + def now(self): + return time() +ALL_CLOCKS.append( _SingletonClockEntry(_TimeDotTimeClock) ) + +# load system specific clocks here +# in notional order of preference + +if os.name == "nt": + + class _NT_GetSystemTimeAsFileTimeClock(_Clock): + ''' A clock made from GetSystemTimeAsFileTime(). + ''' + flags = WALLCLOCK + epoch = EPOCH_16010101T000000 # 01jan1601 + # a negative value wrt 01jan1970 + resolution = 0.0000001 # 100 nanosecond units + # accuracy HW dependent? + def now(self): + # convert 100-nanosecond intervals since 1601 to UNIX style seconds + return ( _time._GetSystemTimeAsFileTime() / 10000000 + + NT_GetSystemTimeAsFileTimeClock.epoch + ) + ALL_CLOCKS.append( _SingletonClockEntry(_NT_GetSystemTimeAsFileTimeClock) ) + + class _NT_GetTickCount64(_Clock): + ''' Based on + http://msdn.microsoft.com/en-us/library/windows/desktop/ms724411%28v=vs.85%29.aspx + Note this this specificly disavows high resolution. + ''' + flags = RUNTIME|MONOTONIC + resolution = 0.001 + def now(self): + msecs = _time.GetTickCount64() + return msecs / 1000 + ALL_CLOCKS.append( _SingletonClockEntry(_NT_GetTickCount64) ) + +else: + + # presuming clock_gettime() and clock_getres() exposed in the os + # module, along with the clock id names + if hasattr(_time, "clock_gettime"): + + try: + clk_id = _time.CLOCK_REALTIME + except AttributeError: + pass + else: + try: + timespec = _time.clock_getres(clk_id) + except OSError: + pass + else: + class _UNIX_CLOCK_REALTIME(_Clock): + ''' A clock made from clock_gettime(CLOCK_REALTIME). + ''' + epoch = 0 + flags = WALLCLOCK + resolution = timespec.tv_sec + timespec.tv_nsec / 1000000000 + def now(): + timespec = _time.clock_gettime(_time.CLOCK_REALTIME) + return timespec.tv_sec + timespec.tv_nsec / 1000000000 + ALL_CLOCKS.append( _SingletonClockEntry(_UNIX_CLOCK_REALTIME) ) + + try: + clk_id = _time.CLOCK_MONOTONIC + except AttributeError: + pass + else: + try: + timespec = _time.clock_getres(clk_id) + except OSError: + pass + else: + class _UNIX_CLOCK_MONOTONIC(_Clock): + ''' A clock made from clock_gettime(CLOCK_MONOTONIC). + ''' + flags = MONOTONIC|STEADY|ADJUSTED + resolution = timespec.tv_sec + timespec.tv_nsec / 1000000000 + def now(): + timespec = _time.clock_gettime(_time.CLOCK_MONOTONIC) + return timespec.tv_sec + timespec.tv_nsec / 1000000000 + ALL_CLOCKS.append( _SingletonClockEntry(_UNIX_CLOCK_MONOTONIC) ) + + try: + clk_id = _time.CLOCK_MONOTONIC_RAW + except AttributeError: + pass + else: + try: + timespec = _time.clock_getres(clk_id) + except OSError: + pass + else: + class _UNIX_CLOCK_MONOTONIC_RAW(_Clock): + ''' A clock made from clock_gettime(CLOCK_MONOTONIC_RAW). + ''' + flags = MONOTONIC|STEADY + resolution = timespec.tv_sec + timespec.tv_nsec / 1000000000 + def now(): + timespec = _time.clock_gettime(_time.CLOCK_MONOTONIC_RAW) + return timespec.tv_sec + timespec.tv_nsec / 1000000000 + ALL_CLOCKS.append( _SingletonClockEntry(_UNIX_CLOCK_MONOTONIC_RAW) ) + + try: + clk_id = _time.CLOCK_PROCESS_CPUTIME_ID + except AttributeError: + pass + else: + try: + timespec = _time.clock_getres(clk_id) + except OSError: + pass + else: + class _UNIX_CLOCK_PROCESS_CPUTIME_ID(_Clock): + ''' A clock made from clock_gettime(CLOCK_PROCESS_CPUTIME_ID). + ''' + flags = MONOTONIC + resolution = timespec.tv_sec + timespec.tv_nsec / 1000000000 + def now(): + timespec = _time.clock_gettime(_time.CLOCK_PROCESS_CPUTIME_ID) + return timespec.tv_sec + timespec.tv_nsec / 1000000000 + ALL_CLOCKS.append( _SingletonClockEntry(_CLOCK_PROCESS_CPUTIME_ID) ) + + try: + clk_id = _time.CLOCK_THREAD_CPUTIME_ID + except AttributeError: + pass + else: + try: + timespec = _time.clock_getres(clk_id) + except OSError: + pass + else: + class _UNIX_CLOCK_THREAD_CPUTIME_ID(_Clock): + ''' A clock made from clock_gettime(CLOCK_THREAD_CPUTIME_ID). + ''' + flags = MONOTONIC + resolution = timespec.tv_sec + timespec.tv_nsec / 1000000000 + def now(): + timespec = _time.clock_gettime(_time.CLOCK_THREAD_CPUTIME_ID) + return timespec.tv_sec + timespec.tv_nsec / 1000000000 + ALL_CLOCKS.append( _SingletonClockEntry(_CLOCK_CLOCK_THREAD_CPUTIME_ID) ) + + if hasattr(_time, "gettimeofday"): + class _UNIX_gettimeofday(_Clock): + ''' A clock made from gettimeofday(). + ''' + epoch = 0 + flags = WALLCLOCK + resolution = 0.000001 + def now(self): + timeval = _time.gettimeofday() + return timeval.tv_sec + timeval.tv_usec / 1000000 + ALL_CLOCKS.append( _SingletonClockEntry(_UNIX_gettimeofday) ) + + if hasattr(_time, "ftime"): + class _UNIX_ftime(_Clock): + ''' A clock made from ftime(). + ''' + epoch = 0 + flags = WALLCLOCK|ADJUSTED + resolution = 0.001 + def now(self): + timeb = _time.ftime() + return timeb.time + timeb.millitm / 1000 + ALL_CLOCKS.append( _SingletonClockEntry(_UNIX_ftime) ) + +# an example synthetic clock, coming after time.time() +# because I think synthetic clocks should be less desired +# - they tend to have side effects; but perhaps offered anyway because +# they can offer flag combinations not always presented by the system +# clocks + +# a simple synthetic monotonic clock +# may skew with respect to other instances +# Steven D'Aprano wrote a better one +class SyntheticMonotonic(_Clock): + flags = SYNTHETIC|MONOTONIC + def __init__(self, base_clock=None): + _Clock.__init__(self) + if base_clock is None: + base_clock = _TimeDotTimeClock() + self.base_clock = base_clock + for attr in 'epoch', 'resolution': + try: + attrval = getattr(base_clock, attr) + except AttributeError: + pass + else: + setattr(self, attr, attrval) + self.__last = None + self.__base = base_clock + def now(self): + last = self.__last + t = self.__base.now() + if last is None or last < t: + self.__last = t + else: + t = last + return t + +ALL_CLOCKS.append( ClockEntry(SyntheticMonotonic.flags, SyntheticMonotonic) ) + +# With more clocks, these will be ALL_CLOCKS listed in order of preference +# for these types i.e. MONOTONIC_CLOCKS will list only monotonic clocks +# in order of quality (an arbitrary measure, perhaps). +MONOTONIC_CLOCKS = ALL_CLOCKS +HIGHRES_CLOCKS = ALL_CLOCKS +STEADY_CLOCKS = ALL_CLOCKS + +if __name__ == '__main__': + print("ALL_CLOCKS =", repr(ALL_CLOCKS)) + for clock in get_clocks(): + print("clock = %r" % (clock,)) + print(clock.__class__.__doc__)