diff --git a/Lib/profiling/sampling/_flamegraph_assets/flamegraph.js b/Lib/profiling/sampling/_flamegraph_assets/flamegraph.js index 840acf2c27d1201..a60feb733f20d53 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() { const baseData = isShowingElided ? elidedFlamegraphData : normalData; @@ -246,12 +250,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'); @@ -313,9 +317,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; @@ -399,9 +403,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"); @@ -499,7 +503,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..abbf6210bf94529 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.