From b1e1a0e3b1aaab6e96cd1def87435c03a990e212 Mon Sep 17 00:00:00 2001 From: Pablo Galindo Salgado Date: Sun, 21 Sep 2025 19:29:57 +0100 Subject: [PATCH 1/4] gh-138122: Allow to filter by thread in tachyon's flamegraph --- Lib/profiling/sampling/collector.py | 2 +- Lib/profiling/sampling/flamegraph.css | 59 ++++++ Lib/profiling/sampling/flamegraph.js | 181 +++++++++++++++++- .../sampling/flamegraph_template.html | 6 + Lib/profiling/sampling/pstats_collector.py | 2 +- Lib/profiling/sampling/stack_collector.py | 28 ++- 6 files changed, 259 insertions(+), 19 deletions(-) diff --git a/Lib/profiling/sampling/collector.py b/Lib/profiling/sampling/collector.py index 3333e7bc99d177..b7a033ac0a6637 100644 --- a/Lib/profiling/sampling/collector.py +++ b/Lib/profiling/sampling/collector.py @@ -30,4 +30,4 @@ def _iter_all_frames(self, stack_frames, skip_idle=False): continue frames = thread_info.frame_info if frames: - yield frames + yield frames, thread_info.thread_id diff --git a/Lib/profiling/sampling/flamegraph.css b/Lib/profiling/sampling/flamegraph.css index 87387f20f5f958..4a9965e1d01256 100644 --- a/Lib/profiling/sampling/flamegraph.css +++ b/Lib/profiling/sampling/flamegraph.css @@ -227,6 +227,65 @@ body { background: #ffcd02; } +.thread-filter-wrapper { + display: inline-flex; + align-items: center; + margin-left: 16px; + background: white; + border-radius: 6px; + padding: 4px 8px 4px 12px; + border: 2px solid #3776ab; + transition: all 0.2s ease; +} + +.thread-filter-wrapper:hover { + border-color: #2d5aa0; + box-shadow: 0 2px 6px rgba(55, 118, 171, 0.2); +} + +.thread-filter-label { + color: #3776ab; + font-size: 14px; + font-weight: 600; + margin-right: 8px; + display: flex; + align-items: center; +} + +.thread-filter-select { + background: transparent; + color: #2e3338; + border: none; + padding: 4px 24px 4px 4px; + font-size: 14px; + font-weight: 600; + cursor: pointer; + min-width: 120px; + font-family: inherit; + appearance: none; + -webkit-appearance: none; + -moz-appearance: none; + background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%233776ab' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e"); + background-repeat: no-repeat; + background-position: right 4px center; + background-size: 16px; +} + +.thread-filter-select:focus { + outline: none; +} + +.thread-filter-select:hover { + color: #3776ab; +} + +.thread-filter-select option { + padding: 8px; + background: white; + color: #2e3338; + font-weight: normal; +} + #chart { width: 100%; height: calc(100vh - 160px); diff --git a/Lib/profiling/sampling/flamegraph.js b/Lib/profiling/sampling/flamegraph.js index 418d9995cdcbe6..a62f50f3a449cd 100644 --- a/Lib/profiling/sampling/flamegraph.js +++ b/Lib/profiling/sampling/flamegraph.js @@ -2,6 +2,8 @@ const EMBEDDED_DATA = {{FLAMEGRAPH_DATA}}; // Global string table for resolving string indices let stringTable = []; +let originalData = null; +let currentThreadFilter = 'all'; // Function to resolve string indices to actual strings function resolveString(index) { @@ -374,6 +376,12 @@ function initFlamegraph() { processedData = resolveStringIndices(EMBEDDED_DATA); } + // Store original data for filtering + originalData = processedData; + + // Initialize thread filter dropdown + initThreadFilter(processedData); + const tooltip = createPythonTooltip(processedData); const chart = createFlamegraph(tooltip, processedData.value); renderFlamegraph(chart, processedData); @@ -395,10 +403,39 @@ function populateStats(data) { const functionMap = new Map(); function collectFunctions(node) { - const filename = resolveString(node.filename); - const funcname = resolveString(node.funcname); + // Debug to understand the node structure + if (!node) return; + + // Try multiple ways to get the filename and function name + let filename = node.filename; + let funcname = node.funcname || node.name; + + // If they're numbers (string indices), resolve them + if (typeof filename === 'number') { + filename = resolveString(filename); + } + if (typeof funcname === 'number') { + funcname = resolveString(funcname); + } - if (filename && funcname) { + // If they're still undefined or null, try extracting from the name field + if (!filename && node.name) { + const nameStr = typeof node.name === 'number' ? resolveString(node.name) : node.name; + if (nameStr && nameStr.includes('(')) { + // Parse format: "funcname (filename:lineno)" + const match = nameStr.match(/^(.+?)\s*\((.+?):(\d+)\)$/); + if (match) { + funcname = funcname || match[1]; + filename = filename || match[2]; + } + } + } + + // Final fallback + filename = filename || 'unknown'; + funcname = funcname || 'unknown'; + + if (filename !== 'unknown' && funcname !== 'unknown' && node.value > 0) { // Calculate direct samples (this node's value minus children's values) let childrenValue = 0; if (node.children) { @@ -447,15 +484,18 @@ function populateStats(data) { // Populate the 3 cards for (let i = 0; i < 3; i++) { const num = i + 1; - if (i < hotSpots.length) { + if (i < hotSpots.length && hotSpots[i]) { const hotspot = hotSpots[i]; - const basename = hotspot.filename.split('/').pop(); - let funcDisplay = hotspot.funcname; + // Safe extraction with fallbacks + const filename = hotspot.filename || 'unknown'; + const basename = filename !== 'unknown' ? filename.split('/').pop() : 'unknown'; + const lineno = hotspot.lineno !== undefined && hotspot.lineno !== null ? hotspot.lineno : '?'; + let funcDisplay = hotspot.funcname || 'unknown'; if (funcDisplay.length > 35) { funcDisplay = funcDisplay.substring(0, 32) + '...'; } - document.getElementById(`hotspot-file-${num}`).textContent = `${basename}:${hotspot.lineno}`; + document.getElementById(`hotspot-file-${num}`).textContent = `${basename}:${lineno}`; document.getElementById(`hotspot-func-${num}`).textContent = funcDisplay; document.getElementById(`hotspot-detail-${num}`).textContent = `${hotspot.directPercent.toFixed(1)}% samples (${hotspot.directSamples.toLocaleString()})`; } else { @@ -505,3 +545,130 @@ function clearSearch() { } } +function initThreadFilter(data) { + const threadFilter = document.getElementById('thread-filter'); + const threadWrapper = document.querySelector('.thread-filter-wrapper'); + + if (!threadFilter || !data.threads) { + // Hide thread filter if no thread data + if (threadWrapper) { + threadWrapper.style.display = 'none'; + } + return; + } + + // Clear existing options except "All Threads" + threadFilter.innerHTML = ''; + + // Add thread options + const threads = data.threads || []; + threads.forEach(threadId => { + const option = document.createElement('option'); + option.value = threadId; + option.textContent = `Thread ${threadId}`; + threadFilter.appendChild(option); + }); + + // Hide filter if only one thread or no threads + if (threads.length <= 1 && threadWrapper) { + threadWrapper.style.display = 'none'; + } +} + +function filterByThread() { + const threadFilter = document.getElementById('thread-filter'); + if (!threadFilter || !originalData) return; + + const selectedThread = threadFilter.value; + currentThreadFilter = selectedThread; + + let filteredData; + if (selectedThread === 'all') { + // Show all data + filteredData = originalData; + } else { + // Filter data by thread + const threadId = parseInt(selectedThread); + filteredData = filterDataByThread(originalData, threadId); + + // Ensure string indices are resolved for the filtered data + if (filteredData.strings) { + stringTable = filteredData.strings; + filteredData = resolveStringIndices(filteredData); + } + } + + // Re-render flamegraph with filtered data + const tooltip = createPythonTooltip(filteredData); + const chart = createFlamegraph(tooltip, filteredData.value); + renderFlamegraph(chart, filteredData); +} + +function filterDataByThread(data, threadId) { + // Deep clone the data structure and filter by thread + function filterNode(node) { + // Check if this node contains the thread + if (!node.threads || !node.threads.includes(threadId)) { + return null; + } + + // Create a filtered copy of the node, preserving all fields + const filteredNode = { + name: node.name, + value: node.value, + filename: node.filename, + funcname: node.funcname, + lineno: node.lineno, + threads: node.threads, + source: node.source, + children: [] + }; + + // Copy any other properties that might exist + Object.keys(node).forEach(key => { + if (!(key in filteredNode)) { + filteredNode[key] = node[key]; + } + }); + + // Recursively filter children + if (node.children && Array.isArray(node.children)) { + filteredNode.children = node.children + .map(child => filterNode(child)) + .filter(child => child !== null); + } + + return filteredNode; + } + + // Create filtered root, preserving all metadata + const filteredRoot = { + ...data, + children: [], + strings: data.strings // Preserve string table + }; + + // Filter children + if (data.children && Array.isArray(data.children)) { + filteredRoot.children = data.children + .map(child => filterNode(child)) + .filter(child => child !== null); + } + + // Recalculate total value based on filtered children + function recalculateValue(node) { + if (!node.children || node.children.length === 0) { + return node.value || 0; + } + const childrenValue = node.children.reduce((sum, child) => { + return sum + recalculateValue(child); + }, 0); + node.value = Math.max(node.value || 0, childrenValue); + return node.value; + } + + recalculateValue(filteredRoot); + + return filteredRoot; +} + diff --git a/Lib/profiling/sampling/flamegraph_template.html b/Lib/profiling/sampling/flamegraph_template.html index 358791e80ce0cf..585a1abb61f812 100644 --- a/Lib/profiling/sampling/flamegraph_template.html +++ b/Lib/profiling/sampling/flamegraph_template.html @@ -63,6 +63,12 @@

