landingAggregateFlamegraph.tsx 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  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, isLoading, isError} = 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 flamegraphFrameFilter: ((frame: Frame) => boolean) | undefined = useMemo(() => {
  149. if (frameFilter === 'all') {
  150. return () => true;
  151. }
  152. if (frameFilter === 'application') {
  153. return frame => frame.is_application;
  154. }
  155. return frame => !frame.is_application;
  156. }, [frameFilter]);
  157. const canvasPoolManager = useMemo(() => new CanvasPoolManager(), []);
  158. const scheduler = useCanvasScheduler(canvasPoolManager);
  159. const [view, setView] = useState<'flamegraph' | 'profiles'>(
  160. decodeViewOrDefault(location.query.view, 'flamegraph')
  161. );
  162. useEffect(() => {
  163. const newView = decodeViewOrDefault(location.query.view, 'flamegraph');
  164. if (newView !== view) {
  165. setView(decodeViewOrDefault(location.query.view, 'flamegraph'));
  166. }
  167. }, [location.query.view, view]);
  168. const onHideRegressionsClick = useCallback(() => {
  169. return setHideRegressions(!hideRegressions);
  170. }, [hideRegressions, setHideRegressions]);
  171. return (
  172. <ProfileGroupProvider
  173. traceID=""
  174. type="flamegraph"
  175. input={data ?? null}
  176. frameFilter={flamegraphFrameFilter}
  177. >
  178. <FlamegraphStateProvider initialState={DEFAULT_FLAMEGRAPH_PREFERENCES}>
  179. <FlamegraphThemeProvider>
  180. <FlamegraphProvider>
  181. <AggregateFlamegraphContainer>
  182. <AggregateFlamegraphToolbar
  183. scheduler={scheduler}
  184. canvasPoolManager={canvasPoolManager}
  185. visualization={visualization}
  186. onVisualizationChange={onVisualizationChange}
  187. frameFilter={frameFilter}
  188. onFrameFilterChange={onFrameFilterChange}
  189. hideSystemFrames={false}
  190. setHideSystemFrames={noop}
  191. onHideRegressionsClick={onHideRegressionsClick}
  192. />
  193. {isLoading ? (
  194. <RequestStateMessageContainer>
  195. <LoadingIndicator />
  196. </RequestStateMessageContainer>
  197. ) : isError ? (
  198. <RequestStateMessageContainer>
  199. {t('There was an error loading the flamegraph.')}
  200. </RequestStateMessageContainer>
  201. ) : null}
  202. {visualization === 'flamegraph' ? (
  203. <AggregateFlamegraph
  204. canvasPoolManager={canvasPoolManager}
  205. scheduler={scheduler}
  206. />
  207. ) : (
  208. <AggregateFlamegraphTreeTable
  209. recursion={null}
  210. expanded={false}
  211. withoutBorders
  212. frameFilter={frameFilter}
  213. canvasPoolManager={canvasPoolManager}
  214. />
  215. )}
  216. </AggregateFlamegraphContainer>
  217. </FlamegraphProvider>
  218. </FlamegraphThemeProvider>
  219. </FlamegraphStateProvider>
  220. </ProfileGroupProvider>
  221. );
  222. }
  223. const AggregateFlamegraphSearch = styled(FlamegraphSearch)`
  224. max-width: 300px;
  225. `;
  226. const AggregateFlamegraphToolbarContainer = styled('div')`
  227. display: flex;
  228. justify-content: space-between;
  229. gap: ${space(1)};
  230. padding: ${space(1)} ${space(1)};
  231. /*
  232. force height to be the same as profile digest header,
  233. but subtract 1px for the border that doesnt exist on the header
  234. */
  235. height: 41px;
  236. border-bottom: 1px solid ${p => p.theme.border};
  237. `;
  238. const ViewSelectContainer = styled('div')`
  239. min-width: 160px;
  240. `;
  241. const RequestStateMessageContainer = styled('div')`
  242. position: absolute;
  243. left: 0;
  244. right: 0;
  245. top: 0;
  246. bottom: 0;
  247. display: flex;
  248. justify-content: center;
  249. align-items: center;
  250. color: ${p => p.theme.subText};
  251. `;
  252. const AggregateFlamegraphContainer = styled('div')`
  253. display: flex;
  254. flex-direction: column;
  255. flex: 1 1 100%;
  256. height: 100%;
  257. width: 100%;
  258. overflow: hidden;
  259. position: absolute;
  260. left: 0px;
  261. top: 0px;
  262. `;