Skip to content
Merged
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
2 changes: 0 additions & 2 deletions src/cli-template.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,8 @@ const CliTemplate: React.FC = () => {
const uint8Array = new Uint8Array(window.PROFILE_DATA)
const profile = Profile.decode(uint8Array)

console.log('Decoded profile:', profile)
setProfile(profile)
} catch (err) {
console.error('Profile parsing error:', err)
setError(err instanceof Error ? err.message : 'Failed to load profile')
} finally {
setLoading(false)
Expand Down
9 changes: 7 additions & 2 deletions src/components/FlameGraph.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ const createNodeJSServerProfile = (): Profile => {
location: locations,
function: profileFunctions,
stringTable,
timeNanos: BigInt(Date.now() * 1000000),
timeNanos: Date.now() * 1000000,
durationNanos: 10000000000, // 10 second profile
periodType: new ValueType({
type: stringTable.dedup('cpu'),
Expand Down Expand Up @@ -249,9 +249,14 @@ const meta: Meta<typeof FlameGraph> = {
),
],
argTypes: {
profile: {
// Exclude profile from controls as it contains BigInt values that can't be serialized
profile: {
control: false,
table: { disable: true }, // Hide profile from controls since it's complex
},
onFrameClick: { control: false },
onZoomChange: { control: false },
onAnimationComplete: { control: false },
width: {
control: { type: 'number' },
description: 'Width of the flamegraph',
Expand Down
20 changes: 16 additions & 4 deletions src/components/FlameGraph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ export interface FlameGraphProps {
zoomOnScroll?: boolean
scrollZoomSpeed?: number
scrollZoomInverted?: boolean
onFrameClick?: (frame: FrameData, stackTrace: FlameNode[], children: FlameNode[]) => void
selectedFrameId?: string | null
onFrameClick?: (frame: FrameData | null, stackTrace: FlameNode[], children: FlameNode[]) => void
onZoomChange?: (zoomLevel: number) => void
onAnimationComplete?: () => void
}
Expand All @@ -42,6 +43,7 @@ export const FlameGraph = forwardRef<{ rendererRef: React.RefObject<FlameGraphRe
zoomOnScroll = false,
scrollZoomSpeed = 0.05,
scrollZoomInverted = false,
selectedFrameId,
onFrameClick,
onZoomChange: _onZoomChange,
onAnimationComplete,
Expand Down Expand Up @@ -136,7 +138,6 @@ export const FlameGraph = forwardRef<{ rendererRef: React.RefObject<FlameGraphRe
renderer.destroy()
}
} catch (error) {
console.error('Failed to initialize FlameGraph renderer:', error)
setInitError(error instanceof Error ? error.message : 'Failed to initialize WebGL renderer')
}
}
Expand Down Expand Up @@ -167,6 +168,14 @@ export const FlameGraph = forwardRef<{ rendererRef: React.RefObject<FlameGraphRe
}
}, [shadowOpacity])

// Handle external frame selection
useEffect(() => {
if (rendererRef.current && selectedFrameId !== undefined) {
rendererRef.current.setFrameStates(selectedFrameId, hoveredFrame)
setSelectedFrame(selectedFrameId)
rendererRef.current.render()
}
}, [selectedFrameId, hoveredFrame])

useEffect(() => {
if (rendererRef.current && canvasRef.current && containerRef.current) {
Expand Down Expand Up @@ -341,6 +350,10 @@ export const FlameGraph = forwardRef<{ rendererRef: React.RefObject<FlameGraphRe
}, 100)
} else {
setSelectedFrame(null)
// Notify parent that selection was cleared
if (onFrameClick) {
onFrameClick(null as any, [], [])
}
// Update scrollable and pannable state after zoom reset
setTimeout(() => {
setCanPan(rendererRef.current!.canPan())
Expand Down Expand Up @@ -503,8 +516,7 @@ export const FlameGraph = forwardRef<{ rendererRef: React.RefObject<FlameGraphRe
frameData={hoveredFrameData}
mouseX={mousePosition.x}
mouseY={mousePosition.y}
primaryColor={primaryColor}
textColor={textColor}
fontFamily={fontFamily}
/>
)}
</div>
Expand Down
188 changes: 188 additions & 0 deletions src/components/FlameGraphTooltip.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import type { Meta, StoryObj } from '@storybook/react'
import { FlameGraphTooltip } from './FlameGraphTooltip'
import { FlameNode } from '../renderer'
import { useState, useEffect } from 'react'

const meta = {
title: 'FlameGraphTooltip',
component: FlameGraphTooltip,
parameters: {
layout: 'fullscreen',
},
decorators: [
(Story) => (
<div style={{
backgroundColor: '#0a0a0a',
padding: '40px',
minHeight: '100vh',
position: 'relative'
}}>
<Story />
</div>
),
],
} satisfies Meta<typeof FlameGraphTooltip>

export default meta
type Story = StoryObj<typeof meta>

// Sample frame data
const sampleFrame: FlameNode = {
id: 'root/main/server/handler',
name: 'requestHandler',
value: 1250,
width: 0.45,
depth: 3,
x: 0.2,
children: []
}

const longNameFrame: FlameNode = {
id: 'root/main/server/handler/very/long/path',
name: 'veryLongFunctionNameThatMightCauseWrappingInTheTooltipDisplay',
value: 567890,
width: 0.123456,
depth: 7,
x: 0.456,
children: []
}

const largeValueFrame: FlameNode = {
id: 'root/expensive',
name: 'expensiveOperation',
value: 1234567890,
width: 0.9876,
depth: 2,
x: 0.05,
children: []
}

const smallValueFrame: FlameNode = {
id: 'root/tiny',
name: 'tinyOperation',
value: 1,
width: 0.0001,
depth: 5,
x: 0.999,
children: []
}

export const Default: Story = {
args: {
frameData: sampleFrame,
mouseX: 400,
mouseY: 300,
},
}

export const LongName: Story = {
args: {
frameData: longNameFrame,
mouseX: 400,
mouseY: 300,
},
}

export const LargeValue: Story = {
args: {
frameData: largeValueFrame,
mouseX: 400,
mouseY: 300,
},
}

export const SmallValue: Story = {
args: {
frameData: smallValueFrame,
mouseX: 400,
mouseY: 300,
},
}

// Interactive story that follows mouse cursor
export const FollowMouse = () => {
const [mousePos, setMousePos] = useState({ x: 400, y: 300 })

useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
setMousePos({ x: e.clientX, y: e.clientY })
}

window.addEventListener('mousemove', handleMouseMove)
return () => window.removeEventListener('mousemove', handleMouseMove)
}, [])

return (
<>
<div style={{
position: 'fixed',
top: 20,
left: 20,
color: '#ffffff',
fontSize: '12px',
backgroundColor: 'rgba(0, 0, 0, 0.5)',
padding: '8px',
borderRadius: '4px',
zIndex: 2000
}}>
Mouse position: ({mousePos.x}, {mousePos.y})
</div>
<FlameGraphTooltip
frameData={sampleFrame}
mouseX={mousePos.x}
mouseY={mousePos.y}
/>
</>
)
}

// Story with very deep nesting
export const DeepNesting: Story = {
args: {
frameData: {
id: 'root/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p',
name: 'deeplyNestedFunction',
value: 42,
width: 0.00042,
depth: 16,
x: 0.5,
children: []
},
mouseX: 400,
mouseY: 300,
},
}

// Story demonstrating edge case values
export const EdgeCaseValues: Story = {
args: {
frameData: {
id: 'root/edge',
name: 'edgeCaseFunction',
value: 0,
width: 0,
depth: 0,
x: 0,
children: []
},
mouseX: 400,
mouseY: 300,
},
}

// Story with percentage precision
export const PrecisionDisplay: Story = {
args: {
frameData: {
id: 'root/precise',
name: 'preciseFunction',
value: 12345.6789,
width: 0.123456789,
depth: 3,
x: 0.333333,
children: []
},
mouseX: 400,
mouseY: 300,
},
}
Loading