Tachyon Profiler Performance Flamegraph

+
+ + +
diff --git a/Lib/profiling/sampling/pstats_collector.py b/Lib/profiling/sampling/pstats_collector.py index dec81b60659c53..e06dbf40aa1d89 100644 --- a/Lib/profiling/sampling/pstats_collector.py +++ b/Lib/profiling/sampling/pstats_collector.py @@ -41,7 +41,7 @@ def _process_frames(self, frames): self.callers[callee][caller] += 1 def collect(self, stack_frames): - for frames in self._iter_all_frames(stack_frames, skip_idle=self.skip_idle): + for frames, thread_id in self._iter_all_frames(stack_frames, skip_idle=self.skip_idle): self._process_frames(frames) def export(self, filename): diff --git a/Lib/profiling/sampling/stack_collector.py b/Lib/profiling/sampling/stack_collector.py index 6983be70ee0440..466061ba8c53d9 100644 --- a/Lib/profiling/sampling/stack_collector.py +++ b/Lib/profiling/sampling/stack_collector.py @@ -15,12 +15,12 @@ def __init__(self, *, skip_idle=False): self.skip_idle = skip_idle def collect(self, stack_frames, skip_idle=False): - for frames in self._iter_all_frames(stack_frames, skip_idle=skip_idle): + for frames, thread_id in self._iter_all_frames(stack_frames, skip_idle=skip_idle): if not frames: continue - self.process_frames(frames) + self.process_frames(frames, thread_id) - def process_frames(self, frames): + def process_frames(self, frames, thread_id): pass @@ -29,17 +29,17 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.stack_counter = collections.Counter() - def process_frames(self, frames): + def process_frames(self, frames, thread_id): call_tree = tuple(reversed(frames)) - self.stack_counter[call_tree] += 1 + self.stack_counter[(call_tree, thread_id)] += 1 def export(self, filename): lines = [] - for call_tree, count in self.stack_counter.items(): + for (call_tree, thread_id), count in self.stack_counter.items(): stack_str = ";".join( f"{os.path.basename(f[0])}:{f[2]}:{f[1]}" for f in call_tree ) - lines.append((stack_str, count)) + lines.append((f"tid:{thread_id};{stack_str}", count)) lines.sort(key=lambda x: (-x[1], x[0])) @@ -53,10 +53,11 @@ class FlamegraphCollector(StackTraceCollector): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.stats = {} - self._root = {"samples": 0, "children": {}} + self._root = {"samples": 0, "children": {}, "threads": set()} self._total_samples = 0 self._func_intern = {} self._string_table = StringTable() + self._all_threads = set() def set_stats(self, sample_interval_usec, duration_sec, sample_rate, error_rate=None): """Set profiling statistics to include in flamegraph data.""" @@ -111,6 +112,7 @@ def _convert_to_flamegraph_format(self): "name": self._string_table.intern("No Data"), "value": 0, "children": [], + "threads": [], "strings": self._string_table.get_strings() } @@ -133,6 +135,7 @@ def convert_children(children, min_samples): "filename": filename_idx, "lineno": func[1], "funcname": funcname_idx, + "threads": list(node.get("threads", set())), } source = self._get_source_lines(func) @@ -172,6 +175,7 @@ def convert_children(children, min_samples): new_name = f"Program Root: {old_name}" main_child["name"] = self._string_table.intern(new_name) main_child["stats"] = self.stats + main_child["threads"] = sorted(list(self._all_threads)) main_child["strings"] = self._string_table.get_strings() return main_child @@ -180,14 +184,17 @@ def convert_children(children, min_samples): "value": total_samples, "children": root_children, "stats": self.stats, + "threads": sorted(list(self._all_threads)), "strings": self._string_table.get_strings() } - def process_frames(self, frames): + def process_frames(self, frames, thread_id): # Reverse to root->leaf call_tree = reversed(frames) self._root["samples"] += 1 self._total_samples += 1 + self._root["threads"].add(thread_id) + self._all_threads.add(thread_id) current = self._root for func in call_tree: @@ -195,9 +202,10 @@ def process_frames(self, frames): children = current["children"] node = children.get(func) if node is None: - node = {"samples": 0, "children": {}} + node = {"samples": 0, "children": {}, "threads": set()} children[func] = node node["samples"] += 1 + node["threads"].add(thread_id) current = node def _get_source_lines(self, func): From 3ec314d20ee6d2c196d4bc143c63c8a0fe2d0719 Mon Sep 17 00:00:00 2001 From: Pablo Galindo Salgado Date: Sun, 21 Sep 2025 19:37:49 +0100 Subject: [PATCH 2/4] Simplify --- Lib/profiling/sampling/flamegraph.js | 54 ++++------------------- Lib/profiling/sampling/stack_collector.py | 2 +- 2 files changed, 9 insertions(+), 47 deletions(-) diff --git a/Lib/profiling/sampling/flamegraph.js b/Lib/profiling/sampling/flamegraph.js index a62f50f3a449cd..2a703ad3046992 100644 --- a/Lib/profiling/sampling/flamegraph.js +++ b/Lib/profiling/sampling/flamegraph.js @@ -403,26 +403,14 @@ function populateStats(data) { const functionMap = new Map(); function collectFunctions(node) { - // Debug to understand the node structure if (!node) return; - // Try multiple ways to get the filename and function name - let filename = node.filename; - let funcname = node.funcname || node.name; + let filename = typeof node.filename === 'number' ? resolveString(node.filename) : node.filename; + let funcname = typeof node.funcname === 'number' ? resolveString(node.funcname) : node.funcname; - // If they're numbers (string indices), resolve them - if (typeof filename === 'number') { - filename = resolveString(filename); - } - if (typeof funcname === 'number') { - funcname = resolveString(funcname); - } - - // If they're still undefined or null, try extracting from the name field - if (!filename && node.name) { + if (!filename || !funcname) { const nameStr = typeof node.name === 'number' ? resolveString(node.name) : node.name; - if (nameStr && nameStr.includes('(')) { - // Parse format: "funcname (filename:lineno)" + if (nameStr?.includes('(')) { const match = nameStr.match(/^(.+?)\s*\((.+?):(\d+)\)$/); if (match) { funcname = funcname || match[1]; @@ -431,7 +419,6 @@ function populateStats(data) { } } - // Final fallback filename = filename || 'unknown'; funcname = funcname || 'unknown'; @@ -486,10 +473,9 @@ function populateStats(data) { const num = i + 1; if (i < hotSpots.length && hotSpots[i]) { const hotspot = hotSpots[i]; - // Safe extraction with fallbacks const filename = hotspot.filename || 'unknown'; const basename = filename !== 'unknown' ? filename.split('/').pop() : 'unknown'; - const lineno = hotspot.lineno !== undefined && hotspot.lineno !== null ? hotspot.lineno : '?'; + const lineno = hotspot.lineno ?? '?'; let funcDisplay = hotspot.funcname || 'unknown'; if (funcDisplay.length > 35) { funcDisplay = funcDisplay.substring(0, 32) + '...'; @@ -591,7 +577,6 @@ function filterByThread() { const threadId = parseInt(selectedThread); filteredData = filterDataByThread(originalData, threadId); - // Ensure string indices are resolved for the filtered data if (filteredData.strings) { stringTable = filteredData.strings; filteredData = resolveStringIndices(filteredData); @@ -605,33 +590,16 @@ function filterByThread() { } function filterDataByThread(data, threadId) { - // Deep clone the data structure and filter by thread function filterNode(node) { - // Check if this node contains the thread if (!node.threads || !node.threads.includes(threadId)) { return null; } - // Create a filtered copy of the node, preserving all fields const filteredNode = { - name: node.name, - value: node.value, - filename: node.filename, - funcname: node.funcname, - lineno: node.lineno, - threads: node.threads, - source: node.source, + ...node, children: [] }; - // Copy any other properties that might exist - Object.keys(node).forEach(key => { - if (!(key in filteredNode)) { - filteredNode[key] = node[key]; - } - }); - - // Recursively filter children if (node.children && Array.isArray(node.children)) { filteredNode.children = node.children .map(child => filterNode(child)) @@ -641,28 +609,22 @@ function filterDataByThread(data, threadId) { return filteredNode; } - // Create filtered root, preserving all metadata const filteredRoot = { ...data, - children: [], - strings: data.strings // Preserve string table + children: [] }; - // Filter children if (data.children && Array.isArray(data.children)) { filteredRoot.children = data.children .map(child => filterNode(child)) .filter(child => child !== null); } - // Recalculate total value based on filtered children function recalculateValue(node) { if (!node.children || node.children.length === 0) { return node.value || 0; } - const childrenValue = node.children.reduce((sum, child) => { - return sum + recalculateValue(child); - }, 0); + const childrenValue = node.children.reduce((sum, child) => sum + recalculateValue(child), 0); node.value = Math.max(node.value || 0, childrenValue); return node.value; } diff --git a/Lib/profiling/sampling/stack_collector.py b/Lib/profiling/sampling/stack_collector.py index 466061ba8c53d9..bc38151e067989 100644 --- a/Lib/profiling/sampling/stack_collector.py +++ b/Lib/profiling/sampling/stack_collector.py @@ -135,7 +135,7 @@ def convert_children(children, min_samples): "filename": filename_idx, "lineno": func[1], "funcname": funcname_idx, - "threads": list(node.get("threads", set())), + "threads": sorted(list(node.get("threads", set()))), } source = self._get_source_lines(func) From 1a86c1b620f557956f6d8316cfb57902e7be4057 Mon Sep 17 00:00:00 2001 From: Pablo Galindo Salgado Date: Sun, 21 Sep 2025 19:37:57 +0100 Subject: [PATCH 3/4] Fix tests --- .../test_profiling/test_sampling_profiler.py | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/Lib/test/test_profiling/test_sampling_profiler.py b/Lib/test/test_profiling/test_sampling_profiler.py index fd6b1862230288..e4e5e2dfa20c9f 100644 --- a/Lib/test/test_profiling/test_sampling_profiler.py +++ b/Lib/test/test_profiling/test_sampling_profiler.py @@ -278,8 +278,9 @@ def test_collapsed_stack_collector_with_empty_and_deep_stacks(self): test_frames = [MockInterpreterInfo(0, [MockThreadInfo(1, [("file.py", 10, "func")])])] collector.collect(test_frames) self.assertEqual(len(collector.stack_counter), 1) - ((path,), count), = collector.stack_counter.items() - self.assertEqual(path, ("file.py", 10, "func")) + ((path, thread_id), count), = collector.stack_counter.items() + self.assertEqual(path, (("file.py", 10, "func"),)) + self.assertEqual(thread_id, 1) self.assertEqual(count, 1) # Test with very deep stack @@ -288,10 +289,11 @@ def test_collapsed_stack_collector_with_empty_and_deep_stacks(self): collector = CollapsedStackCollector() collector.collect(test_frames) # One aggregated path with 100 frames (reversed) - (path_tuple,), = (collector.stack_counter.keys(),) + ((path_tuple, thread_id),), = (collector.stack_counter.keys(),) self.assertEqual(len(path_tuple), 100) self.assertEqual(path_tuple[0], ("file99.py", 99, "func99")) self.assertEqual(path_tuple[-1], ("file0.py", 0, "func0")) + self.assertEqual(thread_id, 1) def test_pstats_collector_basic(self): """Test basic PstatsCollector functionality.""" @@ -393,9 +395,10 @@ def test_collapsed_stack_collector_basic(self): # Should store one reversed path self.assertEqual(len(collector.stack_counter), 1) - (path, count), = collector.stack_counter.items() + ((path, thread_id), count), = collector.stack_counter.items() expected_tree = (("file.py", 20, "func2"), ("file.py", 10, "func1")) self.assertEqual(path, expected_tree) + self.assertEqual(thread_id, 1) self.assertEqual(count, 1) def test_collapsed_stack_collector_export(self): @@ -424,9 +427,9 @@ def test_collapsed_stack_collector_export(self): lines = content.strip().split("\n") self.assertEqual(len(lines), 2) # Two unique stacks - # Check collapsed format: file:func:line;file:func:line count - stack1_expected = "file.py:func2:20;file.py:func1:10 2" - stack2_expected = "other.py:other_func:5 1" + # Check collapsed format: tid:X;file:func:line;file:func:line count + stack1_expected = "tid:1;file.py:func2:20;file.py:func1:10 2" + stack2_expected = "tid:1;other.py:other_func:5 1" self.assertIn(stack1_expected, lines) self.assertIn(stack2_expected, lines) @@ -1514,7 +1517,8 @@ def test_collapsed_stack_with_recursion(self): self.assertEqual(len(collector.stack_counter), 2) # First path should be longer (deeper recursion) than the second - paths = list(collector.stack_counter.keys()) + path_tuples = list(collector.stack_counter.keys()) + paths = [p[0] for p in path_tuples] # Extract just the call paths lengths = [len(p) for p in paths] self.assertNotEqual(lengths[0], lengths[1]) @@ -1527,7 +1531,7 @@ def test_collapsed_stack_with_recursion(self): def total_occurrences(func): total = 0 - for path, count in collector.stack_counter.items(): + for (path, thread_id), count in collector.stack_counter.items(): total += sum(1 for f in path if f == func) * count return total From 1e9628941d874b22d09ed536b161b4c89d46edd2 Mon Sep 17 00:00:00 2001 From: Pablo Galindo Date: Thu, 25 Sep 2025 14:58:00 +0100 Subject: [PATCH 4/4] Address code review --- Lib/profiling/sampling/flamegraph.css | 2 +- Lib/profiling/sampling/flamegraph.js | 10 +++------- Lib/profiling/sampling/sample.py | 2 +- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/Lib/profiling/sampling/flamegraph.css b/Lib/profiling/sampling/flamegraph.css index 4a9965e1d01256..67754ca609aa43 100644 --- a/Lib/profiling/sampling/flamegraph.css +++ b/Lib/profiling/sampling/flamegraph.css @@ -228,7 +228,7 @@ body { } .thread-filter-wrapper { - display: inline-flex; + display: none; align-items: center; margin-left: 16px; background: white; diff --git a/Lib/profiling/sampling/flamegraph.js b/Lib/profiling/sampling/flamegraph.js index 2a703ad3046992..95ad7ca6184ac6 100644 --- a/Lib/profiling/sampling/flamegraph.js +++ b/Lib/profiling/sampling/flamegraph.js @@ -536,10 +536,6 @@ function initThreadFilter(data) { const threadWrapper = document.querySelector('.thread-filter-wrapper'); if (!threadFilter || !data.threads) { - // Hide thread filter if no thread data - if (threadWrapper) { - threadWrapper.style.display = 'none'; - } return; } @@ -555,9 +551,9 @@ function initThreadFilter(data) { threadFilter.appendChild(option); }); - // Hide filter if only one thread or no threads - if (threads.length <= 1 && threadWrapper) { - threadWrapper.style.display = 'none'; + // Show filter if more than one thread + if (threads.length > 1 && threadWrapper) { + threadWrapper.style.display = 'inline-flex'; } } diff --git a/Lib/profiling/sampling/sample.py b/Lib/profiling/sampling/sample.py index 20437481a0af98..b5d3f395395a11 100644 --- a/Lib/profiling/sampling/sample.py +++ b/Lib/profiling/sampling/sample.py @@ -754,7 +754,7 @@ def main(): "--mode", choices=["wall", "cpu", "gil"], default="wall", - help="Sampling mode: wall (all threads), cpu (only CPU-running threads), gil (only GIL-holding threads)", + help="Sampling mode: wall (all threads), cpu (only CPU-running threads), gil (only GIL-holding threads) (default: wall)", ) # Output format selection