landingAggregateFlamegraph.tsx 9.6 KB

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