8000 gh-114911: Add CPUStopwatch test helper (GH-114912) · python/cpython@7acf1fb · GitHub
[go: up one dir, main page]

Skip to content

Commit 7acf1fb

Browse files
authored
gh-114911: Add CPUStopwatch test helper (GH-114912)
A few of our tests measure the time of CPU-bound operation, mainly to avoid quadratic or worse behaviour. Add a helper to ignore GC and time spent in other processes.
1 parent 3b63d07 commit 7acf1fb

File tree

3 files changed

+75
-42
lines changed

3 files changed

+75
-42
lines changed

Lib/test/support/__init__.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2381,6 +2381,46 @@ def sleeping_retry(timeout, err_msg=None, /,
23812381
delay = min(delay * 2, max_delay)
23822382

23832383

2384+
class CPUStopwatch:
2385+
"""Context manager to roughly time a CPU-bound operation.
2386+
2387+
Disables GC. Uses CPU time if it can (i.e. excludes sleeps & time of
2388+
other processes).
2389+
2390+
N.B.:
2391+
- This *includes* time spent in other threads.
2392+
- Some systems only have a coarse resolution; check
2393+
stopwatch.clock_info.rseolution if.
2394+
2395+
Usage:
2396+
2397+
with ProcessStopwatch() as stopwatch:
2398+
...
2399+
elapsed = stopwatch.seconds
2400+
resolution = stopwatch.clock_info.resolution
2401+
"""
2402+
def __enter__(self):
2403+
get_time = time.process_time
2404+
clock_info = time.get_clock_info('process_time')
2405+
if get_time() <= 0: # some platforms like WASM lack process_time()
2406+
get_time = time.monotonic
2407+
clock_info = time.get_clock_info('monotonic')
2408+
self.context = disable_gc()
2409+
self.context.__enter__()
2410+
self.get_time = get_time
2411+
self.clock_info = clock_info
2412+
self.start_time = get_time()
2413+
return self
2414+
2415+
def __exit__(self, *exc):
2416+
try:
2417+
end_time = self.get_time()
2418+
finally:
2419+
result = self.context.__exit__(*exc)
2420+
self.seconds = end_time - self.start_time
2421+
return result
2422+
2423+
23842424
@contextlib.contextmanager
23852425
def adjust_int_max_str_digits(max_digits):
23862426
"""Temporarily change the integer string conversion length limit."""

Lib/test/test_int.py

Lines changed: 26 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -664,84 +664,78 @@ def test_denial_of_service_prevented_int_to_str(self):
664664
"""Regression test: ensure we fail before performing O(N**2) work."""
665665
maxdigits = sys.get_int_max_str_digits()
666666
assert maxdigits < 50_000, maxdigits # A test prerequisite.
667-
get_time = time.process_time
668-
if get_time() <= 0: # some platforms like WASM lack process_time()
669-
get_time = time.monotonic
670667

671668
huge_int = int(f'0x{"c"*65_000}', base=16) # 78268 decimal digits.
672669
digits = 78_268
673-
with support.adjust_int_max_str_digits(digits):
674-
start = get_time()
670+
with (
671+
support.adjust_int_max_str_digits(digits),
672+
support.CPUStopwatch() as sw_convert):
675673
huge_decimal = str(huge_int)
676-
seconds_to_convert = get_time() - start
677674
self.assertEqual(len(huge_decimal), digits)
678675
# Ensuring that we chose a slow enough conversion to measure.
679676
# It takes 0.1 seconds on a Zen based cloud VM in an opt build.
680677
# Some OSes have a low res 1/64s timer, skip if hard to measure.
681-
if seconds_to_convert < 1/64:
678+
if sw_convert.seconds < sw_convert.clock_info.resolution * 2:
682679
raise unittest.SkipTest('"slow" conversion took only '
683-
f'{seconds_to_convert} seconds.')
680+
f'{sw_convert.seconds} seconds.')
684681

685682
# We test with the limit almost at the size needed to check performance.
686683
# The performant limit check is slightly fuzzy, give it a some room.
687684
with support.adjust_int_max_str_digits(int(.995 * digits)):
688-
with self.assertRaises(ValueError) as err:
689-
start = get_time()
685+
with (
686+
self.assertRaises(ValueError) as err,
687+
support.CPUStopwatch() as sw_fail_huge):
690688
str(huge_int)
691-
seconds_to_fail_huge = get_time() - start
692689
self.assertIn('conversion', str(err.exception))
693-
self.assertLessEqual(seconds_to_fail_huge, seconds_to_convert/2)
690+
self.assertLessEqual(sw_fail_huge.seconds, sw_convert.seconds/2)
694691

