Skip to content

Conversation

@jlfwong
Copy link
Owner

@jlfwong jlfwong commented Dec 11, 2025

Summary

  • Add a "Copy Summary" button in the toolbar that generates and copies a text summary of all loaded profiles to the clipboard
  • Add right-click context menu on flamechart frames with "Copy summary as text" option to copy a summary of a specific node and its callees
  • The summaries include both Bottoms Up (by self time) and Call Tree views, filtered to show entries ≥1% of total weight

This makes it easy to share performance profile context with LLMs for analysis.

Test plan

  • Load a profile, click "Copy Summary" in toolbar, verify text is copied to clipboard
  • Right-click a frame in the flamechart, select "Copy summary as text", verify the summary is specific to that subtree
  • Test with multiple profiles loaded to verify the combined summary works
Adds a toolbar button that generates a combined summary of all loaded profiles (including Bottoms Up and Call Tree views) and copies it to the clipboard. This makes it easy to share profile context with LLMs for performance analysis.
@coveralls
Copy link

Coverage Status

coverage: 42.376% (-1.4%) from 43.737%
when pulling 0c9b1b2 on jlfwong/copy-summary-text
into 3613918 on main.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds functionality to copy performance profile summaries as text to the clipboard for sharing with LLMs. It introduces two main copy mechanisms: a toolbar button that generates summaries for all loaded profiles, and a right-click context menu on flamechart frames to copy summaries of specific subtrees. The summaries include both Bottoms Up (by self time) and Call Tree views, filtered to show entries ≥1% of total weight.

Key Changes:

  • Added tree-summary.ts library with functions to generate formatted text summaries of call trees
  • Added Copy Summary button to toolbar for exporting all loaded profiles
  • Added context menu to flamechart with "Copy summary as text" option for individual nodes

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 6 comments.

File Description
src/lib/tree-summary.ts New library providing tree summary generation and clipboard copy functionality
src/views/toolbar.tsx Added "Copy Summary" button to toolbar with handler for copying all profile summaries
src/views/flamechart-pan-zoom-view.tsx Added context menu support and integrated tree summary copy for individual frames
src/views/context-menu.tsx New context menu component with theme support and viewport-aware positioning

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +194 to +284
export function generateTreeSummary(options: TreeSummaryOptions): string {
const {node, totalWeight, formatValue} = options

// Build the output
const output: string[] = []

// Header
output.push('Performance Summary')
output.push('='.repeat(60))
output.push('')

// Get the node's weight for thresholds
const nodeWeight = node.isRoot() ? totalWeight : node.getTotalWeight()

// Root node info
if (!node.isRoot()) {
output.push(`Selected: ${node.frame.name}`)
if (node.frame.file) {
let location = node.frame.file
if (node.frame.line != null) {
location += `:${node.frame.line}`
if (node.frame.col != null) {
location += `:${node.frame.col}`
}
}
output.push(`Location: ${location}`)
}
const totalPercent = (node.getTotalWeight() / totalWeight) * 100
const selfPercent = (node.getSelfWeight() / totalWeight) * 100
output.push(`Total: ${formatValue(node.getTotalWeight())} (${formatPercent(totalPercent)})`)
output.push(`Self: ${formatValue(node.getSelfWeight())} (${formatPercent(selfPercent)})`)
output.push('')
}

// Bottoms Up view (all unique frames in subtree, aggregated)
// Filter to frames with self weight >= 1% of total profile weight
const bottomsUpMinSelfWeight = totalWeight * MIN_WEIGHT_THRESHOLD
const bottomsUpEntries = buildBottomsUpEntries(node, totalWeight, bottomsUpMinSelfWeight)

if (bottomsUpEntries.length > 0) {
output.push('Bottoms Up (by self time, >=1% of total):')
output.push('-'.repeat(60))
output.push('')

for (const entry of bottomsUpEntries) {
let name = entry.frame.name
if (entry.frame.file) {
let location = entry.frame.file
if (entry.frame.line != null) {
location += `:${entry.frame.line}`
if (entry.frame.col != null) {
location += `:${entry.frame.col}`
}
}
name += ` (${location})`
}
const stats = `[self: ${formatValue(entry.selfWeight)} (${formatPercent(
entry.selfPercent,
)}), total: ${formatValue(entry.totalWeight)} (${formatPercent(entry.totalPercent)})]`
output.push(`${name}`)
output.push(`${stats}`)
output.push('')
}
}

// Call Tree view (children of this node)
// Filter to nodes >= 1% of the copied node's weight
const callTreeMinWeight = nodeWeight * MIN_WEIGHT_THRESHOLD
const callTreeLines: TreeLine[] = []
buildTreeLines(node, totalWeight, callTreeMinWeight, callTreeLines, '', true, true)

if (callTreeLines.length > 0) {
output.push('Call Tree (callees, >=1% of selection):')
output.push('-'.repeat(60))
output.push('')

for (const line of callTreeLines) {
output.push(...formatTreeLine(line, formatValue))
}
output.push('')
}

if (bottomsUpEntries.length === 0 && callTreeLines.length === 0) {
return 'No data available'
}

output.push('-'.repeat(60))
output.push(`Total weight of profile: ${formatValue(totalWeight)}`)

return output.join('\n')
}
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new tree summary generation functions lack test coverage. Given the complexity of the bottoms-up and call tree generation logic, consider adding unit tests to verify correct behavior for edge cases like empty profiles, nodes with zero weight, and proper filtering/sorting of entries.

