Skip to content

Commit cf63cf1

Browse files
authored
feat(flamechart): allow rendering the chart on arbitrary x axis (#33577)
* ref(profiles): use weight of profiles as duration * ref(import): check if transaction is present and add sampled profile * ref(flamegraphRenderer): rename configToPhysicalSpace to configViewToPhysicalSpace * ref(flamegraph): remove storing some unnecessary properties * feat(flamegraph): allow passing transaction * feat(flamegraph): allow using custom configSpace and translate chart * feat(flamegraph): avoid reiterating frames and move configSpace to contructor * feat(flamegraph): use transaction axis * feat(flamegraph): add menu * ref(flamegraph): pass configSpace as param and sync minimap effect * fix(flamegraph): remove inverted check * fix(flamegraph): remove inverted check
1 parent 8213db2 commit cf63cf1

30 files changed

+11238
-240
lines changed

docs-ui/stories/components/profiling/EventedTrace.json

Lines changed: 2672 additions & 1 deletion
Large diffs are not rendered by default.

docs-ui/stories/components/profiling/SampledTrace.json

Lines changed: 8260 additions & 0 deletions
Large diffs are not rendered by default.

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,20 @@ export const EventedTrace = () => {
2222
);
2323
};
2424

25+
const sampledTrace = importProfile(require('./SampledTrace.json'));
26+
27+
export const SampledTrace = () => {
28+
return (
29+
<FlamegraphStateProvider>
30+
<FlamegraphThemeProvider>
31+
<FullScreenFlamegraphContainer>
32+
<Flamegraph profiles={sampledTrace} />
33+
</FullScreenFlamegraphContainer>
34+
</FlamegraphThemeProvider>
35+
</FlamegraphStateProvider>
36+
);
37+
};
38+
2539
const jsSelfProfile = importProfile(require('./JSSelfProfilingTrace.json'));
2640

