| import gc | |
| import os | |
| import textwrap | |
| import time | |
| import unittest | |
| from test.support import ( | |
| Py_GIL_DISABLED, | |
| import_helper, | |
| requires_gil_enabled, | |
| requires_remote_subprocess_debugging, | |
| ) | |
| from test.test_profiling.test_sampling_profiler.helpers import test_subprocess | |
| try: | |
| import _remote_debugging # noqa: F401 | |
| except ImportError: | |
| raise unittest.SkipTest( | |
| "Test only runs when _remote_debugging is available" | |
| ) | |
| GC_STATS_FIELDS = ( | |
| "gen", "iid", "ts_start", "ts_stop", "collections", "collected", | |
| "uncollectable", "candidates", "heap_size", "duration") | |
| def get_interpreter_identifiers(gc_stats) -> tuple[int,...]: | |
| return tuple(sorted({s.iid for s in gc_stats})) | |
| def get_generations(gc_stats) -> tuple[int,int,int]: | |
| generations = set() | |
| for s in gc_stats: | |
| generations.add(s.gen) | |
| return tuple(sorted(generations)) | |
| def get_last_item(gc_stats, generation: int, iid: int): | |
| item = None | |
| for s in gc_stats: | |
| if s.gen == generation and s.iid == iid: | |
| if item is None or item.ts_start < s.ts_start: | |
| item = s | |
| return item | |
| def has_local_process_debugging(): | |
| try: | |
| return _remote_debugging.is_python_process(os.getpid()) | |
| except Exception: | |
| return False | |
| def check_gc_stats_fields(testcase, stats): | |
| testcase.assertIsInstance(stats, list) | |
| testcase.assertGreater(len(stats), 0) | |
| for item in stats: | |
| testcase.assertIsInstance(item, _remote_debugging.GCStatsInfo) | |
| testcase.assertEqual(type(item).__match_args__, GC_STATS_FIELDS) | |
| testcase.assertEqual(len(item), len(GC_STATS_FIELDS)) | |
| def gc_stats_counters_advanced(before_stats, after_stats, generations, iid): | |
| for generation in generations: | |
| before = get_last_item(before_stats, generation, iid) | |
| after = get_last_item(after_stats, generation, iid) | |
| if after is None or before is None: | |
| return False | |
| if after.duration <= before.duration: | |
| return False | |
| if after.candidates <= before.candidates: | |
| return False | |
| return True | |
| @unittest.skipUnless( | |
| has_local_process_debugging(), "requires local process debugging") | |
| class TestLocalGCStats(unittest.TestCase): | |
| _main_iid = 0 # main interpreter ID | |
| def test_gc_stats_fields(self): | |
| monitor = _remote_debugging.GCMonitor(os.getpid(), debug=True) | |
| stats = monitor.get_gc_stats(all_interpreters=False) | |
| check_gc_stats_fields(self, stats) | |
| def test_module_get_gc_stats_fields(self): | |
| stats = _remote_debugging.get_gc_stats( | |
| os.getpid(), all_interpreters=False) | |
| check_gc_stats_fields(self, stats) | |
| def test_all_interpreters_filter_for_local_process(self): | |
| interpreters = import_helper.import_module("concurrent.interpreters") | |
| source = """ | |
| import gc | |
| objects = [] | |
| obj = [] | |
| obj.append(obj) | |
| objects.append(obj) | |
| gc.collect(0) | |
| gc.collect(1) | |
| gc.collect(2) | |
| """ | |
| interp = interpreters.create() | |
| try: | |
| interp.exec(textwrap.dedent(source)) | |
| for generation in range(3): | |
| gc.collect(generation) | |
| main_stats = _remote_debugging.get_gc_stats( | |
| os.getpid(), all_interpreters=False) | |
| all_stats = _remote_debugging.get_gc_stats( | |
| os.getpid(), all_interpreters=True) | |
| finally: | |
| interp.close() | |
| self.assertEqual(get_interpreter_identifiers(main_stats), (0,)) | |
| self.assertIn(0, get_interpreter_identifiers(all_stats)) | |
| self.assertGreater(len(get_interpreter_identifiers(all_stats)), 1) | |
| self.assertEqual(get_generations(main_stats), (0, 1, 2)) | |
| self.assertEqual(get_generations(all_stats), (0, 1, 2)) | |
| for iid in get_interpreter_identifiers(all_stats): | |
| for generation in range(3): | |
| self.assertIsNotNone(get_last_item(all_stats, generation, iid)) | |
| @unittest.skipUnless(Py_GIL_DISABLED, "requires free-threaded GC") | |
| def test_gc_stats_counters_for_main_interpreter_free_threaded(self): | |
| generations = (0, 1, 2) | |
| before_stats = _remote_debugging.get_gc_stats( | |
| os.getpid(), all_interpreters=False) | |
| for generation in generations: | |
| self.assertIsNotNone( | |
| get_last_item(before_stats, generation, self._main_iid)) | |
| objects = [] | |
| for _ in range(1000): | |
| obj = [] | |
| obj.append(obj) | |
| objects.append(obj) | |
| for generation in generations: | |
| gc.collect(generation) | |
| after_stats = _remote_debugging.get_gc_stats( | |
| os.getpid(), all_interpreters=False) | |
| self.assertTrue( | |
| gc_stats_counters_advanced( | |
| before_stats, after_stats, generations, self._main_iid), | |
| (before_stats, after_stats) | |
| ) | |
| @requires_remote_subprocess_debugging() | |
| class TestGCStats(unittest.TestCase): | |
| @classmethod | |
| def setUpClass(cls): | |
| cls._main_iid = 0 # main interpreter ID | |
| cls._main_interpreter_script = ''' | |
| import gc | |
| import time | |
| gc.collect(0) | |
| gc.collect(1) | |
| gc.collect(2) | |
| _test_sock.sendall(b"working") | |
| objects = [] | |
| while True: | |
| if len(objects) > 100: | |
| objects = [] | |
| obj = [] | |
| obj.append(obj) | |
| objects.append(obj) | |
| time.sleep(0.1) | |
| gc.collect(0) | |
| gc.collect(1) | |
| gc.collect(2) | |
| ''' | |
| cls._script = ''' | |
| import concurrent.interpreters as interpreters | |
| import gc | |
| import time | |
| source = """if True: | |
| import gc | |
| if "objects" not in globals(): | |
| objects = [] | |
| if len(objects) > 100: | |
| objects = [] | |
| obj = [] | |
| obj.append(obj) | |
| objects.append(obj) | |
| gc.collect(0) | |
| gc.collect(1) | |
| gc.collect(2) | |
| """ | |
| if {0}: | |
| interp = interpreters.create() | |
| interp.exec(source) | |
| gc.collect(0) | |
| gc.collect(1) | |
| gc.collect(2) | |
| _test_sock.sendall(b"working") | |
| objects = [] | |
| while True: | |
| if len(objects) > 100: | |
| objects = [] | |
| obj = [] | |
| obj.append(obj) | |
| objects.append(obj) | |
| time.sleep(0.1) | |
| if {0}: | |
| interp.exec(source) | |
| gc.collect(0) | |
| gc.collect(1) | |
| gc.collect(2) | |
| ''' | |
| def _gc_stats_advanced(self, before_stats, after_stats, generations): | |
| for generation in generations: | |
| before = get_last_item(before_stats, generation, self._main_iid) | |
| after = get_last_item(after_stats, generation, self._main_iid) | |
| if after is None or before is None: | |
| return False | |
| if after.ts_stop <= before.ts_stop: | |
| return False | |
| return True | |
| def _collect_gc_stats(self, script: str, all_interpreters: bool, | |
| generations=(2,)): | |
| with (test_subprocess(script, wait_for_working=True) as subproc): | |
| monitor = _remote_debugging.GCMonitor(subproc.process.pid, debug=True) | |
| before_stats = monitor.get_gc_stats(all_interpreters=all_interpreters) | |
| for generation in generations: | |
| before = get_last_item(before_stats, generation, self._main_iid) | |
| self.assertIsNotNone(before) | |
| after_stats = before_stats | |
| for _ in range(10): | |
| time.sleep(0.5) | |
| after_stats = monitor.get_gc_stats(all_interpreters=all_interpreters) | |
| if self._gc_stats_advanced(before_stats, after_stats, generations): | |
| break | |
| else: | |
| self.fail( | |
| f"GC stats for generations {generations!r} did not " | |
| f"advance: {before_stats!r} -> {after_stats!r}" | |
| ) | |
| return before_stats, after_stats | |
| def _check_gc_stats(self, before, after): | |
| self.assertIsNotNone(before) | |
| self.assertIsNotNone(after) | |
| self.assertGreater(after.collections, before.collections, (before, after)) | |
| self.assertGreater(after.ts_start, before.ts_start, (before, after)) | |
| self.assertGreater(after.ts_stop, before.ts_stop, (before, after)) | |
| self.assertGreater(after.duration, before.duration, (before, after)) | |
| self.assertGreater(after.candidates, before.candidates, (before, after)) | |
| # may not grow | |
| self.assertGreaterEqual(after.collected, before.collected, (before, after)) | |
| self.assertGreaterEqual(after.uncollectable, before.uncollectable, (before, after)) | |
| def _check_interpreter_gc_stats(self, before_stats, after_stats): | |
| before_iids = get_interpreter_identifiers(before_stats) | |
| after_iids = get_interpreter_identifiers(after_stats) | |
| self.assertEqual(before_iids, after_iids) | |
| self.assertEqual(get_generations(before_stats), (0, 1, 2)) | |
| self.assertEqual(get_generations(after_stats), (0, 1, 2)) | |
| for iid in after_iids: | |
| with self.subTest(f"interpreter id={iid}"): | |
| before_last_items = (get_last_item(before_stats, 0, iid), | |
| get_last_item(before_stats, 1, iid), | |
| get_last_item(before_stats, 2, iid)) | |
| after_last_items = (get_last_item(after_stats, 0, iid), | |
| get_last_item(after_stats, 1, iid), | |
| get_last_item(after_stats, 2, iid)) | |
| for before, after in zip(before_last_items, after_last_items): | |
| self._check_gc_stats(before, after) | |
| def test_gc_stats_timestamps_for_main_interpreter(self): | |
| script = textwrap.dedent(self._main_interpreter_script) | |
| before_stats, after_stats = self._collect_gc_stats( | |
| script, False, generations=(0, 1, 2)) | |
| for generation in range(3): | |
| with self.subTest(generation=generation): | |
| before = get_last_item(before_stats, generation, self._main_iid) | |
| after = get_last_item(after_stats, generation, self._main_iid) | |
| self.assertIsNotNone(before) | |
| self.assertIsNotNone(after) | |
| self.assertGreater( | |
| after.collections, before.collections, | |
| (before, after)) | |
| self.assertGreater( | |
| after.ts_start, before.ts_start, | |
| (before, after)) | |
| self.assertGreater( | |
| after.ts_stop, before.ts_stop, | |
| (before, after)) | |
| @requires_gil_enabled() | |
| def test_gc_stats_for_main_interpreter(self): | |
| script = textwrap.dedent(self._script.format(False)) | |
| before_stats, after_stats = self._collect_gc_stats(script, False) | |
| self._check_interpreter_gc_stats(before_stats, after_stats) | |
| @requires_gil_enabled() | |
| def test_gc_stats_for_main_interpreter_if_subinterpreter_exists(self): | |
| script = textwrap.dedent(self._script.format(True)) | |
| before_stats, after_stats = self._collect_gc_stats(script, False) | |
| self._check_interpreter_gc_stats(before_stats, after_stats) | |
| @requires_gil_enabled() | |
| def test_gc_stats_for_all_interpreters(self): | |
| script = textwrap.dedent(self._script.format(True)) | |
| before_stats, after_stats = self._collect_gc_stats(script, True) | |
| before_iids = get_interpreter_identifiers(before_stats) | |
| after_iids = get_interpreter_identifiers(after_stats) | |
| self.assertGreater(len(before_iids), 1) | |
| self.assertGreater(len(after_iids), 1) | |
| self.assertEqual(before_iids, after_iids) | |
| self._check_interpreter_gc_stats(before_stats, after_stats) |