diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index 0dcee56b7d233f..79262ea0905979 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -617,6 +617,17 @@ sys.monitoring Two new events are added: :monitoring-event:`BRANCH_LEFT` and :monitoring-event:`BRANCH_RIGHT`. The ``BRANCH`` event is deprecated. + +time +---- + +* Specialize :func:`time.sleep(0) ` on non-Windows platforms to + always use :manpage:`select(2)` even if the :manpage:`clock_nanosleep` or + :manpage:`nanosleep` functions are present as these functions would sleep + for much longer than what is actually needed. + (Contributed by Bénédikt Tran in :gh:`125997`.) + + tkinter ------- diff --git a/Lib/test/test_time.py b/Lib/test/test_time.py index 1c540bed33c71e..9ecba8dffca67b 100644 --- a/Lib/test/test_time.py +++ b/Lib/test/test_time.py @@ -158,10 +158,19 @@ def test_conversions(self): self.assertEqual(int(time.mktime(time.localtime(self.t))), int(self.t)) - def test_sleep(self): + def test_sleep_exceptions(self): + self.assertRaises(TypeError, time.sleep, []) + self.assertRaises(TypeError, time.sleep, "a") + self.assertRaises(TypeError, time.sleep, complex(0, 0)) + self.assertRaises(ValueError, time.sleep, -2) self.assertRaises(ValueError, time.sleep, -1) - time.sleep(1.2) + self.assertRaises(ValueError, time.sleep, -0.1) + + def test_sleep(self): + for value in [-0.0, 0, 0.0, 1e-6, 1, 1.2]: + with self.subTest(value=value): + time.sleep(value) def test_epoch(self): # bpo-43869: Make sure that Python use the same Epoch on all platforms: diff --git a/Misc/NEWS.d/next/Library/2024-12-26-13-09-25.gh-issue-125997.Vsibep.rst b/Misc/NEWS.d/next/Library/2024-12-26-13-09-25.gh-issue-125997.Vsibep.rst new file mode 100644 index 00000000000000..b1feeb60f37930 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-12-26-13-09-25.gh-issue-125997.Vsibep.rst @@ -0,0 +1,4 @@ +Specialize :func:`time.sleep(0) ` on non-Windows platforms to +always use :manpage:`select(2)` even if the :manpage:`clock_nanosleep` or +:manpage:`nanosleep` functions are present as these functions would sleep +for much longer than what is actually needed. Patch by Bénédikt Tran. diff --git a/Modules/timemodule.c b/Modules/timemodule.c index 340011fc08b551..1d45c2054a4bd8 100644 --- a/Modules/timemodule.c +++ b/Modules/timemodule.c @@ -72,6 +72,7 @@ module time /* Forward declarations */ static int pysleep(PyTime_t timeout); +static int pysleep_zero(void); // see gh-125997 typedef struct { @@ -2213,6 +2214,10 @@ static int pysleep(PyTime_t timeout) { assert(timeout >= 0); + assert(!PyErr_Occurred()); + if (timeout == 0) { // gh-125997 + return pysleep_zero(); + } #ifndef MS_WINDOWS #ifdef HAVE_CLOCK_NANOSLEEP @@ -2292,20 +2297,8 @@ pysleep(PyTime_t timeout) return 0; #else // MS_WINDOWS PyTime_t timeout_100ns = _PyTime_As100Nanoseconds(timeout, - _PyTime_ROUND_CEILING); - - // Maintain Windows Sleep() semantics for time.sleep(0) - if (timeout_100ns == 0) { - Py_BEGIN_ALLOW_THREADS - // A value of zero causes the thread to relinquish the remainder of its - // time slice to any other thread that is ready to run. If there are no - // other threads ready to run, the function returns immediately, and - // the thread continues execution. - Sleep(0); - Py_END_ALLOW_THREADS - return 0; - } - + _PyTime_ROUND_CEILING); + assert(timeout_100ns > 0); LARGE_INTEGER relative_timeout; // No need to check for integer overflow, both types are signed assert(sizeof(relative_timeout) == sizeof(timeout_100ns)); @@ -2390,3 +2383,57 @@ pysleep(PyTime_t timeout) return -1; #endif } + + +// time.sleep(0) optimized implementation. +// On error, raise an exception and return -1. +// On success, return 0. +// +// Rationale +// --------- +// time.sleep(0) is slower when using the generic implementation, but we make +// it faster than time.sleep(eps) for eps > 0 so to avoid some performance +// annoyance. For details, see https://github.com/python/cpython/pull/128274. +static int +pysleep_zero(void) +{ + assert(!PyErr_Occurred()); +#ifndef MS_WINDOWS + int ret, err; + Py_BEGIN_ALLOW_THREADS + // POSIX-compliant select(2) allows the 'timeout' parameter to + // be modified but also mandates that the function should return + // immediately if *both* structure's fields are zero (which is + // the case here). + // + // However, since System V (but not BSD) variant typically sets + // the timeout before returning (but does not specify whether + // this is also the case for zero timeouts), we prefer supplying + // a fresh timeout everytime. + struct timeval zero = {0, 0}; + ret = select(0, NULL, NULL, NULL, &zero); + err = errno; + Py_END_ALLOW_THREADS + if (ret == 0) { + return 0; + } + if (err != EINTR) { + errno = err; + PyErr_SetFromErrno(PyExc_OSError); + return -1; + } + /* sleep was interrupted by SIGINT */ + if (PyErr_CheckSignals()) { + return -1; + } +#else // Windows implementation + Py_BEGIN_ALLOW_THREADS + // A value of zero causes the thread to relinquish the remainder of its + // time slice to any other thread that is ready to run. If there are no + // other threads ready to run, the function returns immediately, and + // the thread continues execution. + Sleep(0); + Py_END_ALLOW_THREADS +#endif + return 0; +}