landingAggregateFlamegraph.tsx 9.6 KB

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