Skip to content

Commit 8382c67

Browse files
authored
feat(profiling) aggregate flamegraph on landing page (#75190)
Add aggregate flamegraph to the continuous profiling landing page
1 parent 3dc95d7 commit 8382c67

File tree

2 files changed

+317
-0
lines changed

2 files changed

+317
-0
lines changed

static/app/views/profiling/content.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import {decodeScalar} from 'sentry/utils/queryString';
3636
import useOrganization from 'sentry/utils/useOrganization';
3737
import usePageFilters from 'sentry/utils/usePageFilters';
3838
import useProjects from 'sentry/utils/useProjects';
39+
import {LandingAggregateFlamegraph} from 'sentry/views/profiling/landingAggregateFlamegraph';
3940
import {DEFAULT_PROFILING_DATETIME_SELECTION} from 'sentry/views/profiling/utils';
4041

4142
import {LandingWidgetSelector} from './landing/landingWidgetSelector';
@@ -396,6 +397,9 @@ function ProfilingContent({location}: ProfilingContentProps) {
396397
maxQueryLength={MAX_QUERY_LENGTH}
397398
/>
398399
</ActionBar>
400+
<LandingAggregateFlamegraphContainer>
401+
<LandingAggregateFlamegraph />
402+
</LandingAggregateFlamegraphContainer>
399403
{shouldShowProfilingOnboardingPanel ? (
400404
<Fragment>
401405
<ProfilingOnboardingPanel
@@ -510,6 +514,15 @@ const ALL_FIELDS = [
510514

511515
type FieldType = (typeof ALL_FIELDS)[number];
512516

517+
const LandingAggregateFlamegraphContainer = styled('div')`
518+
height: 40vh;
519+
min-height: 300px;
520+
position: relative;
521+
border: 1px solid ${p => p.theme.border};
522+
border-radius: ${p => p.theme.borderRadius};
523+
margin-bottom: ${space(2)};
524+
`;
525+
513526
const StyledHeaderContent = styled(Layout.HeaderContent)`
514527
display: flex;
515528
align-items: center;
Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
import {useCallback, useEffect, useMemo, useState} from 'react';
2+
import styled from '@emotion/styled';
3+
4+
import {Button} from 'sentry/components/button';
5+
import {CompactSelect} from 'sentry/components/compactSelect';
6+
import type {SelectOption} from 'sentry/components/compactSelect/types';
7+
import LoadingIndicator from 'sentry/components/loadingIndicator';
8+
import {AggregateFlamegraph} from 'sentry/components/profiling/flamegraph/aggregateFlamegraph';
9+
import {AggregateFlamegraphTreeTable} from 'sentry/components/profiling/flamegraph/aggregateFlamegraphTreeTable';
10+
import {FlamegraphSearch} from 'sentry/components/profiling/flamegraph/flamegraphToolbar/flamegraphSearch';
11+
import {SegmentedControl} from 'sentry/components/segmentedControl';
12+
import {IconPanel} from 'sentry/icons';
13+
import {t} from 'sentry/locale';
14+
import {space} from 'sentry/styles/space';
15+
import type {DeepPartial} from 'sentry/types/utils';
16+
import {isAggregateField} from 'sentry/utils/discover/fields';
17+
import type {CanvasScheduler} from 'sentry/utils/profiling/canvasScheduler';
18+
import {
19+
CanvasPoolManager,
20+
useCanvasScheduler,
21+
} from 'sentry/utils/profiling/canvasScheduler';
22+
import type {FlamegraphState} from 'sentry/utils/profiling/flamegraph/flamegraphStateProvider/flamegraphContext';
23+
import {FlamegraphStateProvider} from 'sentry/utils/profiling/flamegraph/flamegraphStateProvider/flamegraphContextProvider';
24+
import {FlamegraphThemeProvider} from 'sentry/utils/profiling/flamegraph/flamegraphThemeProvider';
25+
import type {Frame} from 'sentry/utils/profiling/frame';
26+
import {useAggregateFlamegraphQuery} from 'sentry/utils/profiling/hooks/useAggregateFlamegraphQuery';
27+
import {decodeScalar} from 'sentry/utils/queryString';
28+
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
29+
import {useLocalStorageState} from 'sentry/utils/useLocalStorageState';
30+
import {useLocation} from 'sentry/utils/useLocation';
31+
import {
32+
FlamegraphProvider,
33+
useFlamegraph,
34+
} from 'sentry/views/profiling/flamegraphProvider';
35+
import {ProfileGroupProvider} from 'sentry/views/profiling/profileGroupProvider';
36+
37+
const DEFAULT_FLAMEGRAPH_PREFERENCES: DeepPartial<FlamegraphState> = {
38+
preferences: {
39+
sorting: 'alphabetical' satisfies FlamegraphState['preferences']['sorting'],
40+
},
41+
};
42+
43+
const noop = () => void 0;
44+
45+
function decodeViewOrDefault(
46+
value: string | string[] | null | undefined,
47+
defaultValue: 'flamegraph' | 'profiles'
48+
): 'flamegraph' | 'profiles' {
49+
if (!value || Array.isArray(value)) {
50+
return defaultValue;
51+
}
52+
if (value === 'flamegraph' || value === 'profiles') {
53+
return value;
54+
}
55+
return defaultValue;
56+
}
57+
58+
interface AggregateFlamegraphToolbarProps {
59+
canvasPoolManager: CanvasPoolManager;
60+
frameFilter: 'system' | 'application' | 'all';
61+
hideSystemFrames: boolean;
62+
onFrameFilterChange: (value: 'system' | 'application' | 'all') => void;
63+
onHideRegressionsClick: () => void;
64+
onVisualizationChange: (value: 'flamegraph' | 'call tree') => void;
65+
scheduler: CanvasScheduler;
66+
setHideSystemFrames: (value: boolean) => void;
67+
visualization: 'flamegraph' | 'call tree';
68+
}
69+
70+
function AggregateFlamegraphToolbar(props: AggregateFlamegraphToolbarProps) {
71+
const flamegraph = useFlamegraph();
72+
const flamegraphs = useMemo(() => [flamegraph], [flamegraph]);
73+
const spans = useMemo(() => [], []);
74+
75+
const frameSelectOptions: SelectOption<'system' | 'application' | 'all'>[] =
76+
useMemo(() => {
77+
return [
78+
{value: 'system', label: t('System Frames')},
79+
{value: 'application', label: t('Application Frames')},
80+
{value: 'all', label: t('All Frames')},
81+
];
82+
}, []);
83+
84+
const onResetZoom = useCallback(() => {
85+
props.scheduler.dispatch('reset zoom');
86+
}, [props.scheduler]);
87+
88+
const onFrameFilterChange = useCallback(
89+
(value: {value: 'application' | 'system' | 'all'}) => {
90+
props.onFrameFilterChange(value.value);
91+
},
92+
[props]
93+
);
94+
95+
return (
96+
<AggregateFlamegraphToolbarContainer>
97+
<ViewSelectContainer>
98+
<SegmentedControl
99+
aria-label={t('View')}
100+
size="xs"
101+
value={props.visualization}
102+
onChange={props.onVisualizationChange}
103+
>
104+
<SegmentedControl.Item key="flamegraph">
105+
{t('Flamegraph')}
106+
</SegmentedControl.Item>
107+
<SegmentedControl.Item key="call tree">{t('Call Tree')}</SegmentedControl.Item>
108+
</SegmentedControl>
109+
</ViewSelectContainer>
110+
<AggregateFlamegraphSearch
111+
spans={spans}
112+
canvasPoolManager={props.canvasPoolManager}
113+
flamegraphs={flamegraphs}
114+
/>
115+
<Button size="xs" onClick={onResetZoom}>
116+
{t('Reset Zoom')}
117+
</Button>
118+
<CompactSelect
119+
size="xs"
120+
onChange={onFrameFilterChange}
121+
value={props.frameFilter}
122+
options={frameSelectOptions}
123+
/>
124+
<Button
125+
size="xs"
126+
onClick={props.onHideRegressionsClick}
127+
title={t('Expand or collapse the view')}
128+
>
129+
<IconPanel size="xs" direction="right" />
130+
</Button>
131+
</AggregateFlamegraphToolbarContainer>
132+
);
133+
}
134+
135+
export function LandingAggregateFlamegraph(): React.ReactNode {
136+
const location = useLocation();
137+
const rawQuery = decodeScalar(location?.query?.query, '');
138+
139+
const query = useMemo(() => {
140+
const search = new MutableSearch(rawQuery);
141+
// there are no aggregations happening on this page,
142+
// so remove any aggregate filters
143+
Object.keys(search.filters).forEach(field => {
144+
if (isAggregateField(field)) {
145+
search.removeFilter(field);
146+
}
147+
});
148+
return search.formatString();
149+
}, [rawQuery]);
150+
151+
const {data, isLoading, isError} = useAggregateFlamegraphQuery({
152+
query,
153+
});
154+
155+
const [visualization, setVisualization] = useLocalStorageState<
156+
'flamegraph' | 'call tree'
157+
>('flamegraph-visualization', 'flamegraph');
158+
159+
const onVisualizationChange = useCallback(
160+
(value: 'flamegraph' | 'call tree') => {
161+
setVisualization(value);
162+
},
163+
[setVisualization]
164+
);
165+
166+
const [hideRegressions, setHideRegressions] = useLocalStorageState<boolean>(
167+
'flamegraph-hide-regressions',
168+
false
169+
);
170+
const [frameFilter, setFrameFilter] = useLocalStorageState<
171+
'system' | 'application' | 'all'
172+
>('flamegraph-frame-filter', 'application');
173+
174+
const onFrameFilterChange = useCallback(
175+
(value: 'system' | 'application' | 'all') => {
176+
setFrameFilter(value);
177+
},
178+
[setFrameFilter]
179+
);
180+
181+
const flamegraphFrameFilter: ((frame: Frame) => boolean) | undefined = useMemo(() => {
182+
if (frameFilter === 'all') {
183+
return () => true;
184+
}
185+
if (frameFilter === 'application') {
186+
return frame => frame.is_application;
187+
}
188+
return frame => !frame.is_application;
189+
}, [frameFilter]);
190+
191+
const canvasPoolManager = useMemo(() => new CanvasPoolManager(), []);
192+
const scheduler = useCanvasScheduler(canvasPoolManager);
193+
194+
const [view, setView] = useState<'flamegraph' | 'profiles'>(
195+
decodeViewOrDefault(location.query.view, 'flamegraph')
196+
);
197+
198+
useEffect(() => {
199+
const newView = decodeViewOrDefault(location.query.view, 'flamegraph');
200+
if (newView !== view) {
201+
setView(decodeViewOrDefault(location.query.view, 'flamegraph'));
202+
}
203+
}, [location.query.view, view]);
204+
205+
const onHideRegressionsClick = useCallback(() => {
206+
return setHideRegressions(!hideRegressions);
207+
}, [hideRegressions, setHideRegressions]);
208+
209+
return (
210+
<ProfileGroupProvider
211+
traceID=""
212+
type="flamegraph"
213+
input={data ?? null}
214+
frameFilter={flamegraphFrameFilter}
215+
>
216+
<FlamegraphStateProvider initialState={DEFAULT_FLAMEGRAPH_PREFERENCES}>
217+
<FlamegraphThemeProvider>
218+
<FlamegraphProvider>
219+
<AggregateFlamegraphContainer>
220+
<AggregateFlamegraphToolbar
221+
scheduler={scheduler}
222+
canvasPoolManager={canvasPoolManager}
223+
visualization={visualization}
224+
onVisualizationChange={onVisualizationChange}
225+
frameFilter={frameFilter}
226+
onFrameFilterChange={onFrameFilterChange}
227+
hideSystemFrames={false}
228+
setHideSystemFrames={noop}
229+
onHideRegressionsClick={onHideRegressionsClick}
230+
/>
231+
{isLoading ? (
232+
<RequestStateMessageContainer>
233+
<LoadingIndicator />
234+
</RequestStateMessageContainer>
235+
) : isError ? (
236+
<RequestStateMessageContainer>
237+
{t('There was an error loading the flamegraph.')}
238+
</RequestStateMessageContainer>
239+
) : null}
240+
{visualization === 'flamegraph' ? (
241+
<AggregateFlamegraph
242+
canvasPoolManager={canvasPoolManager}
243+
scheduler={scheduler}
244+
/>
245+
) : (
246+
<AggregateFlamegraphTreeTable
247+
recursion={null}
248+
expanded={false}
249+
frameFilter={frameFilter}
250+
canvasPoolManager={canvasPoolManager}
251+
/>
252+
)}
253+
</AggregateFlamegraphContainer>
254+
</FlamegraphProvider>
255+
</FlamegraphThemeProvider>
256+
</FlamegraphStateProvider>
257+
</ProfileGroupProvider>
258+
);
259+
}
260+
261+
const AggregateFlamegraphSearch = styled(FlamegraphSearch)`
262+
max-width: 300px;
263+
`;
264+
265+
const AggregateFlamegraphToolbarContainer = styled('div')`
266+
display: flex;
267+
justify-content: space-between;
268+
gap: ${space(1)};
269+
padding: ${space(1)} ${space(1)};
270+
/*
271+
force height to be the same as profile digest header,
272+
but subtract 1px for the border that doesnt exist on the header
273+
*/
274+
height: 41px;
275+
border-bottom: 1px solid ${p => p.theme.border};
276+
`;
277+
278+
const ViewSelectContainer = styled('div')`
279+
min-width: 160px;
280+
`;
281+
282+
const RequestStateMessageContainer = styled('div')`
283+
position: absolute;
284+
left: 0;
285+
right: 0;
286+
top: 0;
287+
bottom: 0;
288+
display: flex;
289+
justify-content: center;
290+
align-items: center;
291+
color: ${p => p.theme.subText};
292+
`;
293+
294+
const AggregateFlamegraphContainer = styled('div')`
295+
display: flex;
296+
flex-direction: column;
297+
flex: 1 1 100%;
298+
height: 100%;
299+
width: 100%;
300+
overflow: hidden;
301+
position: absolute;
302+
left: 0px;
303+
top: 0px;
304+
`;

0 commit comments

Comments
 (0)