From 6c1486b8651f833a69adb56291e8e8c454c77b25 Mon Sep 17 00:00:00 2001 From: Pablo Galindo Salgado Date: Sun, 19 Jul 2026 00:25:58 +0100 Subject: [PATCH 1/5] gh-154033: Fix Tachyon flame graph time units --- .../sampling/_flamegraph_assets/flamegraph.js | 21 ++++++++++++------- Lib/profiling/sampling/stack_collector.py | 2 +- .../test_sampling_profiler/test_collectors.py | 5 ++++- ...-07-19-00-00-00.gh-issue-154033.JiBcOM.rst | 2 ++ 4 files changed, 20 insertions(+), 10 deletions(-) create mode 100644 Misc/NEWS.d/next/Tools-Demos/2026-07-19-00-00-00.gh-issue-154033.JiBcOM.rst diff --git a/Lib/profiling/sampling/_flamegraph_assets/flamegraph.js b/Lib/profiling/sampling/_flamegraph_assets/flamegraph.js index 840acf2c27d1201..67d8290e52c0bd3 100644 --- a/Lib/profiling/sampling/_flamegraph_assets/flamegraph.js +++ b/Lib/profiling/sampling/_flamegraph_assets/flamegraph.js @@ -99,6 +99,11 @@ function getDisplayName(moduleName, filename) { return filename; } +function samplesToMilliseconds(samples, data) { + const sampleIntervalUsec = data.stats.sample_interval_usec; + return samples * sampleIntervalUsec / 1000; +} + function selectFlamegraphData() { const baseData = isShowingElided ? elidedFlamegraphData : normalData; @@ -246,12 +251,12 @@ function setupLogos() { // Status Bar // ============================================================================ -function updateStatusBar(nodeData, rootValue) { +function updateStatusBar(nodeData, rootValue, data) { const funcname = resolveString(nodeData.funcname) || resolveString(nodeData.name) || "--"; const filename = resolveString(nodeData.filename) || ""; const moduleName = resolveString(nodeData.module) || ""; const lineno = nodeData.lineno; - const timeMs = (nodeData.value / 1000).toFixed(2); + const timeMs = samplesToMilliseconds(nodeData.value, data).toFixed(2); const percent = rootValue > 0 ? ((nodeData.value / rootValue) * 100).toFixed(1) : "0.0"; const brandEl = document.getElementById('status-brand'); @@ -313,9 +318,9 @@ function createPythonTooltip(data) { .style("opacity", 0); } - const timeMs = (d.data.value / 1000).toFixed(2); + const timeMs = samplesToMilliseconds(d.data.value, data).toFixed(2); const selfSamples = d.data.self || 0; - const selfMs = (selfSamples / 1000).toFixed(2); + const selfMs = samplesToMilliseconds(selfSamples, data).toFixed(2); const percentage = ((d.data.value / data.value) * 100).toFixed(2); const relativePercentage = Math.min(100, ((d.data.value / (zoomedNodeValue ?? data.value)) * 100)).toFixed(2); const calls = d.data.calls || 0; @@ -399,9 +404,9 @@ function createPythonTooltip(data) { // Differential stats section let diffSection = ""; if (d.data.diff !== undefined && d.data.baseline !== undefined) { - const baselineSelf = (d.data.baseline / 1000).toFixed(2); - const currentSelf = ((d.data.self_time || 0) / 1000).toFixed(2); - const diffMs = (d.data.diff / 1000).toFixed(2); + const baselineSelf = samplesToMilliseconds(d.data.baseline, data).toFixed(2); + const currentSelf = samplesToMilliseconds(d.data.self_time || 0, data).toFixed(2); + const diffMs = samplesToMilliseconds(d.data.diff, data).toFixed(2); const diffPct = d.data.diff_pct; const sign = d.data.diff >= 0 ? "+" : ""; const diffClass = d.data.diff > 0 ? "regression" : (d.data.diff < 0 ? "improvement" : "neutral"); @@ -499,7 +504,7 @@ function createPythonTooltip(data) { .style("opacity", 1); // Update status bar - updateStatusBar(d.data, data.value); + updateStatusBar(d.data, data.value, data); }; pythonTooltip.hide = function () { diff --git a/Lib/profiling/sampling/stack_collector.py b/Lib/profiling/sampling/stack_collector.py index eb1a3fba93cf33b..b13b7792cac8a1e 100644 --- a/Lib/profiling/sampling/stack_collector.py +++ b/Lib/profiling/sampling/stack_collector.py @@ -70,7 +70,7 @@ def export(self, filename): class FlamegraphCollector(StackTraceCollector): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.stats = {} + self.stats = {"sample_interval_usec": self.sample_interval_usec} self._root = {"samples": 0, "children": {}, "threads": set()} self._total_samples = 0 self._sample_count = 0 # Track actual number of samples (not thread traces) 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..d66f8e029830a02 100644 --- a/Lib/test/test_profiling/test_sampling_profiler/test_collectors.py +++ b/Lib/test/test_profiling/test_sampling_profiler/test_collectors.py @@ -505,6 +505,7 @@ def test_flamegraph_collector_basic(self): self.assertIn("func1 (file.py:10)", resolve_name(child, strings)) self.assertEqual(child["value"], 1) self.assertEqual(child["self"], 1) # leaf: all time is self + self.assertEqual(data["stats"]["sample_interval_usec"], 1000) def test_flamegraph_collector_export(self): """Test flamegraph HTML export functionality.""" @@ -513,7 +514,7 @@ def test_flamegraph_collector_export(self): ) self.addCleanup(close_and_unlink, flamegraph_out) - collector = FlamegraphCollector(1000) + collector = FlamegraphCollector(10000) # Create some test data (use Interpreter/Thread objects like runtime) test_frames1 = [ @@ -569,6 +570,8 @@ def test_flamegraph_collector_export(self): self.assertIn('"name":', content) self.assertIn('"value":', content) self.assertIn('"children":', content) + self.assertIn('"sample_interval_usec": 10000', content) + self.assertIn("samples * sampleIntervalUsec / 1000", content) def test_flamegraph_collector_empty_export_fails(self): """Test empty flamegraph export reports no output.""" diff --git a/Misc/NEWS.d/next/Tools-Demos/2026-07-19-00-00-00.gh-issue-154033.JiBcOM.rst b/Misc/NEWS.d/next/Tools-Demos/2026-07-19-00-00-00.gh-issue-154033.JiBcOM.rst new file mode 100644 index 000000000000000..92e047067e97bdf --- /dev/null +++ b/Misc/NEWS.d/next/Tools-Demos/2026-07-19-00-00-00.gh-issue-154033.JiBcOM.rst @@ -0,0 +1,2 @@ +Fix the time units in Tachyon flame graph tooltips by accounting for the +sampling interval when converting samples to milliseconds. From 2c4a7adc93f422d52cb87eefb6c6a474546e1a7b Mon Sep 17 00:00:00 2001 From: Pablo Galindo Salgado Date: Sun, 19 Jul 2026 11:55:48 +0100 Subject: [PATCH 2/5] gh-154059: Link flamegraph time unit fix --- ....JiBcOM.rst => 2026-07-19-00-00-00.gh-issue-154059.JiBcOM.rst} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Misc/NEWS.d/next/Tools-Demos/{2026-07-19-00-00-00.gh-issue-154033.JiBcOM.rst => 2026-07-19-00-00-00.gh-issue-154059.JiBcOM.rst} (100%) diff --git a/Misc/NEWS.d/next/Tools-Demos/2026-07-19-00-00-00.gh-issue-154033.JiBcOM.rst b/Misc/NEWS.d/next/Tools-Demos/2026-07-19-00-00-00.gh-issue-154059.JiBcOM.rst similarity index 100% rename from Misc/NEWS.d/next/Tools-Demos/2026-07-19-00-00-00.gh-issue-154033.JiBcOM.rst rename to Misc/NEWS.d/next/Tools-Demos/2026-07-19-00-00-00.gh-issue-154059.JiBcOM.rst From 376392b52cdefe3c5d6afaa83d4f02811fabb987 Mon Sep 17 00:00:00 2001 From: Pablo Galindo Salgado Date: Mon, 20 Jul 2026 23:29:46 +0100 Subject: [PATCH 3/5] Update Lib/profiling/sampling/_flamegraph_assets/flamegraph.js MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: László Kiss Kollár --- Lib/profiling/sampling/_flamegraph_assets/flamegraph.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Lib/profiling/sampling/_flamegraph_assets/flamegraph.js b/Lib/profiling/sampling/_flamegraph_assets/flamegraph.js index 67d8290e52c0bd3..12836e78dab6184 100644 --- a/Lib/profiling/sampling/_flamegraph_assets/flamegraph.js +++ b/Lib/profiling/sampling/_flamegraph_assets/flamegraph.js @@ -100,8 +100,7 @@ function getDisplayName(moduleName, filename) { } function samplesToMilliseconds(samples, data) { - const sampleIntervalUsec = data.stats.sample_interval_usec; - return samples * sampleIntervalUsec / 1000; + return (samples * data.stats.sample_interval_usec / 1000).toFixed(2); } function selectFlamegraphData() { From cd8f412e4f579df3cf0c4c8d0787b462837726af Mon Sep 17 00:00:00 2001 From: Ivona Stojanovic Date: Tue, 21 Jul 2026 16:43:49 +0200 Subject: [PATCH 4/5] Fix samplesToMilliseconds --- .../sampling/_flamegraph_assets/flamegraph.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Lib/profiling/sampling/_flamegraph_assets/flamegraph.js b/Lib/profiling/sampling/_flamegraph_assets/flamegraph.js index 12836e78dab6184..a60feb733f20d53 100644 --- a/Lib/profiling/sampling/_flamegraph_assets/flamegraph.js +++ b/Lib/profiling/sampling/_flamegraph_assets/flamegraph.js @@ -255,7 +255,7 @@ function updateStatusBar(nodeData, rootValue, data) { const filename = resolveString(nodeData.filename) || ""; const moduleName = resolveString(nodeData.module) || ""; const lineno = nodeData.lineno; - const timeMs = samplesToMilliseconds(nodeData.value, data).toFixed(2); + const timeMs = samplesToMilliseconds(nodeData.value, data); const percent = rootValue > 0 ? ((nodeData.value / rootValue) * 100).toFixed(1) : "0.0"; const brandEl = document.getElementById('status-brand'); @@ -317,9 +317,9 @@ function createPythonTooltip(data) { .style("opacity", 0); } - const timeMs = samplesToMilliseconds(d.data.value, data).toFixed(2); + const timeMs = samplesToMilliseconds(d.data.value, data); const selfSamples = d.data.self || 0; - const selfMs = samplesToMilliseconds(selfSamples, data).toFixed(2); + const selfMs = samplesToMilliseconds(selfSamples, data); const percentage = ((d.data.value / data.value) * 100).toFixed(2); const relativePercentage = Math.min(100, ((d.data.value / (zoomedNodeValue ?? data.value)) * 100)).toFixed(2); const calls = d.data.calls || 0; @@ -403,9 +403,9 @@ function createPythonTooltip(data) { // Differential stats section let diffSection = ""; if (d.data.diff !== undefined && d.data.baseline !== undefined) { - const baselineSelf = samplesToMilliseconds(d.data.baseline, data).toFixed(2); - const currentSelf = samplesToMilliseconds(d.data.self_time || 0, data).toFixed(2); - const diffMs = samplesToMilliseconds(d.data.diff, data).toFixed(2); + const baselineSelf = samplesToMilliseconds(d.data.baseline, data); + const currentSelf = samplesToMilliseconds(d.data.self_time || 0, data); + const diffMs = samplesToMilliseconds(d.data.diff, data); const diffPct = d.data.diff_pct; const sign = d.data.diff >= 0 ? "+" : ""; const diffClass = d.data.diff > 0 ? "regression" : (d.data.diff < 0 ? "improvement" : "neutral"); From 36ab011995299b1e37cb6ec51d2feb2398ac8780 Mon Sep 17 00:00:00 2001 From: Ivona Stojanovic Date: Tue, 21 Jul 2026 18:02:49 +0200 Subject: [PATCH 5/5] Fix tests --- .../test_profiling/test_sampling_profiler/test_collectors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 d66f8e029830a02..abbf6210bf94529 100644 --- a/Lib/test/test_profiling/test_sampling_profiler/test_collectors.py +++ b/Lib/test/test_profiling/test_sampling_profiler/test_collectors.py @@ -571,7 +571,7 @@ def test_flamegraph_collector_export(self): self.assertIn('"value":', content) self.assertIn('"children":', content) self.assertIn('"sample_interval_usec": 10000', content) - self.assertIn("samples * sampleIntervalUsec / 1000", content) + self.assertIn("samples * data.stats.sample_interval_usec / 1000", content) def test_flamegraph_collector_empty_export_fails(self): """Test empty flamegraph export reports no output."""