frameStack.tsx 12 KB

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