695692
# Now we test that a conversion that would take 30x as long also fails
696693
# in a similarly fast fashion.
697694
extra_huge_int = int(f'0x{"c"*500_000}', base=16) # 602060 digits.
698-
with self.assertRaises(ValueError) as err:
699-
start = get_time()
695+
with (
696+
self.assertRaises(ValueError) as err,
697+
support.CPUStopwatch() as sw_fail_extra_huge):
700698
# If not limited, 8 seconds said Zen based cloud VM.
701699
str(extra_huge_int)
702-
seconds_to_fail_extra_huge = get_time() - start
703700
self.assertIn('conversion', str(err.exception))
704-
self.assertLess(seconds_to_fail_extra_huge, seconds_to_convert/2)
701+
self.assertLess(sw_fail_extra_huge.seconds, sw_convert.seconds/2)
705702

706703
def test_denial_of_service_prevented_str_to_int(self):
707704
"""Regression test: ensure we fail before performing O(N**2) work."""
708705
maxdigits = sys.get_int_max_str_digits()
709706
assert maxdigits < 100_000, maxdigits # A test prerequisite.
710-
get_time = time.process_time
711-
if get_time() <= 0: # some platforms like WASM lack process_time()
712-
get_time = time.monotonic
713707

714708
digits = 133700
715709
huge = '8'*digits
716-
with support.adjust_int_max_str_digits(digits):
717-
start = get_time()
710+
with (
711+
support.adjust_int_max_str_digits(digits),
712+
support.CPUStopwatch() as sw_convert):
718713
int(huge)
719-
seconds_to_convert = get_time() - start
720714
# Ensuring that we chose a slow enough conversion to measure.
721715
# It takes 0.1 seconds on a Zen based cloud VM in an opt build.
722716
# Some OSes have a low res 1/64s timer, skip if hard to measure.
723-
if seconds_to_convert < 1/64:
717+
if sw_convert.seconds < sw_convert.clock_info.resolution * 2:
724718
raise unittest.SkipTest('"slow" conversion took only '
725-
f'{seconds_to_convert} seconds.')
719+
f'{sw_convert.seconds} seconds.')
726720

727721
with support.adjust_int_max_str_digits(digits - 1):
728-
with self.assertRaises(ValueError) as err:
729-
start = get_time()
722+
with (
723+
self.assertRaises(ValueError) as err,
724+
support.CPUStopwatch() as sw_fail_huge):
730725
int(huge)
731-
seconds_to_fail_huge = get_time() - start
732726
self.assertIn('conversion', str(err.exception))
733-
self.assertLessEqual(seconds_to_fail_huge, seconds_to_convert/2)
727+
self.assertLessEqual(sw_fail_huge.seconds, sw_convert.seconds/2)
734728

735729
# Now we test that a conversion that would take 30x as long also fails
736730
# in a similarly fast fashion.
737731
extra_huge = '7'*1_200_000
738-
with self.assertRaises(ValueError) as err:
739-
start = get_time()
732+
with (
733+
self.assertRaises(ValueError) as err,
734+
support.CPUStopwatch() as sw_fail_extra_huge):
740735
# If not limited, 8 seconds in the Zen based cloud VM.
741736
int(extra_huge)
742-
seconds_to_fail_extra_huge = get_time() - start
743737
self.assertIn('conversion', str(err.exception))
744-
self.assertLessEqual(seconds_to_fail_extra_huge, seconds_to_convert/2)
738+
self.assertLessEqual(sw_fail_extra_huge.seconds, sw_convert.seconds/2)
745739

746740
def test_power_of_two_bases_unlimited(self):
747741
"""The limit does not apply to power of 2 bases."""

Lib/test/test_re.py

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from test.support import (gc_collect, bigmemtest, _2G,
22
cpython_only, captured_stdout,
33
check_disallow_instantiation, is_emscripten, is_wasi,
4-
warnings_helper, SHORT_TIMEOUT)
4+
warnings_helper, SHORT_TIMEOUT, CPUStopwatch)
55
import locale
66
import re
77
import string
@@ -2284,17 +2284,16 @@ def test_bug_40736(self):
22842284

22852285
def test_search_anchor_at_beginning(self):
22862286
s = 'x'*10**7
2287-
start = time.perf_counter()
2288-
for p in r'\Ay', r'^y':
2289-
self.assertIsNone(re.search(p, s))
2290-
self.assertEqual(re.split(p, s), [s])
2291-
self.assertEqual(re.findall(p, s), [])
2292-
self.assertEqual(list(re.finditer(p, s)), [])
2293-
self.assertEqual(re.sub(p, '', s), s)
2294-
t = time.perf_counter() - start
2287+
with CPUStopwatch() as stopwatch:
2288+
for p in r'\Ay', r'^y':
2289+
self.assertIsNone(re.search(p, s))
2290+
self.assertEqual(re.split(p, s), [s])
2291+
self.assertEqual(re.findall(p, s), [])
2292+
self.assertEqual(list(re.finditer(p, s)), [])
2293+
self.assertEqual(re.sub(p, '', s), s)
22952294
# Without optimization it takes 1 second on my computer.
22962295
# With optimization -- 0.0003 seconds.
2297-
self.assertLess(t, 0.1)
2296+
self.assertLess(stopwatch.seconds, 0.1)
22982297

22992298
def test_possessive_quantifiers(self):
23002299
"""Test Possessive Quantifiers

0 commit comments

Comments
 (0)
0