- Notifications
You must be signed in to change notification settings - Fork 303
Add copy summary feature for sharing profile context with LLMs #538
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
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.
There was a problem hiding this 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.
| 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') | ||
| } |
Copilot AI Dec 11, 2025
There was a problem hiding this comment.
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.
| if (!success) { | ||
| console.error('Failed to copy summary to clipboard') | ||
| } | ||
| } |
Copilot AI Dec 11, 2025
There was a problem hiding this comment.
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.
| if (!success) { | |
| console.error('Failed to copy summary to clipboard') | |
| } | |
| } | |
| if (success) { | |
| this.closeContextMenu(); | |
| } else { | |
| console.error('Failed to copy summary to clipboard') | |
| } |
| return [ | ||
| `${line.indent}${name}`, | ||
| `${line.indent}${' '.repeat(Math.max(0, name.length - stats.length))}${stats}`, |
Copilot AI Dec 11, 2025
There was a problem hiding this comment.
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.
| 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, |
| menuItem: { | ||
| padding: '6px 12px', | ||
| cursor: 'pointer', | ||
| color: theme.fgPrimaryColor, | ||
| ':hover': { | ||
| backgroundColor: theme.selectionPrimaryColor, | ||
| color: theme.altFgPrimaryColor, | ||
| }, |
Copilot AI Dec 11, 2025
There was a problem hiding this comment.
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.
| 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 | ||
| } | ||
| } |
Copilot AI Dec 11, 2025
There was a problem hiding this comment.
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.
| const copySummary = ( | ||
| <div className={css(style.toolbarTab)} onClick={handleCopySummary}> |
Copilot AI Dec 11, 2025
There was a problem hiding this comment.
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.
| 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" | |
| > |
Summary
This makes it easy to share performance profile context with LLMs for analysis.
Test plan