frameStack.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425
  1. import {memo, MouseEventHandler, useCallback, useMemo, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import Button from 'sentry/components/button';
  4. import {ExportProfileButton} from 'sentry/components/profiling/exportProfileButton';
  5. import {IconPanel} from 'sentry/icons';
  6. import {t} from 'sentry/locale';
  7. import space from 'sentry/styles/space';
  8. import {CanvasPoolManager} from 'sentry/utils/profiling/canvasScheduler';
  9. import {filterFlamegraphTree} from 'sentry/utils/profiling/filterFlamegraphTree';
  10. import {Flamegraph} from 'sentry/utils/profiling/flamegraph';
  11. import {FlamegraphPreferences} from 'sentry/utils/profiling/flamegraph/flamegraphStateProvider/reducers/flamegraphPreferences';
  12. import {useFlamegraphPreferences} from 'sentry/utils/profiling/flamegraph/hooks/useFlamegraphPreferences';
  13. import {useDispatchFlamegraphState} from 'sentry/utils/profiling/flamegraph/hooks/useFlamegraphState';
  14. import {FlamegraphFrame} from 'sentry/utils/profiling/flamegraphFrame';
  15. import {ProfileGroup} from 'sentry/utils/profiling/profile/importProfile';
  16. import {invertCallTree} from 'sentry/utils/profiling/profile/utils';
  17. import {useParams} from 'sentry/utils/useParams';
  18. import {FrameStackTable} from './frameStackTable';
  19. import {ProfileDetails} from './profileDetails';
  20. interface FrameStackProps {
  21. canvasPoolManager: CanvasPoolManager;
  22. flamegraph: Flamegraph;
  23. formatDuration: Flamegraph['formatter'];
  24. getFrameColor: (frame: FlamegraphFrame) => string;
  25. profileGroup: ProfileGroup;
  26. referenceNode: FlamegraphFrame;
  27. rootNodes: FlamegraphFrame[];
  28. onResize?: MouseEventHandler<HTMLElement>;
  29. }
  30. const FrameStack = memo(function FrameStack(props: FrameStackProps) {
  31. const params = useParams();
  32. const flamegraphPreferences = useFlamegraphPreferences();
  33. const dispatch = useDispatchFlamegraphState();
  34. const [tab, setTab] = useState<'bottom up' | 'call order'>('call order');
  35. const [treeType, setTreeType] = useState<'all' | 'application' | 'system'>('all');
  36. const [recursion, setRecursion] = useState<'collapsed' | null>(null);
  37. const maybeFilteredOrInvertedTree: FlamegraphFrame[] | null = useMemo(() => {
  38. const skipFunction: (f: FlamegraphFrame) => boolean =
  39. treeType === 'application'
  40. ? f => !f.frame.is_application
  41. : treeType === 'system'
  42. ? f => f.frame.is_application
  43. : () => false;
  44. const maybeFilteredRoots =
  45. treeType !== 'all'
  46. ? filterFlamegraphTree(props.rootNodes, skipFunction)
  47. : props.rootNodes;
  48. if (tab === 'call order') {
  49. return maybeFilteredRoots;
  50. }
  51. return invertCallTree(maybeFilteredRoots);
  52. }, [tab, treeType, props.rootNodes]);
  53. const handleRecursionChange = useCallback(
  54. (evt: React.ChangeEvent<HTMLInputElement>) => {
  55. setRecursion(evt.currentTarget.checked ? 'collapsed' : null);
  56. },
  57. []
  58. );
  59. const onBottomUpClick = useCallback(() => {
  60. setTab('bottom up');
  61. }, []);
  62. const onCallOrderClick = useCallback(() => {
  63. setTab('call order');
  64. }, []);
  65. const onAllApplicationsClick = useCallback(() => {
  66. setTreeType('all');
  67. }, []);
  68. const onApplicationsClick = useCallback(() => {
  69. setTreeType('application');
  70. }, []);
  71. const onSystemsClick = useCallback(() => {
  72. setTreeType('system');
  73. }, []);
  74. const onTableLeftClick = useCallback(() => {
  75. dispatch({type: 'set layout', payload: 'table left'});
  76. }, [dispatch]);
  77. const onTableBottomClick = useCallback(() => {
  78. dispatch({type: 'set layout', payload: 'table bottom'});
  79. }, [dispatch]);
  80. const onTableRightClick = useCallback(() => {
  81. dispatch({type: 'set layout', payload: 'table right'});
  82. }, [dispatch]);
  83. return (
  84. <FrameDrawer layout={flamegraphPreferences.layout}>
  85. <ProfilingDetailsFrameTabs>
  86. <ProfilingDetailsListItem className={tab === 'bottom up' ? 'active' : undefined}>
  87. <Button
  88. data-title={t('Bottom Up')}
  89. priority="link"
  90. size="zero"
  91. onClick={onBottomUpClick}
  92. >
  93. {t('Bottom Up')}
  94. </Button>
  95. </ProfilingDetailsListItem>
  96. <ProfilingDetailsListItem
  97. margin="none"
  98. className={tab === 'call order' ? 'active' : undefined}
  99. >
  100. <Button
  101. data-title={t('Call Order')}
  102. priority="link"
  103. size="zero"
  104. onClick={onCallOrderClick}
  105. >
  106. {t('Call Order')}
  107. </Button>
  108. </ProfilingDetailsListItem>
  109. <Separator />
  110. <ProfilingDetailsListItem className={treeType === 'all' ? 'active' : undefined}>
  111. <Button
  112. data-title={t('All Frames')}
  113. priority="link"
  114. size="zero"
  115. onClick={onAllApplicationsClick}
  116. >
  117. {t('All Frames')}
  118. </Button>
  119. </ProfilingDetailsListItem>
  120. <ProfilingDetailsListItem
  121. className={treeType === 'application' ? 'active' : undefined}
  122. >
  123. <Button
  124. data-title={t('Application Frames')}
  125. priority="link"
  126. size="zero"
  127. onClick={onApplicationsClick}
  128. >
  129. {t('Application Frames')}
  130. </Button>
  131. </ProfilingDetailsListItem>
  132. <ProfilingDetailsListItem
  133. margin="none"
  134. className={treeType === 'system' ? 'active' : undefined}
  135. >
  136. <Button
  137. data-title={t('System Frames')}
  138. priority="link"
  139. size="zero"
  140. onClick={onSystemsClick}
  141. >
  142. {t('System Frames')}
  143. </Button>
  144. </ProfilingDetailsListItem>
  145. <Separator />
  146. <ProfilingDetailsListItem>
  147. <FrameDrawerLabel>
  148. <input
  149. type="checkbox"
  150. checked={recursion === 'collapsed'}
  151. onChange={handleRecursionChange}
  152. />
  153. {t('Collapse recursion')}
  154. </FrameDrawerLabel>
  155. </ProfilingDetailsListItem>
  156. <ProfilingDetailsListItem
  157. style={{
  158. flex: '1 1 100%',
  159. cursor:
  160. flamegraphPreferences.layout === 'table bottom' ? 'ns-resize' : undefined,
  161. }}
  162. onMouseDown={
  163. flamegraphPreferences.layout === 'table bottom' ? props.onResize : undefined
  164. }
  165. />
  166. <ProfilingDetailsListItem margin="none">
  167. <ExportProfileButton
  168. variant="xs"
  169. eventId={params.eventId}
  170. orgId={params.orgId}
  171. projectId={params.projectId}
  172. disabled={
  173. params.eventId === undefined ||
  174. params.orgId === undefined ||
  175. params.projectId === undefined
  176. }
  177. />
  178. </ProfilingDetailsListItem>
  179. <Separator />
  180. <ProfilingDetailsListItem>
  181. <LayoutSelectionContainer>
  182. <StyledButton
  183. active={flamegraphPreferences.layout === 'table left'}
  184. onClick={onTableLeftClick}
  185. size="xs"
  186. title={t('Table left')}
  187. >
  188. <IconPanel size="xs" direction="left" />
  189. </StyledButton>
  190. <StyledButton
  191. active={flamegraphPreferences.layout === 'table bottom'}
  192. onClick={onTableBottomClick}
  193. size="xs"
  194. title={t('Table bottom')}
  195. >
  196. <IconPanel size="xs" direction="down" />
  197. </StyledButton>
  198. <StyledButton
  199. active={flamegraphPreferences.layout === 'table right'}
  200. onClick={onTableRightClick}
  201. size="xs"
  202. title={t('Table right')}
  203. >
  204. <IconPanel size="xs" direction="right" />
  205. </StyledButton>
  206. </LayoutSelectionContainer>
  207. </ProfilingDetailsListItem>
  208. </ProfilingDetailsFrameTabs>
  209. <FrameStackTable
  210. {...props}
  211. expanded={tab === 'call order'}
  212. recursion={recursion}
  213. flamegraph={props.flamegraph}
  214. referenceNode={props.referenceNode}
  215. tree={maybeFilteredOrInvertedTree ?? []}
  216. canvasPoolManager={props.canvasPoolManager}
  217. />
  218. <ProfileDetails profileGroup={props.profileGroup} />
  219. {flamegraphPreferences.layout === 'table left' ||
  220. flamegraphPreferences.layout === 'table right' ? (
  221. <ResizableVerticalDrawer>
  222. {/* The border should be 1px, but we want the actual handler to be wider
  223. to improve the user experience and not have users have to click on the exact pixel */}
  224. <InvisibleHandler onMouseDown={props.onResize} />
  225. </ResizableVerticalDrawer>
  226. ) : null}
  227. </FrameDrawer>
  228. );
  229. });
  230. const ResizableVerticalDrawer = styled('div')`
  231. width: 1px;
  232. grid-area: drawer;
  233. background-color: ${p => p.theme.border};
  234. position: relative;
  235. `;
  236. const InvisibleHandler = styled('div')`
  237. opacity: 0;
  238. width: ${space(1)};
  239. position: absolute;
  240. inset: 0;
  241. cursor: ew-resize;
  242. transform: translateX(-50%);
  243. background-color: transparent;
  244. `;
  245. const FrameDrawerLabel = styled('label')`
  246. display: flex;
  247. align-items: center;
  248. white-space: nowrap;
  249. margin-bottom: 0;
  250. height: 100%;
  251. font-weight: normal;
  252. > input {
  253. margin: 0 ${space(0.5)} 0 0;
  254. }
  255. `;
  256. // Linter produces a false positive for the grid layout. I did not manage to find out
  257. // how to "fix it" or why it is not working, I imagine it could be due to the ternary?
  258. const FrameDrawer = styled('div')<{layout: FlamegraphPreferences['layout']}>`
  259. display: grid;
  260. grid-template-rows: auto 1fr;
  261. grid-template-columns: ${({layout}) =>
  262. layout === 'table left' ? '1fr auto' : layout === 'table right' ? 'auto 1fr' : '1fr'};
  263. /* false positive for grid layout */
  264. /* stylelint-disable */
  265. grid-template-areas: ${({layout}) =>
  266. layout === 'table bottom'
  267. ? `
  268. 'tabs tabs'
  269. 'table details'
  270. 'drawer drawer'
  271. `
  272. : layout === 'table left'
  273. ? `
  274. 'tabs tabs drawer'
  275. 'table table drawer'
  276. 'details details drawer';
  277. `
  278. : `
  279. 'drawer tabs tabs'
  280. 'drawer table table'
  281. 'drawer details details';
  282. `};
  283. `;
  284. const Separator = styled('li')`
  285. width: 1px;
  286. height: 66%;
  287. margin: 0 ${space(1)};
  288. background: 1px solid ${p => p.theme.border};
  289. transform: translateY(29%);
  290. `;
  291. export const ProfilingDetailsFrameTabs = styled('ul')`
  292. display: flex;
  293. list-style-type: none;
  294. padding: 0 ${space(1)};
  295. margin: 0;
  296. border-top: 1px solid ${prop => prop.theme.border};
  297. background-color: ${props => props.theme.surface100};
  298. user-select: none;
  299. grid-area: tabs;
  300. `;
  301. export const ProfilingDetailsListItem = styled('li')<{
  302. margin?: 'none';
  303. size?: 'sm';
  304. }>`
  305. font-size: ${p => p.theme.fontSizeSmall};
  306. margin-right: ${p => (p.margin === 'none' ? 0 : space(1))};
  307. button {
  308. border: none;
  309. border-top: 2px solid transparent;
  310. border-bottom: 2px solid transparent;
  311. border-radius: 0;
  312. margin: 0;
  313. padding: ${p => (p.size === 'sm' ? space(0.25) : space(0.5))} 0;
  314. color: ${p => p.theme.textColor};
  315. max-height: ${p => (p.size === 'sm' ? '24px' : 'auto')};
  316. &::after {
  317. display: block;
  318. content: attr(data-title);
  319. font-weight: bold;
  320. height: 1px;
  321. color: transparent;
  322. overflow: hidden;
  323. visibility: hidden;
  324. white-space: nowrap;
  325. }
  326. &:hover {
  327. color: ${p => p.theme.textColor};
  328. }
  329. }
  330. &.active button {
  331. font-weight: bold;
  332. border-bottom: 2px solid ${prop => prop.theme.active};
  333. }
  334. `;
  335. const StyledButton = styled(Button)<{active: boolean}>`
  336. border: none;
  337. background-color: transparent;
  338. box-shadow: none;
  339. transition: none !important;
  340. opacity: ${p => (p.active ? 0.7 : 0.5)};
  341. &:not(:last-child) {
  342. margin-right: ${space(1)};
  343. }
  344. &:hover {
  345. border: none;
  346. background-color: transparent;
  347. box-shadow: none;
  348. opacity: ${p => (p.active ? 0.6 : 0.5)};
  349. }
  350. `;
  351. const LayoutSelectionContainer = styled('div')`
  352. display: flex;
  353. align-items: center;
  354. `;
  355. const FRAME_WEIGHT_CELL_WIDTH_PX = 164;
  356. export const FrameCallersTableCell = styled('div')<{
  357. isSelected?: boolean;
  358. noPadding?: boolean;
  359. textAlign?: React.CSSProperties['textAlign'];
  360. }>`
  361. width: ${FRAME_WEIGHT_CELL_WIDTH_PX}px;
  362. position: relative;
  363. white-space: nowrap;
  364. flex-shrink: 0;
  365. padding: 0 ${p => (p.noPadding ? 0 : space(1))} 0 0;
  366. text-align: ${p => p.textAlign ?? 'initial'};
  367. &:first-child,
  368. &:nth-child(2) {
  369. position: sticky;
  370. z-index: 1;
  371. background-color: ${p => (p.isSelected ? p.theme.blue300 : p.theme.background)};
  372. }
  373. &:first-child {
  374. left: 0;
  375. }
  376. &:nth-child(2) {
  377. left: ${FRAME_WEIGHT_CELL_WIDTH_PX}px;
  378. }
  379. &:not(:last-child) {
  380. border-right: 1px solid ${p => p.theme.border};
  381. }
  382. `;
  383. export {FrameStack};