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]