Copilot uses AI. Check for mistakes.
Comment on lines +744 to +747
if (!success) {
console.error('Failed to copy summary to clipboard')
}
}
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The async copy operation doesn't close the context menu automatically on success. This could leave the menu open after copying, which may be confusing to users. Consider calling this.closeContextMenu() after a successful copy to provide clearer feedback.

Suggested change
if (!success) {
console.error('Failed to copy summary to clipboard')
}
}
if (success) {
this.closeContextMenu();
} else {
console.error('Failed to copy summary to clipboard')
}
Copilot uses AI. Check for mistakes.
Comment on lines +180 to +182
return [
`${line.indent}${name}`,
`${line.indent}${' '.repeat(Math.max(0, name.length - stats.length))}${stats}`,
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The spacing calculation ' '.repeat(Math.max(0, name.length - stats.length)) appears to attempt alignment but may produce incorrect results when name.length is less than stats.length, resulting in no spacing. This logic seems intended to align stats but the calculation doesn't account for the indent width. Consider revising or removing this alignment logic if it's not producing the intended visual result.

Suggested change
return [
`${line.indent}${name}`,
`${line.indent}${' '.repeat(Math.max(0, name.length - stats.length))}${stats}`,
// Choose a fixed width for the name column (including indent), e.g., 40 chars
const NAME_COL_WIDTH = 40
const nameWithIndent = `${line.indent}${name}`
return [
nameWithIndent,
nameWithIndent.padEnd(NAME_COL_WIDTH, ' ') + stats,
Copilot uses AI. Check for mistakes.
Comment on lines +73 to +80
menuItem: {
padding: '6px 12px',
cursor: 'pointer',
color: theme.fgPrimaryColor,
':hover': {
backgroundColor: theme.selectionPrimaryColor,
color: theme.altFgPrimaryColor,
},
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The context menu items lack keyboard accessibility. Interactive elements inside the menu should be keyboard navigable and activatable with Enter/Space keys. Consider adding tabIndex={0} and handling onKeyDown events to support keyboard users who cannot use a mouse.

Copilot uses AI. Check for mistakes.
Comment on lines +366 to +374
export async function copyToClipboard(text: string): Promise<boolean> {
try {
await navigator.clipboard.writeText(text)
return true
} catch (err) {
console.error('Failed to copy to clipboard:', err)
return false
}
}
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The clipboard API requires a secure context (HTTPS or localhost). Consider adding a fallback mechanism or user-visible warning for non-secure contexts, as navigator.clipboard may be undefined or fail silently in some environments.

Copilot uses AI. Check for mistakes.
Comment on lines +179 to +180
const copySummary = (
<div className={css(style.toolbarTab)} onClick={handleCopySummary}>
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Copy Summary button is missing keyboard accessibility support. Consider adding tabIndex={0} and onKeyDown handlers to allow keyboard users to activate it with Enter or Space keys, consistent with accessibility best practices for interactive elements.

Suggested change
const copySummary = (
<div className={css(style.toolbarTab)} onClick={handleCopySummary}>
const handleCopySummaryKeyDown = useCallback(
(event: KeyboardEvent) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
handleCopySummary();
}
},
[handleCopySummary]
);
const copySummary = (
<div
className={css(style.toolbarTab)}
onClick={handleCopySummary}
tabIndex={0}
onKeyDown={handleCopySummaryKeyDown}
role="button"
>
Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

3 participants