From d421d76e39e8e4d48ccd9c6863f76124577a041b Mon Sep 17 00:00:00 2001 From: Pablo Galindo Salgado Date: Sun, 19 Jul 2026 11:22:45 +0100 Subject: [PATCH 1/2] gh-154088: Count elided stacks consistently --- Lib/profiling/sampling/stack_collector.py | 13 ++-- .../test_sampling_profiler/test_collectors.py | 60 ++++++++++++++++++- ...-07-19-11-30-00.gh-issue-154088.pC7nRa.rst | 2 + 3 files changed, 70 insertions(+), 5 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-07-19-11-30-00.gh-issue-154088.pC7nRa.rst diff --git a/Lib/profiling/sampling/stack_collector.py b/Lib/profiling/sampling/stack_collector.py index eb1a3fba93cf33b..43407788fd5adc2 100644 --- a/Lib/profiling/sampling/stack_collector.py +++ b/Lib/profiling/sampling/stack_collector.py @@ -206,7 +206,7 @@ def _get_module_name(self, filename, path_info): self._module_cache[filename] = module_name return module_name - def _convert_to_flamegraph_format(self): + def _convert_to_flamegraph_format(self, *, min_samples=None): if self._total_samples == 0: return { "name": self._string_table.intern("No Data"), @@ -268,7 +268,8 @@ def convert_children(children, min_samples, path_info): # Filter out very small functions (less than 0.1% of total samples) total_samples = self._total_samples - min_samples = max(1, int(total_samples * 0.001)) + if min_samples is None: + min_samples = max(1, int(total_samples * 0.001)) path_info = get_python_path_info() root_children = convert_children(self._root["children"], min_samples, path_info) @@ -622,7 +623,9 @@ def _add_elided_flamegraph(self, current_flamegraph, current_stats, baseline_sta """Calculate elided paths and add elided flamegraph to stats.""" self._elided_paths = baseline_stats.keys() - current_stats.keys() - current_flamegraph["stats"]["elided_count"] = len(self._elided_paths) + parent_paths = {path[:-1] for path in self._elided_paths} + elided_stacks = self._elided_paths - parent_paths + current_flamegraph["stats"]["elided_count"] = len(elided_stacks) if self._elided_paths: elided_flamegraph = self._build_elided_flamegraph(baseline_stats, scale) @@ -645,7 +648,9 @@ def _build_elided_flamegraph(self, baseline_stats, scale): orig_get_source = self._baseline_collector._get_source_lines self._baseline_collector._get_source_lines = lambda func: None try: - baseline_data = self._baseline_collector._convert_to_flamegraph_format() + baseline_data = self._baseline_collector._convert_to_flamegraph_format( + min_samples=1 + ) finally: self._baseline_collector._get_source_lines = orig_get_source 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 7746811014a9e2f..eaf9ec37223d509 100644 --- a/Lib/test/test_profiling/test_sampling_profiler/test_collectors.py +++ b/Lib/test/test_profiling/test_sampling_profiler/test_collectors.py @@ -1581,7 +1581,7 @@ def test_diff_flamegraph_elided_stacks(self): data = diff._convert_to_flamegraph_format() - self.assertGreater(data["stats"]["elided_count"], 0) + self.assertEqual(data["stats"]["elided_count"], 1) self.assertIn("elided_flamegraph", data["stats"]) elided = data["stats"]["elided_flamegraph"] self.assertTrue(elided["stats"]["is_differential"]) @@ -1597,6 +1597,64 @@ def test_diff_flamegraph_elided_stacks(self): self.assertGreater(child["baseline"], 0) self.assertAlmostEqual(child["diff"], -child["baseline"]) + def test_diff_flamegraph_counts_elided_stacks_not_paths(self): + """Nested missing paths count as one elided stack.""" + baseline_frames = [ + MockInterpreterInfo(0, [ + MockThreadInfo(1, [ + MockFrameInfo("file.py", 30, "old_leaf"), + MockFrameInfo("file.py", 20, "old_mid"), + MockFrameInfo("file.py", 10, "root"), + ]) + ]) + ] + current_frames = [ + MockInterpreterInfo(0, [ + MockThreadInfo(1, [MockFrameInfo("file.py", 10, "root")]) + ]) + ] + + diff = make_diff_collector_with_mock_baseline([baseline_frames]) + diff.collect(current_frames) + + data = diff._convert_to_flamegraph_format() + self.assertEqual(data["stats"]["elided_count"], 1) + + def test_diff_flamegraph_renders_small_elided_stack(self): + """Elided stacks are not removed by the significance filter.""" + common_frames = [ + MockInterpreterInfo(0, [ + MockThreadInfo(1, [ + MockFrameInfo("file.py", 20, "common"), + MockFrameInfo("file.py", 10, "root"), + ]) + ]) + ] + old_frames = [ + MockInterpreterInfo(0, [ + MockThreadInfo(1, [ + MockFrameInfo("file.py", 30, "old_tiny"), + MockFrameInfo("file.py", 10, "root"), + ]) + ]) + ] + + diff = make_diff_collector_with_mock_baseline( + [common_frames] * 1999 + [old_frames] + ) + for _ in range(1999): + diff.collect(common_frames) + + data = diff._convert_to_flamegraph_format() + self.assertEqual(data["stats"]["elided_count"], 1) + self.assertIn("elided_flamegraph", data["stats"]) + + elided = data["stats"]["elided_flamegraph"] + strings = elided["strings"] + self.assertIsNotNone( + find_child_by_name(elided.get("children", []), strings, "old_tiny") + ) + def test_diff_flamegraph_elided_top_level_root(self): """Elided top-level roots do not crash metadata generation.""" baseline_frames_1 = [ diff --git a/Misc/NEWS.d/next/Library/2026-07-19-11-30-00.gh-issue-154088.pC7nRa.rst b/Misc/NEWS.d/next/Library/2026-07-19-11-30-00.gh-issue-154088.pC7nRa.rst new file mode 100644 index 000000000000000..b9747589a2f1c8e --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-19-11-30-00.gh-issue-154088.pC7nRa.rst @@ -0,0 +1,2 @@ +Make Tachyon count and render elided stacks consistently in differential +flamegraphs. From 22f1036376e8a0cad65cbf4e7121a87fa93357c9 Mon Sep 17 00:00:00 2001 From: Pablo Galindo Salgado Date: Sun, 19 Jul 2026 12:35:42 +0100 Subject: [PATCH 2/2] gh-154088: Count internal elided stack endings --- Lib/profiling/sampling/stack_collector.py | 10 ++++++++-- .../test_sampling_profiler/test_collectors.py | 18 ++++++++++++++---- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/Lib/profiling/sampling/stack_collector.py b/Lib/profiling/sampling/stack_collector.py index 43407788fd5adc2..c884df41a1e0aab 100644 --- a/Lib/profiling/sampling/stack_collector.py +++ b/Lib/profiling/sampling/stack_collector.py @@ -623,8 +623,14 @@ def _add_elided_flamegraph(self, current_flamegraph, current_stats, baseline_sta """Calculate elided paths and add elided flamegraph to stats.""" self._elided_paths = baseline_stats.keys() - current_stats.keys() - parent_paths = {path[:-1] for path in self._elided_paths} - elided_stacks = self._elided_paths - parent_paths + # A sampled stack can end at an internal path that also has elided + # descendants. Count every disappeared path with self samples, not + # just the leaves of the elided path tree. + elided_stacks = { + path + for path in self._elided_paths + if baseline_stats[path]["self"] > 0 + } current_flamegraph["stats"]["elided_count"] = len(elided_stacks) if self._elided_paths: 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 eaf9ec37223d509..84011084dc81949 100644 --- a/Lib/test/test_profiling/test_sampling_profiler/test_collectors.py +++ b/Lib/test/test_profiling/test_sampling_profiler/test_collectors.py @@ -1598,8 +1598,16 @@ def test_diff_flamegraph_elided_stacks(self): self.assertAlmostEqual(child["diff"], -child["baseline"]) def test_diff_flamegraph_counts_elided_stacks_not_paths(self): - """Nested missing paths count as one elided stack.""" - baseline_frames = [ + """Internal and leaf stack endings are counted separately.""" + internal_stack = [ + MockInterpreterInfo(0, [ + MockThreadInfo(1, [ + MockFrameInfo("file.py", 20, "old_mid"), + MockFrameInfo("file.py", 10, "root"), + ]) + ]) + ] + leaf_stack = [ MockInterpreterInfo(0, [ MockThreadInfo(1, [ MockFrameInfo("file.py", 30, "old_leaf"), @@ -1614,11 +1622,13 @@ def test_diff_flamegraph_counts_elided_stacks_not_paths(self): ]) ] - diff = make_diff_collector_with_mock_baseline([baseline_frames]) + diff = make_diff_collector_with_mock_baseline( + [internal_stack, leaf_stack] + ) diff.collect(current_frames) data = diff._convert_to_flamegraph_format() - self.assertEqual(data["stats"]["elided_count"], 1) + self.assertEqual(data["stats"]["elided_count"], 2) def test_diff_flamegraph_renders_small_elided_stack(self): """Elided stacks are not removed by the significance filter."""