Skip to content

Commit 4bd41c9

Browse files
authored
bpo-32025: Add time.thread_time() (#4410)
* bpo-32025: Add time.thread_time() * Add missing #endif * Add NEWS blurb * Add docs and whatsnew * Address review comments * Review comments
1 parent 762b957 commit 4bd41c9

File tree

5 files changed

+206
-0
lines changed

5 files changed

+206
-0
lines changed

Doc/library/time.rst

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@ Functions
241241
* ``'monotonic'``: :func:`time.monotonic`
242242
* ``'perf_counter'``: :func:`time.perf_counter`
243243
* ``'process_time'``: :func:`time.process_time`
244+
* ``'thread_time'``: :func:`time.thread_time`
244245
* ``'time'``: :func:`time.time`
245246

246247
The result has the following attributes:
@@ -603,6 +604,32 @@ Functions
603604
of the calendar date may be accessed as attributes.
604605

605606

607+
.. function:: thread_time() -> float
608+
609+
.. index::
610+
single: CPU time
611+
single: processor time
612+
single: benchmarking
613+
614+
Return the value (in fractional seconds) of the sum of the system and user
615+
CPU time of the current thread. It does not include time elapsed during
616+
sleep. It is thread-specific by definition. The reference point of the
617+
returned value is undefined, so that only the difference between the results
618+
of consecutive calls in the same thread is valid.
619+
620+
Availability: Windows, Linux, Unix systems supporting
621+
``CLOCK_THREAD_CPUTIME_ID``.
622+
623+
.. versionadded:: 3.7
624+
625+
626+
.. function:: thread_time_ns() -> int
627+
628+
Similar to :func:`thread_time` but return time as nanoseconds.
629+
630+
.. versionadded:: 3.7
631+
632+
606633
.. function:: time_ns() -> int
607634

608635
Similar to :func:`time` but returns time as an integer number of nanoseconds

Doc/whatsnew/3.7.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,10 @@ Add new clock identifiers:
372372
the time the system has been running and not suspended, providing accurate
373373
uptime measurement, both absolute and interval.
374374

375+
Added functions :func:`time.thread_time` and :func:`time.thread_time_ns`
376+
to get per-thread CPU time measurements.
377+
(Contributed by Antoine Pitrou in :issue:`32025`.)
378+
375379
unittest.mock
376380
-------------
377381

Lib/test/test_time.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@ class _PyTime(enum.IntEnum):
4747
)
4848

4949

50+
def busy_wait(duration):
51+
deadline = time.monotonic() + duration
52+
while time.monotonic() < deadline:
53+
pass
54+
55+
5056
class TimeTestCase(unittest.TestCase):
5157

5258
def setUp(self):
@@ -81,6 +87,10 @@ def check_ns(sec, ns):
8187
check_ns(time.process_time(),
8288
time.process_time_ns())
8389

90+
if hasattr(time, 'thread_time'):
91+
check_ns(time.thread_time(),
92+
time.thread_time_ns())
93+
8494
if hasattr(time, 'clock_gettime'):
8595
check_ns(time.clock_gettime(time.CLOCK_REALTIME),
8696
time.clock_gettime_ns(time.CLOCK_REALTIME))
@@ -486,10 +496,57 @@ def test_process_time(self):
486496
# on Windows
487497
self.assertLess(stop - start, 0.020)
488498

499+
# process_time() should include CPU time spent in any thread
500+
start = time.process_time()
501+
busy_wait(0.100)
502+
stop = time.process_time()
503+
self.assertGreaterEqual(stop - start, 0.020) # machine busy?
504+
505+
t = threading.Thread(target=busy_wait, args=(0.100,))
506+
start = time.process_time()
507+
t.start()
508+
t.join()
509+
stop = time.process_time()
510+
self.assertGreaterEqual(stop - start, 0.020) # machine busy?
511+
489512
info = time.get_clock_info('process_time')
490513
self.assertTrue(info.monotonic)
491514
self.assertFalse(info.adjustable)
492515

