Allow the --enable-pystats build option to be used with free-threading. The stats are now stored on a per-interpreter basis, rather than process global. For free-threaded builds, the stats structure is allocated per-thread and then periodically merged into the per-interpreter stats structure (on thread exit or when the reporting function is called). Most of the pystats related code has be moved into the file Python/pystats.c.
216 lines
5.8 KiB
Python
216 lines
5.8 KiB
Python
import sys
|
|
import textwrap
|
|
import unittest
|
|
from test.support import script_helper
|
|
|
|
# This function is available for the --enable-pystats config.
|
|
HAVE_PYSTATS = hasattr(sys, '_stats_on')
|
|
|
|
TEST_TEMPLATE = """
|
|
import sys
|
|
import threading
|
|
import time
|
|
|
|
THREADS = 2
|
|
|
|
class A:
|
|
pass
|
|
|
|
class B:
|
|
pass
|
|
|
|
def modify_class():
|
|
# This is used as a rare event we can assume doesn't happen unless we do it.
|
|
# It increments the "Rare event (set_class)" count.
|
|
a = A()
|
|
a.__class__ = B
|
|
|
|
TURNED_ON = False
|
|
def stats_on():
|
|
global TURNED_ON
|
|
sys._stats_on()
|
|
TURNED_ON = True
|
|
|
|
TURNED_OFF = False
|
|
def stats_off():
|
|
global TURNED_OFF
|
|
sys._stats_off()
|
|
TURNED_OFF = True
|
|
|
|
CLEARED = False
|
|
def stats_clear():
|
|
global CLEARED
|
|
sys._stats_clear()
|
|
CLEARED = True
|
|
|
|
def func_start():
|
|
pass
|
|
|
|
def func_end():
|
|
pass
|
|
|
|
def func_test(thread_id):
|
|
pass
|
|
|
|
_TEST_CODE_
|
|
|
|
func_start()
|
|
threads = []
|
|
for i in range(THREADS):
|
|
t = threading.Thread(target=func_test, args=(i,))
|
|
threads.append(t)
|
|
t.start()
|
|
for t in threads:
|
|
t.join()
|
|
func_end()
|
|
"""
|
|
|
|
|
|
def run_test_code(
|
|
test_code,
|
|
args=[],
|
|
env_vars=None,
|
|
):
|
|
"""Run test code and return the value of the "set_class" stats counter.
|
|
"""
|
|
code = textwrap.dedent(TEST_TEMPLATE)
|
|
code = code.replace('_TEST_CODE_', textwrap.dedent(test_code))
|
|
script_args = args + ['-c', code]
|
|
env_vars = env_vars or {}
|
|
res, _ = script_helper.run_python_until_end(*script_args, **env_vars)
|
|
stderr = res.err.decode("ascii", "backslashreplace")
|
|
for line in stderr.split('\n'):
|
|
if 'Rare event (set_class)' in line:
|
|
label, _, value = line.partition(':')
|
|
return value.strip()
|
|
return ''
|
|
|
|
|
|
@unittest.skipUnless(HAVE_PYSTATS, "requires pystats build option")
|
|
class TestPyStats(unittest.TestCase):
|
|
"""Tests for pystats functionality (requires --enable-pystats build
|
|
option).
|
|
"""
|
|
|
|
def test_stats_toggle_on(self):
|
|
"""Check the toggle on functionality.
|
|
"""
|
|
code = """
|
|
def func_start():
|
|
modify_class()
|
|
"""
|
|
|
|
# If turned on with command line flag, should get one count.
|
|
stat_count = run_test_code(code, args=['-X', 'pystats'])
|
|
self.assertEqual(stat_count, '1')
|
|
|
|
# If turned on with env var, should get one count.
|
|
stat_count = run_test_code(code, env_vars={'PYTHONSTATS': '1'})
|
|
self.assertEqual(stat_count, '1')
|
|
|
|
# If not turned on, should be no counts.
|
|
stat_count = run_test_code(code)
|
|
self.assertEqual(stat_count, '')
|
|
|
|
code = """
|
|
def func_start():
|
|
modify_class()
|
|
sys._stats_on()
|
|
modify_class()
|
|
"""
|
|
# Not initially turned on but enabled by sys._stats_on(), should get
|
|
# one count.
|
|
stat_count = run_test_code(code)
|
|
self.assertEqual(stat_count, '1')
|
|
|
|
def test_stats_toggle_on_thread(self):
|
|
"""Check the toggle on functionality when threads are used.
|
|
"""
|
|
code = """
|
|
def func_test(thread_id):
|
|
if thread_id == 0:
|
|
modify_class()
|
|
stats_on()
|
|
modify_class()
|
|
else:
|
|
while not TURNED_ON:
|
|
pass
|
|
modify_class()
|
|
"""
|
|
# Turning on in one thread will count in other thread.
|
|
stat_count = run_test_code(code)
|
|
self.assertEqual(stat_count, '2')
|
|
|
|
code = """
|
|
def func_test(thread_id):
|
|
if thread_id == 0:
|
|
modify_class()
|
|
stats_off()
|
|
modify_class()
|
|
else:
|
|
while not TURNED_OFF:
|
|
pass
|
|
modify_class()
|
|
"""
|
|
# Turning off in one thread will not count in other threads.
|
|
stat_count = run_test_code(code, args=['-X', 'pystats'])
|
|
self.assertEqual(stat_count, '1')
|
|
|
|
def test_thread_exit_merge(self):
|
|
"""Check that per-thread stats (when free-threading enabled) are merged.
|
|
"""
|
|
code = """
|
|
def func_test(thread_id):
|
|
modify_class()
|
|
if thread_id == 0:
|
|
raise SystemExit
|
|
"""
|
|
# Stats from a thread exiting early should still be counted.
|
|
stat_count = run_test_code(code, args=['-X', 'pystats'])
|
|
self.assertEqual(stat_count, '2')
|
|
|
|
def test_stats_dump(self):
|
|
"""Check that sys._stats_dump() works.
|
|
"""
|
|
code = """
|
|
def func_test(thread_id):
|
|
if thread_id == 0:
|
|
stats_on()
|
|
else:
|
|
while not TURNED_ON:
|
|
pass
|
|
modify_class()
|
|
sys._stats_dump()
|
|
stats_off()
|
|
"""
|
|
# Stats from a thread exiting early should still be counted.
|
|
stat_count = run_test_code(code)
|
|
self.assertEqual(stat_count, '1')
|
|
|
|
def test_stats_clear(self):
|
|
"""Check that sys._stats_clear() works.
|
|
"""
|
|
code = """
|
|
ready = False
|
|
def func_test(thread_id):
|
|
global ready
|
|
if thread_id == 0:
|
|
stats_on()
|
|
modify_class()
|
|
while not ready:
|
|
pass # wait until other thread has called modify_class()
|
|
stats_clear() # clears stats for all threads
|
|
else:
|
|
while not TURNED_ON:
|
|
pass
|
|
modify_class()
|
|
ready = True
|
|
"""
|
|
# Clearing stats will clear for all threads
|
|
stat_count = run_test_code(code)
|
|
self.assertEqual(stat_count, '0')
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|