2741
export const JSSelfProfiling = () => {

static/app/components/profiling/boundTooltip.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,14 @@ const useCachedMeasure = (string: string, font: string): Rect => {
3232

3333
interface BoundTooltipProps {
3434
bounds: Rect;
35-
configToPhysicalSpace: mat3;
35+
configViewToPhysicalSpace: mat3;
3636
cursor: vec2 | null;
3737
children?: React.ReactNode;
3838
}
3939

4040
function BoundTooltip({
4141
bounds,
42-
configToPhysicalSpace,
42+
configViewToPhysicalSpace,
4343
cursor,
4444
children,
4545
}: BoundTooltipProps): React.ReactElement | null {
@@ -88,7 +88,7 @@ function BoundTooltip({
8888
vec2.create(),
8989
vec2.fromValues(cursor[0], cursor[1]),
9090

91-
configToPhysicalSpace
91+
configViewToPhysicalSpace
9292
);
9393

9494
const logicalSpaceCursor = vec2.transformMat3(

static/app/components/profiling/flamegraph.tsx

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,21 @@ import {Flamegraph as FlamegraphModel} from 'sentry/utils/profiling/flamegraph';
1414
import {FlamegraphTheme} from 'sentry/utils/profiling/flamegraph/flamegraphTheme';
1515
import {useFlamegraphPreferences} from 'sentry/utils/profiling/flamegraph/useFlamegraphPreferences';
1616
import {useFlamegraphTheme} from 'sentry/utils/profiling/flamegraph/useFlamegraphTheme';
17+
import {Rect} from 'sentry/utils/profiling/gl/utils';
1718
import {ProfileGroup} from 'sentry/utils/profiling/profile/importProfile';
19+
import {Profile} from 'sentry/utils/profiling/profile/profile';
1820

21+
function getTransactionConfigSpace(profiles: Profile[]): Rect {
22+
return new Rect(0, 0, Math.max(...profiles.map(p => p.endedAt)), 0);
23+
}
1924
interface FlamegraphProps {
2025
profiles: ProfileGroup;
2126
}
2227

2328
function Flamegraph(props: FlamegraphProps): ReactElement {
2429
const flamegraphTheme = useFlamegraphTheme();
25-
const [{sorting, view}, dispatch] = useFlamegraphPreferences();
30+
const [{sorting, view, synchronizeXAxisWithTransaction}, dispatch] =
31+
useFlamegraphPreferences();
2632
const canvasPoolManager = useMemo(() => new CanvasPoolManager(), []);
2733

2834
const [activeProfileIndex, setActiveProfileIndex] = useState<number | null>(null);
@@ -35,11 +41,20 @@ function Flamegraph(props: FlamegraphProps): ReactElement {
3541
// if the activeProfileIndex is null, use the activeProfileIndex from the profile group
3642
const profileIndex = activeProfileIndex ?? profiles.activeProfileIndex;
3743

38-
return new FlamegraphModel(profiles.profiles[profileIndex], profileIndex, {
39-
inverted: view === 'bottom up',
40-
leftHeavy: sorting === 'left heavy',
41-
});
42-
}, [profiles, activeProfileIndex, sorting, view]);
44+
const flamegraphModel = new FlamegraphModel(
45+
profiles.profiles[profileIndex],
46+
profileIndex,
47+
{
48+
inverted: view === 'bottom up',
49+
leftHeavy: sorting === 'left heavy',
50+
configSpace: synchronizeXAxisWithTransaction
51+
? getTransactionConfigSpace(profiles.profiles)
52+
: undefined,
53+
}
54+
);
55+
56+
return flamegraphModel;
57+
}, [profiles, activeProfileIndex, sorting, synchronizeXAxisWithTransaction, view]);
4358

4459
const onImport = useCallback((profile: ProfileGroup) => {
4560
setActiveProfileIndex(null);
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import styled from '@emotion/styled';
2+
3+
import DropdownButton from 'sentry/components/dropdownButton';
4+
import DropdownControl, {DropdownItem} from 'sentry/components/dropdownControl';
5+
import {t} from 'sentry/locale';
6+
import space from 'sentry/styles/space';
7+
import {useFlamegraphPreferences} from 'sentry/utils/profiling/flamegraph/useFlamegraphPreferences';
8+
9+
function FlamegraphXAxisOptionsMenu(): React.ReactElement {
10+
const [{synchronizeXAxisWithTransaction}, dispatch] = useFlamegraphPreferences();
11+
12+
return (
13+
<OptionsMenuContainer>
14+
<DropdownControl
15+
button={({isOpen, getActorProps}) => (
16+
<DropdownButton
17+
{...getActorProps()}
18+
isOpen={isOpen}
19+
prefix={t('X Axis')}
20+
size="xsmall"
21+
>
22+
{synchronizeXAxisWithTransaction ? 'Transaction' : 'Standalone'}
23+
</DropdownButton>
24+
)}
25+
>
26+
<DropdownItem
27+
onSelect={() =>
28+
dispatch({
29+
type: 'set synchronizeXAxisWithTransaction',
30+
payload: false,
31+
})
32+
}
33+
isActive={!synchronizeXAxisWithTransaction}
34+
>
35+
Standalone
36+
</DropdownItem>
37+
<DropdownItem
38+
onSelect={() =>
39+
dispatch({
40+
type: 'set synchronizeXAxisWithTransaction',
41+
payload: true,
42+
})
43+
}
44+
isActive={synchronizeXAxisWithTransaction}
45+
>
46+
Transaction
47+
</DropdownItem>
48+
</DropdownControl>
49+
</OptionsMenuContainer>
50+
);
51+
}
52+
53+
const OptionsMenuContainer = styled('div')`
54+
display: flex;
55+
flex-direction: row;
56+
gap: ${space(0.5)};
57+
justify-content: flex-end;
58+
`;
59+
60+
export {FlamegraphXAxisOptionsMenu};

static/app/components/profiling/flamegraphOptionsMenu.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import {CanvasPoolManager} from 'sentry/utils/profiling/canvasScheduler';
99
import {FlamegraphPreferences} from 'sentry/utils/profiling/flamegraph/flamegraphStateProvider';
1010
import {useFlamegraphPreferences} from 'sentry/utils/profiling/flamegraph/useFlamegraphPreferences';
1111

12+
import {FlamegraphXAxisOptionsMenu} from './flamegraphAxisOptionsMenu';
13+
1214
interface FlamegraphOptionsMenuProps {
1315
canvasPoolManager: CanvasPoolManager;
1416
}
@@ -50,6 +52,7 @@ function FlamegraphOptionsMenu({
5052
)
5153
)}
5254
</DropdownControl>
55+
<FlamegraphXAxisOptionsMenu />
5356
<Button size="xsmall" onClick={() => canvasPoolManager.dispatch('resetZoom', [])}>
5457
{t('Reset Zoom')}
5558
</Button>

static/app/components/profiling/flamegraphZoomView.tsx

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ function FlamegraphZoomView({
6161
{draw_border: true}
6262
);
6363

64+
if (!previousRenderer?.configSpace.equals(renderer.configSpace)) {
65+
return renderer;
66+
}
67+
6468
if (previousRenderer?.flamegraph.profile === renderer.flamegraph.profile) {
6569
if (previousRenderer.flamegraph.inverted !== renderer.flamegraph.inverted) {
6670
// Preserve the position where the user just was before they toggled
@@ -195,7 +199,7 @@ function FlamegraphZoomView({
195199
BORDER_WIDTH: flamegraphRenderer.theme.SIZES.FRAME_BORDER_WIDTH,
196200
},
197201
selectedFrameRenderer.context,
198-
flamegraphRenderer.configToPhysicalSpace
202+
flamegraphRenderer.configViewToPhysicalSpace
199203
);
200204
}
201205

@@ -214,7 +218,7 @@ function FlamegraphZoomView({
214218
BORDER_WIDTH: flamegraphRenderer.theme.SIZES.HOVERED_FRAME_BORDER_WIDTH,
215219
},
216220
selectedFrameRenderer.context,
217-
flamegraphRenderer.configToPhysicalSpace
221+
flamegraphRenderer.configViewToPhysicalSpace
218222
);
219223
}
220224
};
@@ -223,15 +227,15 @@ function FlamegraphZoomView({
223227
textRenderer.draw(
224228
flamegraphRenderer.configView,
225229
flamegraphRenderer.configSpace,
226-
flamegraphRenderer.configToPhysicalSpace
230+
flamegraphRenderer.configViewToPhysicalSpace
227231
);
228232
};
229233

230234
const drawGrid = () => {
231235
gridRenderer.draw(
232236
flamegraphRenderer.configView,
233237
flamegraphRenderer.physicalSpace,
234-
flamegraphRenderer.configToPhysicalSpace
238+
flamegraphRenderer.configViewToPhysicalSpace
235239
);
236240
};
237241

@@ -417,7 +421,7 @@ function FlamegraphZoomView({
417421

418422
const physicalToConfig = mat3.invert(
419423
mat3.create(),
420-
flamegraphRenderer.configToPhysicalSpace
424+
flamegraphRenderer.configViewToPhysicalSpace
421425
);
422426
const [m00, m01, m02, m10, m11, m12] = physicalToConfig;
423427

@@ -511,7 +515,7 @@ function FlamegraphZoomView({
511515
const physicalDelta = vec2.fromValues(evt.deltaX, evt.deltaY);
512516
const physicalToConfig = mat3.invert(
513517
mat3.create(),
514-
flamegraphRenderer.configToPhysicalSpace
518+
flamegraphRenderer.configViewToPhysicalSpace
515519
);
516520
const [m00, m01, m02, m10, m11, m12] = physicalToConfig;
517521

@@ -580,7 +584,7 @@ function FlamegraphZoomView({
580584
<BoundTooltip
581585
bounds={canvasBounds}
582586
cursor={configSpaceCursor}
583-
configToPhysicalSpace={flamegraphRenderer?.configToPhysicalSpace}
587+
configViewToPhysicalSpace={flamegraphRenderer?.configViewToPhysicalSpace}
584588
>
585589
{hoveredNode?.frame?.name}
586590
</BoundTooltip>

static/app/components/profiling/flamegraphZoomViewMinimap.tsx

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,34 @@ function FlamegraphZoomViewMinimap({
5050
},
5151
});
5252

53-
if (previousRenderer?.flamegraph.name === renderer.flamegraph.name) {
54-
renderer.setConfigView(previousRenderer.configView);
53+
if (!previousRenderer?.configSpace.equals(renderer.configSpace)) {
54+
return renderer;
55+
}
56+
57+
if (previousRenderer?.flamegraph.profile === renderer.flamegraph.profile) {
58+
if (previousRenderer.flamegraph.inverted !== renderer.flamegraph.inverted) {
59+
// Preserve the position where the user just was before they toggled
60+
// inverted. This means that the horizontal position is unchanged
61+
// while the vertical position needs to determined based on the
62+
// current position.
63+
renderer.setConfigView(
64+
previousRenderer.configView.translateY(
65+
previousRenderer.configSpace.height -
66+
previousRenderer.configView.height -
67+
previousRenderer.configView.y
68+
)
69+
);
70+
} else if (
71+
previousRenderer.flamegraph.leftHeavy !== renderer.flamegraph.leftHeavy
72+
) {
73+
/*
74+
* When the user toggles left heavy, the entire flamegraph will take
75+
* on a different shape. In this case, there's no obvious position
76+
* that can be carried over.
77+
*/
78+
} else {
79+
renderer.setConfigView(previousRenderer.configView);
80+
}
5581
}
5682

5783
return renderer;

static/app/types/profiling.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ namespace Profiling {
5656
type Schema = {
5757
name: string;
5858
activeProfileIndex?: number;
59+
duration_ns: number;
5960
profiles: ReadonlyArray<ProfileTypes>;
6061
shared: {
6162
frames: ReadonlyArray<Omit<FrameInfo, 'key'>>;

0 commit comments

Comments
 (0)