516+
def test_thread_time(self):
517+
if not hasattr(time, 'thread_time'):
518+
if sys.platform.startswith(('linux', 'win')):
519+
self.fail("time.thread_time() should be available on %r"
520+
% (sys.platform,))
521+
else:
522+
self.skipTest("need time.thread_time")
523+
524+
# thread_time() should not include time spend during a sleep
525+
start = time.thread_time()
526+
time.sleep(0.100)
527+
stop = time.thread_time()
528+
# use 20 ms because thread_time() has usually a resolution of 15 ms
529+
# on Windows
530+
self.assertLess(stop - start, 0.020)
531+
532+
# thread_time() should include CPU time spent in current thread...
533+
start = time.thread_time()
534+
busy_wait(0.100)
535+
stop = time.thread_time()
536+
self.assertGreaterEqual(stop - start, 0.020) # machine busy?
537+
538+
# ...but not in other threads
539+
t = threading.Thread(target=busy_wait, args=(0.100,))
540+
start = time.thread_time()
541+
t.start()
542+
t.join()
543+
stop = time.thread_time()
544+
self.assertLess(stop - start, 0.020)
545+
546+
info = time.get_clock_info('thread_time')
547+
self.assertTrue(info.monotonic)
548+
self.assertFalse(info.adjustable)
549+
493550
@unittest.skipUnless(hasattr(time, 'clock_settime'),
494551
'need time.clock_settime')
495552
def test_monotonic_settime(self):
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add time.thread_time() and time.thread_time_ns()

Modules/timemodule.c

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1258,6 +1258,112 @@ Process time for profiling as nanoseconds:\n\
12581258
sum of the kernel and user-space CPU time.");
12591259

12601260

