From c3d1138658329653cd40819d10e501a19b17ed46 Mon Sep 17 00:00:00 2001 From: MAKWANA AKSHAT Date: Sat, 31 Oct 2026 00:03:28 +0530 Subject: [PATCH 1/3] gh-160976: Preserve source file path in Gecko sampling collector --- Lib/profiling/sampling/gecko_collector.py | 2 +- .../test_sampling_profiler/test_collectors.py | 85 +++++++++++++++++++ ...-10-30-23-05-00.gh-issue-160976.GeCk0P.rst | 2 + 3 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2026-10-30-23-05-00.gh-issue-160976.GeCk0P.rst diff --git a/Lib/profiling/sampling/gecko_collector.py b/Lib/profiling/sampling/gecko_collector.py index 2de8cce387e7f2..bc6ce6187b8846 100644 --- a/Lib/profiling/sampling/gecko_collector.py +++ b/Lib/profiling/sampling/gecko_collector.py @@ -617,7 +617,7 @@ def _get_or_create_func(self, thread_data, filename, funcname, lineno): func_resources.append(resource_idx) if is_python: - filename_idx = self._intern_string(os.path.basename(filename)) + filename_idx = self._intern_string(filename) func_filenames.append(filename_idx) func_line_numbers.append(lineno) else: diff --git a/Lib/test/test_profiling/test_sampling_profiler/test_collectors.py b/Lib/test/test_profiling/test_sampling_profiler/test_collectors.py index d4ba2df5044559..5ae4e49d54f4e7 100644 --- a/Lib/test/test_profiling/test_sampling_profiler/test_collectors.py +++ b/Lib/test/test_profiling/test_sampling_profiler/test_collectors.py @@ -741,6 +741,91 @@ def test_gecko_collector_basic(self): self.assertGreater(stack_table["length"], 0) self.assertGreater(len(stack_table["frame"]), 0) + def test_gecko_collector_preserves_source_file_paths(self): + """Test that GeckoCollector preserves source file paths and distinguishes + files with the same basename in different directories.""" + collector = GeckoCollector(1000) + file_a = os.path.join("pkg_a", "__init__.py") + file_b = os.path.join("pkg_b", "__init__.py") + + test_frames = [ + MockInterpreterInfo( + 0, + [ + MockThreadInfo( + 1, + [ + MockFrameInfo(file_a, 10, "func_a"), + MockFrameInfo(file_b, 20, "func_b"), + ], + status=THREAD_STATUS_MAIN_THREAD, + ) + ], + ) + ] + collector.collect(test_frames) + profile_data = collector._build_profile() + + string_array = profile_data["shared"]["stringArray"] + thread_data = profile_data["threads"][0] + func_table = thread_data["funcTable"] + + self.assertEqual(func_table["length"], 2) + self.assertEqual(len(func_table["name"]), 2) + self.assertEqual(len(func_table["fileName"]), 2) + self.assertEqual(len(func_table["lineNumber"]), 2) + + # Strictly validate that string table contains both full paths + self.assertIn(file_a, string_array) + self.assertIn(file_b, string_array) + expected_idx_a = string_array.index(file_a) + expected_idx_b = string_array.index(file_b) + self.assertNotEqual( + expected_idx_a, + expected_idx_b, + "Distinct file paths must have distinct string table indices", + ) + + # Build mapping of func_name -> (file_idx, file_name, line_no) + funcs_by_name = {} + for i in range(func_table["length"]): + name_idx = func_table["name"][i] + self.assertIsInstance(name_idx, int) + self.assertGreaterEqual(name_idx, 0) + self.assertLess(name_idx, len(string_array)) + func_name = string_array[name_idx] + + file_idx = func_table["fileName"][i] + self.assertIsInstance(file_idx, int) + self.assertGreaterEqual(file_idx, 0) + self.assertLess(file_idx, len(string_array)) + file_name = string_array[file_idx] + + line_no = func_table["lineNumber"][i] + self.assertIsInstance(line_no, int) + + funcs_by_name[func_name] = (file_idx, file_name, line_no) + + self.assertEqual(set(funcs_by_name.keys()), {"func_a", "func_b"}) + + # Strictly validate func_a filename index, path string, and line + file_idx_a, resolved_file_a, line_a = funcs_by_name["func_a"] + self.assertEqual(file_idx_a, expected_idx_a) + self.assertEqual(resolved_file_a, file_a) + self.assertEqual(line_a, 10) + + # Strictly validate func_b filename index, path string, and line + file_idx_b, resolved_file_b, line_b = funcs_by_name["func_b"] + self.assertEqual(file_idx_b, expected_idx_b) + self.assertEqual(resolved_file_b, file_b) + self.assertEqual(line_b, 20) + + # Ensure the filename indices in funcTable are distinct + self.assertNotEqual(file_idx_a, file_idx_b) + + # Ensure the basename is not interned in place of the path + self.assertNotIn("__init__.py", string_array) + def test_gecko_collector_async_aware(self): collector = GeckoCollector(1000) diff --git a/Misc/NEWS.d/next/Library/2026-10-30-23-05-00.gh-issue-160976.GeCk0P.rst b/Misc/NEWS.d/next/Library/2026-10-30-23-05-00.gh-issue-160976.GeCk0P.rst new file mode 100644 index 00000000000000..903f84e732ac19 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-30-23-05-00.gh-issue-160976.GeCk0P.rst @@ -0,0 +1,2 @@ +Fixed an issue in :mod:`profiling.sampling` where the Gecko exporter (``--gecko``) +only recorded source file basenames instead of preserving the source file path. From 90c2f071330938cbc0c27a2914ef5d0e296f9e93 Mon Sep 17 00:00:00 2001 From: MAKWANA AKSHAT Date: Sat, 31 Oct 2026 01:24:54 +0530 Subject: [PATCH 2/3] gh-160976: Relax global string table check in Gecko collector test --- .../test_sampling_profiler/test_collectors.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_profiling/test_sampling_profiler/test_collectors.py b/Lib/test/test_profiling/test_sampling_profiler/test_collectors.py index 5ae4e49d54f4e7..334177583b48f5 100644 --- a/Lib/test/test_profiling/test_sampling_profiler/test_collectors.py +++ b/Lib/test/test_profiling/test_sampling_profiler/test_collectors.py @@ -820,11 +820,11 @@ def test_gecko_collector_preserves_source_file_paths(self): self.assertEqual(resolved_file_b, file_b) self.assertEqual(line_b, 20) - # Ensure the filename indices in funcTable are distinct + # Ensure the filename indices and resolved paths in funcTable are distinct self.assertNotEqual(file_idx_a, file_idx_b) - - # Ensure the basename is not interned in place of the path - self.assertNotIn("__init__.py", string_array) + self.assertNotEqual(resolved_file_a, resolved_file_b) + self.assertNotEqual(resolved_file_a, os.path.basename(file_a)) + self.assertNotEqual(resolved_file_b, os.path.basename(file_b)) def test_gecko_collector_async_aware(self): collector = GeckoCollector(1000) From 806d351aff6834bdc5031549bfe14dd47fc12370 Mon Sep 17 00:00:00 2001 From: MAKWANA AKSHAT Date: Mon, 2 Nov 2026 01:11:45 +0530 Subject: [PATCH 3/3] gh-160976: Simplify Gecko source path regression test --- .../test_sampling_profiler/test_collectors.py | 66 ++++--------------- ...-10-30-23-05-00.gh-issue-160976.GeCk0P.rst | 3 +- 2 files changed, 14 insertions(+), 55 deletions(-) diff --git a/Lib/test/test_profiling/test_sampling_profiler/test_collectors.py b/Lib/test/test_profiling/test_sampling_profiler/test_collectors.py index 334177583b48f5..b6c435b58ecfa8 100644 --- a/Lib/test/test_profiling/test_sampling_profiler/test_collectors.py +++ b/Lib/test/test_profiling/test_sampling_profiler/test_collectors.py @@ -767,64 +767,24 @@ def test_gecko_collector_preserves_source_file_paths(self): profile_data = collector._build_profile() string_array = profile_data["shared"]["stringArray"] - thread_data = profile_data["threads"][0] - func_table = thread_data["funcTable"] + func_table = profile_data["threads"][0]["funcTable"] self.assertEqual(func_table["length"], 2) - self.assertEqual(len(func_table["name"]), 2) - self.assertEqual(len(func_table["fileName"]), 2) - self.assertEqual(len(func_table["lineNumber"]), 2) - - # Strictly validate that string table contains both full paths - self.assertIn(file_a, string_array) - self.assertIn(file_b, string_array) - expected_idx_a = string_array.index(file_a) - expected_idx_b = string_array.index(file_b) - self.assertNotEqual( - expected_idx_a, - expected_idx_b, - "Distinct file paths must have distinct string table indices", - ) - - # Build mapping of func_name -> (file_idx, file_name, line_no) - funcs_by_name = {} - for i in range(func_table["length"]): - name_idx = func_table["name"][i] - self.assertIsInstance(name_idx, int) - self.assertGreaterEqual(name_idx, 0) - self.assertLess(name_idx, len(string_array)) - func_name = string_array[name_idx] - - file_idx = func_table["fileName"][i] - self.assertIsInstance(file_idx, int) - self.assertGreaterEqual(file_idx, 0) - self.assertLess(file_idx, len(string_array)) - file_name = string_array[file_idx] - - line_no = func_table["lineNumber"][i] - self.assertIsInstance(line_no, int) - - funcs_by_name[func_name] = (file_idx, file_name, line_no) - - self.assertEqual(set(funcs_by_name.keys()), {"func_a", "func_b"}) - - # Strictly validate func_a filename index, path string, and line - file_idx_a, resolved_file_a, line_a = funcs_by_name["func_a"] - self.assertEqual(file_idx_a, expected_idx_a) - self.assertEqual(resolved_file_a, file_a) - self.assertEqual(line_a, 10) - # Strictly validate func_b filename index, path string, and line - file_idx_b, resolved_file_b, line_b = funcs_by_name["func_b"] - self.assertEqual(file_idx_b, expected_idx_b) - self.assertEqual(resolved_file_b, file_b) - self.assertEqual(line_b, 20) + funcs_by_name = { + string_array[func_table["name"][i]]: ( + func_table["fileName"][i], + string_array[func_table["fileName"][i]], + ) + for i in range(func_table["length"]) + } + + file_idx_a, resolved_file_a = funcs_by_name["func_a"] + file_idx_b, resolved_file_b = funcs_by_name["func_b"] - # Ensure the filename indices and resolved paths in funcTable are distinct + self.assertEqual(resolved_file_a, file_a) + self.assertEqual(resolved_file_b, file_b) self.assertNotEqual(file_idx_a, file_idx_b) - self.assertNotEqual(resolved_file_a, resolved_file_b) - self.assertNotEqual(resolved_file_a, os.path.basename(file_a)) - self.assertNotEqual(resolved_file_b, os.path.basename(file_b)) def test_gecko_collector_async_aware(self): collector = GeckoCollector(1000) diff --git a/Misc/NEWS.d/next/Library/2026-10-30-23-05-00.gh-issue-160976.GeCk0P.rst b/Misc/NEWS.d/next/Library/2026-10-30-23-05-00.gh-issue-160976.GeCk0P.rst index 903f84e732ac19..1da8a1f97e6f98 100644 --- a/Misc/NEWS.d/next/Library/2026-10-30-23-05-00.gh-issue-160976.GeCk0P.rst +++ b/Misc/NEWS.d/next/Library/2026-10-30-23-05-00.gh-issue-160976.GeCk0P.rst @@ -1,2 +1 @@ -Fixed an issue in :mod:`profiling.sampling` where the Gecko exporter (``--gecko``) -only recorded source file basenames instead of preserving the source file path. +The Gecko reporter (``--gecko``) in ``profiling.sampling`` now records full source file paths instead of only basenames.