Skip to content

Commit 8f24da7

Browse files
feat(profiling): add flamegraph tooltip (#31663)
* feat(profiling): flamegraph * feat(profiling): add flamegraph renderer and tests * fix(profiling): ignore unused renderer in test * fix(profiling): ignore unused renderer in test * feat(profiling): add util hooks * ref(hook): add usememowithprevious tests * feat(profiling): zoom view * fix(flamegraphrenderer): adjust for profiles that do not start at 0 and trim text more accurately * feat(profiling): add tooltip component * fix(flamegraph): remove merge marker * test(utils): fix tests * fix(tooltip): code review * feat(tooltip): add useDevicePixelRatio hook * feat(profiling): add view select menu (#31698) * feat(flamegraph): add view select menu * feat(profiling): use buttonbar * fix(viewselectmenu): use locale * test(gridrenderer): update tests * feat(profiling): add ability to search for frames (#31723) * feat(flamegraph): add view select menu * feat(profiling): use buttonbar * feat(profiling): add search * style(lint): Auto commit lint changes * style(lint): Auto commit lint changes * feat(profiling): decouple search fn * fix(flamegraphsearch): missing hook deps Co-authored-by: getsantry[bot] <66042841+getsantry[bot]@users.noreply.github.com> * fix(flamegraphtooltip): update deps Co-authored-by: getsantry[bot] <66042841+getsantry[bot]@users.noreply.github.com>
1 parent 37184e0 commit 8f24da7

File tree

13 files changed

+672
-116
lines changed

13 files changed

+672
-116
lines changed

docs-ui/stories/components/profiling/flamegraphZoomView.stories.js

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import * as React from 'react';
22

3+
import {FlamegraphSearch} from 'sentry/components/profiling/FlamegraphSearch';
4+
import {FlamegraphViewSelectMenu} from 'sentry/components/profiling/FlamegraphViewSelectMenu';
35
import {FlamegraphZoomView} from 'sentry/components/profiling/FlamegraphZoomView';
46
import {FlamegraphZoomViewMinimap} from 'sentry/components/profiling/FlamegraphZoomViewMinimap';
57
import {CanvasPoolManager} from 'sentry/utils/profiling/canvasScheduler';
@@ -16,8 +18,16 @@ export default {
1618
export const EventedTrace = () => {
1719
const canvasPoolManager = new CanvasPoolManager();
1820

21+
const [view, setView] = React.useState({inverted: false, leftHeavy: false});
22+
1923
const profiles = importProfile(trace);
20-
const flamegraph = new Flamegraph(profiles.profiles[0]);
24+
25+
const flamegraph = new Flamegraph(
26+
profiles.profiles[0],
27+
0,
28+
view.inverted,
29+
view.leftHeavy
30+
);
2131

2232
return (
2333
<div
@@ -29,6 +39,18 @@ export const EventedTrace = () => {
2939
overscrollBehavior: 'contain',
3040
}}
3141
>
42+
<div>
43+
<FlamegraphViewSelectMenu
44+
view={view.inverted ? 'bottom up' : 'top down'}
45+
sorting={view.leftHeavy ? 'left heavy' : 'call order'}
46+
onSortingChange={s => {
47+
setView({...view, leftHeavy: s === 'left heavy'});
48+
}}
49+
onViewChange={v => {
50+
setView({...view, inverted: v === 'bottom up'});
51+
}}
52+
/>
53+
</div>
3254
<div style={{height: 100, position: 'relative'}}>
3355
<FlamegraphZoomViewMinimap
3456
flamegraph={flamegraph}
@@ -46,6 +68,10 @@ export const EventedTrace = () => {
4668
canvasPoolManager={canvasPoolManager}
4769
flamegraphTheme={LightFlamegraphTheme}
4870
/>
71+
<FlamegraphSearch
72+
flamegraphs={[flamegraph]}
73+
canvasPoolManager={canvasPoolManager}
74+
/>
4975
</div>
5076
</div>
5177
);
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import * as React from 'react';
2+
import styled from '@emotion/styled';
3+
import {mat3, vec2} from 'gl-matrix';
4+
5+
import {FlamegraphTheme} from 'sentry/utils/profiling/flamegraph/FlamegraphTheme';
6+
import {getContext, measureText, Rect} from 'sentry/utils/profiling/gl/utils';
7+
import {useDevicePixelRatio} from 'sentry/utils/useDevicePixelRatio';
8+
9+
const useCachedMeasure = (string: string): Rect => {
10+
const cache = React.useRef<Record<string, Rect>>({});
11+
const ctx = React.useMemo(() => {
12+
const context = getContext(document.createElement('canvas'), '2d');
13+
14+
context.font = `12px ui-monospace, Menlo, Monaco, 'Cascadia Mono', 'Segoe UI Mono', 'Roboto Mono',
15+
'Oxygen Mono', 'Ubuntu Monospace', 'Source Code Pro', 'Fira Mono', 'Droid Sans Mono',
16+
'Courier New', monospace`;
17+
return context;
18+
}, []);
19+
20+
return React.useMemo(() => {
21+
if (cache.current[string]) {
22+
return cache.current[string];
23+
}
24+
25+
if (!ctx) {
26+
return Rect.Empty();
27+
}
28+
29+
const measures = measureText(string, ctx);
30+
cache.current[string] = measures;
31+
32+
return new Rect(0, 0, measures.width, measures.height);
33+
}, [string, ctx]);
34+
};
35+
36+
interface BoundTooltipProps {
37+
bounds: Rect;
38+
configToPhysicalSpace: mat3;
39+
cursor: vec2 | null;
40+
theme: FlamegraphTheme;
41+
children?: React.ReactNode;
42+
}
43+
44+
function BoundTooltip({
45+
bounds,
46+
configToPhysicalSpace,
47+
cursor,
48+
children,
49+
theme,
50+
}: BoundTooltipProps): React.ReactElement | null {
51+
const tooltipRef = React.useRef<HTMLDivElement>(null);
52+
const tooltipRect = useCachedMeasure(tooltipRef.current?.textContent ?? '');
53+
const devicePixelRatio = useDevicePixelRatio();
54+
55+
const physicalToLogicalSpace = React.useMemo(
56+
() =>
57+
mat3.fromScaling(
58+
mat3.create(),
59+
vec2.fromValues(1 / devicePixelRatio, 1 / devicePixelRatio)
60+
),
61+
[devicePixelRatio]
62+
);
63+
64+
const [tooltipBounds, setTooltipBounds] = React.useState<Rect>(Rect.Empty());
65+
66+
React.useLayoutEffect(() => {
67+
if (!children || bounds.isEmpty() || !tooltipRef.current) {
68+
setTooltipBounds(Rect.Empty());
69+
return;
70+
}
71+
72+
const newTooltipBounds = tooltipRef.current.getBoundingClientRect();
73+
74+
setTooltipBounds(
75+
new Rect(
76+
newTooltipBounds.x,
77+
newTooltipBounds.y,
78+
newTooltipBounds.width,
79+
newTooltipBounds.height
80+
)
81+
);
82+
}, [children, bounds, cursor]);
83+
84+
if (!children || !cursor || bounds.isEmpty()) {
85+
return null;
86+
}
87+
88+
const physicalSpaceCursor = vec2.transformMat3(
89+
vec2.create(),
90+
vec2.fromValues(cursor[0], cursor[1]),
91+
92+
configToPhysicalSpace
93+
);
94+
95+
const logicalSpaceCursor = vec2.transformMat3(
96+
vec2.create(),
97+
physicalSpaceCursor,
98+
physicalToLogicalSpace
99+
);
100+
101+
let cursorHorizontalPosition = logicalSpaceCursor[0];
102+
const cursorVerticalPosition = logicalSpaceCursor[1];
103+
104+
const mid = bounds.width / 2;
105+
106+
// If users screen is on right half of the screen, then we have more space to position on the left and vice versa
107+
// since default is right, we only need to handle 1 case
108+
if (cursorHorizontalPosition > mid) {
109+
// console.log('Cursor over mid');
110+
cursorHorizontalPosition -= tooltipBounds.width;
111+
}
112+
113+
return children ? (
114+
<Tooltip
115+
ref={tooltipRef}
116+
style={{
117+
fontSize: theme.SIZES.TOOLTIP_FONT_SIZE,
118+
fontFamily: theme.FONTS.FONT,
119+
left: cursorHorizontalPosition,
120+
top: cursorVerticalPosition,
121+
width: Math.min(tooltipRect.width, bounds.width - cursorHorizontalPosition - 2),
122+
}}
123+
>
124+
{children}
125+
</Tooltip>
126+
) : null;
127+
}
128+
129+
const Tooltip = styled('div')`
130+
background: #fff;
131+
position: absolute;
132+
white-space: nowrap;
133+
text-overflow: ellipsis;
134+
overflow: hidden;
135+
pointer-events: none;
136+
user-select: none;
137+
`;
138+
139+
export {BoundTooltip};

0 commit comments

Comments
 (0)