landingAggregateFlamegraph.tsx 10.0 KB

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