Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 15 additions & 6 deletions Lib/profiling/sampling/_flamegraph_assets/flamegraph.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,22 @@ const EMBEDDED_DATA = {{FLAMEGRAPH_DATA}};

// Global string table for resolving string indices
let stringTable = [];
// Duration of a single sample, in microseconds; node values are sample counts.
let sampleIntervalUsec = EMBEDDED_DATA?.stats?.sample_interval_usec;
let normalData = null;
let invertedData = null;
let currentThreadFilter = 'all';
let isInverted = false;
let useModuleNames = true;
let zoomedNodeValue = null;

// Convert a sample count into wall-clock milliseconds. Returns "--" when the
// sampling interval is unknown, since a sample count is not a time.
function formatSampleMs(samples) {
if (sampleIntervalUsec <= 0) return "--";
return ((samples * sampleIntervalUsec) / 1_000).toFixed(2);
}

// Heat colors are now defined in CSS variables (--heat-1 through --heat-8)
// and automatically switch with theme changes - no JS color arrays needed!

Expand Down Expand Up @@ -260,7 +269,7 @@ function updateStatusBar(nodeData, rootValue) {
const filename = resolveString(nodeData.filename) || "";
const moduleName = resolveString(nodeData.module) || "";
const lineno = nodeData.lineno;
const timeMs = (nodeData.value / 1000).toFixed(2);
const timeMs = formatSampleMs(nodeData.value);
const percent = rootValue > 0 ? ((nodeData.value / rootValue) * 100).toFixed(1) : "0.0";

const brandEl = document.getElementById('status-brand');
Expand Down Expand Up @@ -322,9 +331,9 @@ function createPythonTooltip(data) {
.style("opacity", 0);
}

const timeMs = (d.data.value / 1000).toFixed(2);
const timeMs = formatSampleMs(d.data.value);
const selfSamples = d.data.self || 0;
const selfMs = (selfSamples / 1000).toFixed(2);
const selfMs = formatSampleMs(selfSamples);
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;
Expand Down Expand Up @@ -408,9 +417,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 = formatSampleMs(d.data.baseline);
const currentSelf = formatSampleMs(d.data.self_time || 0);
const diffMs = formatSampleMs(d.data.diff);
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");
Expand Down
2 changes: 2 additions & 0 deletions Lib/profiling/sampling/stack_collector.py
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,7 @@ def convert_children(children, min_samples, path_info):
old_label = self._string_table.get_string(main_child["label"])
main_child["label"] = self._string_table.intern(f"Program Root: {old_label}")
main_child["stats"] = {
"sample_interval_usec": self.sample_interval_usec,
**self.stats,
"thread_stats": thread_stats,
"per_thread_stats": per_thread_stats_with_pct
Expand All @@ -387,6 +388,7 @@ def convert_children(children, min_samples, path_info):
"value": total_samples,
"children": root_children,
"stats": {
"sample_interval_usec": self.sample_interval_usec,
**self.stats,
"thread_stats": thread_stats,
"per_thread_stats": per_thread_stats_with_pct
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1336,6 +1336,31 @@ def test_flamegraph_collector_json_structure_includes_stats(self):
self.assertIn("gc_pct", thread_data)
self.assertIn("total", thread_data)

def test_flamegraph_stats_always_include_sample_interval(self):
"""The sampling interval is needed to turn sample counts into times."""
collector = FlamegraphCollector(sample_interval_usec=250)

stack_frames = [
MockInterpreterInfo(
0,
[MockThreadInfo(1, [MockFrameInfo("a.py", 1, "func_a")])],
)
]
collector.collect(stack_frames)

# No set_stats() call: the interval must still be reported.
data = collector._convert_to_flamegraph_format()
self.assertEqual(data["stats"]["sample_interval_usec"], 250)

collector.set_stats(
sample_interval_usec=250,
duration_sec=1.0,
sample_rate=4000.0,
mode=PROFILING_MODE_WALL,
)
data = collector._convert_to_flamegraph_format()
self.assertEqual(data["stats"]["sample_interval_usec"], 250)

def test_flamegraph_nodes_include_per_thread_values(self):
collector = FlamegraphCollector(sample_interval_usec=1000)
root = MockFrameInfo("app.py", 1, "main")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Fix the times shown in :mod:`profiling.sampling` flame graph tooltips and status
bar. Sample counts were divided by 1000 and labelled as milliseconds, so the
reported times were wrong by a factor of the sampling interval. They are now
converted using the actual interval, which is always recorded in the exported
data.
Loading