frameStack.tsx 11 KB

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