https://github.com/python/cpython/commit/ee68f5f46a14e2bbf22d72796e45e5f467951792
commit: ee68f5f46a14e2bbf22d72796e45e5f467951792
branch: main
author: Pablo Galindo Salgado <[email protected]>
committer: pablogsal <[email protected]>
date: 2026-08-11T23:16:05Z
summary:

gh-154059: Fix Tachyon flame graph time units (#154036)

files:
A Misc/NEWS.d/next/Tools-Demos/2026-07-19-00-00-00.gh-issue-154059.JiBcOM.rst
M Lib/profiling/sampling/_flamegraph_assets/flamegraph.js
M Lib/profiling/sampling/stack_collector.py
M Lib/test/test_profiling/test_sampling_profiler/test_collectors.py

diff --git a/Lib/profiling/sampling/_flamegraph_assets/flamegraph.js 
b/Lib/profiling/sampling/_flamegraph_assets/flamegraph.js
index f1cdf5142fa3949..e055801a405e36f 100644
--- a/Lib/profiling/sampling/_flamegraph_assets/flamegraph.js
+++ b/Lib/profiling/sampling/_flamegraph_assets/flamegraph.js
@@ -99,6 +99,10 @@ function getDisplayName(moduleName, filename) {
   return filename;
 }
 
+function samplesToMilliseconds(samples, data) {
+  return (samples * data.stats.sample_interval_usec / 1000).toFixed(2);
+}
+
 function selectFlamegraphData(selectedThreadId = null) {
   let baseData = isShowingElided ? elidedFlamegraphData : normalData;
 
@@ -255,12 +259,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);
   const percent = rootValue > 0 ? ((nodeData.value / rootValue) * 
100).toFixed(1) : "0.0";
 
   const brandEl = document.getElementById('status-brand');
@@ -322,9 +326,9 @@ function createPythonTooltip(data) {
         .style("opacity", 0);
     }
 
-    const timeMs = (d.data.value / 1000).toFixed(2);
+    const timeMs = samplesToMilliseconds(d.data.value, data);
     const selfSamples = d.data.self || 0;
-    const selfMs = (selfSamples / 1000).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;
@@ -408,9 +412,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);
+      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");
@@ -508,7 +512,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 ace0e1a12290131..796a900e084676f 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": {},
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 1aba1572cc89d38..c27ad6663df1c8a 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 * data.stats.sample_interval_usec / 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-154059.JiBcOM.rst 
b/Misc/NEWS.d/next/Tools-Demos/2026-07-19-00-00-00.gh-issue-154059.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-154059.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.

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to