PEP 418: Cameron Simpson rewrote the text of his alternative

This commit is contained in:
Victor Stinner 2012-04-15 11:46:01 +02:00
parent b6f2709957
commit 51b61fa439
2 changed files with 457 additions and 9 deletions

View File

@ -2,7 +2,7 @@ PEP: 418
Title: Add monotonic time, performance counter and process time functions
Version: $Revision$
Last-Modified: $Date$
Author: Jim Jewett <jimjjewett@gmail.com>, Victor Stinner <victor.stinner@gmail.com>
Author: Cameron Simpson <cs@zip.com.au>, Jim Jewett <jimjjewett@gmail.com>, Victor Stinner <victor.stinner@gmail.com>
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 <http://hg.python.org/peps/file/tip/pep-0418/clockutils.py>`_.
One function with a flag: time.monotonic(fallback=True)
-------------------------------------------------------

408
pep-0418/clockutils.py Normal file
View File

@ -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 <cs@zip.com.au> 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__)