1261+
#if defined(MS_WINDOWS)
1262+
#define HAVE_THREAD_TIME
1263+
static int
1264+
_PyTime_GetThreadTimeWithInfo(_PyTime_t *tp, _Py_clock_info_t *info)
1265+
{
1266+
HANDLE thread;
1267+
FILETIME creation_time, exit_time, kernel_time, user_time;
1268+
ULARGE_INTEGER large;
1269+
_PyTime_t ktime, utime, t;
1270+
BOOL ok;
1271+
1272+
thread = GetCurrentThread();
1273+
ok = GetThreadTimes(thread, &creation_time, &exit_time,
1274+
&kernel_time, &user_time);
1275+
if (!ok) {
1276+
PyErr_SetFromWindowsErr(0);
1277+
return -1;
1278+
}
1279+
1280+
if (info) {
1281+
info->implementation = "GetThreadTimes()";
1282+
info->resolution = 1e-7;
1283+
info->monotonic = 1;
1284+
info->adjustable = 0;
1285+
}
1286+
1287+
large.u.LowPart = kernel_time.dwLowDateTime;
1288+
large.u.HighPart = kernel_time.dwHighDateTime;
1289+
ktime = large.QuadPart;
1290+
1291+
large.u.LowPart = user_time.dwLowDateTime;
1292+
large.u.HighPart = user_time.dwHighDateTime;
1293+
utime = large.QuadPart;
1294+
1295+
/* ktime and utime have a resolution of 100 nanoseconds */
1296+
t = _PyTime_FromNanoseconds((ktime + utime) * 100);
1297+
*tp = t;
1298+
return 0;
1299+
}
1300+
1301+
#elif defined(HAVE_CLOCK_GETTIME) && defined(CLOCK_PROCESS_CPUTIME_ID)
1302+
#define HAVE_THREAD_TIME
1303+
static int
1304+
_PyTime_GetThreadTimeWithInfo(_PyTime_t *tp, _Py_clock_info_t *info)
1305+
{
1306+
struct timespec ts;
1307+
const clockid_t clk_id = CLOCK_THREAD_CPUTIME_ID;
1308+
const char *function = "clock_gettime(CLOCK_THREAD_CPUTIME_ID)";
1309+
1310+
if (clock_gettime(clk_id, &ts)) {
1311+
PyErr_SetFromErrno(PyExc_OSError);
1312+
return -1;
1313+
}
1314+
if (info) {
1315+
struct timespec res;
1316+
info->implementation = function;
1317+
info->monotonic = 1;
1318+
info->adjustable = 0;
1319+
if (clock_getres(clk_id, &res)) {
1320+
PyErr_SetFromErrno(PyExc_OSError);
1321+
return -1;
1322+
}
1323+
info->resolution = res.tv_sec + res.tv_nsec * 1e-9;
1324+
}
1325+
1326+
if (_PyTime_FromTimespec(tp, &ts) < 0) {
1327+
return -1;
1328+
}
1329+
return 0;
1330+
}
1331+
#endif
1332+
1333+
#ifdef HAVE_THREAD_TIME
1334+
static PyObject *
1335+
time_thread_time(PyObject *self, PyObject *unused)
1336+
{
1337+
_PyTime_t t;
1338+
if (_PyTime_GetThreadTimeWithInfo(&t, NULL) < 0) {
1339+
return NULL;
1340+
}
1341+
return _PyFloat_FromPyTime(t);
1342+
}
1343+
1344+
PyDoc_STRVAR(thread_time_doc,
1345+
"thread_time() -> float\n\
1346+
\n\
1347+
Thread time for profiling: sum of the kernel and user-space CPU time.");
1348+
1349+
static PyObject *
1350+
time_thread_time_ns(PyObject *self, PyObject *unused)
1351+
{
1352+
_PyTime_t t;
1353+
if (_PyTime_GetThreadTimeWithInfo(&t, NULL) < 0) {
1354+
return NULL;
1355+
}
1356+
return _PyTime_AsNanosecondsObject(t);
1357+
}
1358+
1359+
PyDoc_STRVAR(thread_time_ns_doc,
1360+
"thread_time() -> int\n\
1361+
\n\
1362+
Thread time for profiling as nanoseconds:\n\
1363+
sum of the kernel and user-space CPU time.");
1364+
#endif
1365+
1366+
12611367
static PyObject *
12621368
time_get_clock_info(PyObject *self, PyObject *args)
12631369
{
@@ -1311,6 +1417,13 @@ time_get_clock_info(PyObject *self, PyObject *args)
13111417
return NULL;
13121418
}
13131419
}
1420+
#ifdef HAVE_THREAD_TIME
1421+
else if (strcmp(name, "thread_time") == 0) {
1422+
if (_PyTime_GetThreadTimeWithInfo(&t, &info) < 0) {
1423+
return NULL;
1424+
}
1425+
}
1426+
#endif
13141427
else {
13151428
PyErr_SetString(PyExc_ValueError, "unknown clock");
13161429
return NULL;
@@ -1519,6 +1632,10 @@ static PyMethodDef time_methods[] = {
15191632
{"monotonic_ns", time_monotonic_ns, METH_NOARGS, monotonic_ns_doc},
15201633
{"process_time", time_process_time, METH_NOARGS, process_time_doc},
15211634
{"process_time_ns", time_process_time_ns, METH_NOARGS, process_time_ns_doc},
1635+
#ifdef HAVE_THREAD_TIME
1636+
{"thread_time", time_thread_time, METH_NOARGS, thread_time_doc},
1637+
{"thread_time_ns", time_thread_time_ns, METH_NOARGS, thread_time_ns_doc},
1638+
#endif
15221639
{"perf_counter", time_perf_counter, METH_NOARGS, perf_counter_doc},
15231640
{"perf_counter_ns", time_perf_counter_ns, METH_NOARGS, perf_counter_ns_doc},
15241641
{"get_clock_info", time_get_clock_info, METH_VARARGS, get_clock_info_doc},

0 commit comments

Comments
 (0)