frameStackTable.tsx 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  1. import {useCallback, useMemo, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import {IconArrow} from 'sentry/icons';
  4. import {t} from 'sentry/locale';
  5. import space from 'sentry/styles/space';
  6. import {CanvasPoolManager} from 'sentry/utils/profiling/canvasScheduler';
  7. import {Flamegraph} from 'sentry/utils/profiling/flamegraph';
  8. import {FlamegraphFrame} from 'sentry/utils/profiling/flamegraphFrame';
  9. import {useContextMenu} from 'sentry/utils/profiling/hooks/useContextMenu';
  10. import {
  11. UseVirtualizedListProps,
  12. useVirtualizedTree,
  13. } from 'sentry/utils/profiling/hooks/useVirtualizedTree/useVirtualizedTree';
  14. import {VirtualizedTreeNode} from 'sentry/utils/profiling/hooks/useVirtualizedTree/VirtualizedTreeNode';
  15. import {FrameCallersTableCell} from './frameStack';
  16. import {FrameStackContextMenu} from './frameStackContextMenu';
  17. import {FrameStackTableRow} from './frameStackTableRow';
  18. function makeSortFunction(
  19. property: 'total weight' | 'self weight' | 'name',
  20. direction: 'asc' | 'desc'
  21. ) {
  22. if (property === 'total weight') {
  23. return direction === 'desc'
  24. ? (
  25. a: VirtualizedTreeNode<FlamegraphFrame>,
  26. b: VirtualizedTreeNode<FlamegraphFrame>
  27. ) => {
  28. return b.node.node.totalWeight - a.node.node.totalWeight;
  29. }
  30. : (
  31. a: VirtualizedTreeNode<FlamegraphFrame>,
  32. b: VirtualizedTreeNode<FlamegraphFrame>
  33. ) => {
  34. return a.node.node.totalWeight - b.node.node.totalWeight;
  35. };
  36. }
  37. if (property === 'self weight') {
  38. return direction === 'desc'
  39. ? (
  40. a: VirtualizedTreeNode<FlamegraphFrame>,
  41. b: VirtualizedTreeNode<FlamegraphFrame>
  42. ) => {
  43. return b.node.node.selfWeight - a.node.node.selfWeight;
  44. }
  45. : (
  46. a: VirtualizedTreeNode<FlamegraphFrame>,
  47. b: VirtualizedTreeNode<FlamegraphFrame>
  48. ) => {
  49. return a.node.node.selfWeight - b.node.node.selfWeight;
  50. };
  51. }
  52. if (property === 'name') {
  53. return direction === 'desc'
  54. ? (
  55. a: VirtualizedTreeNode<FlamegraphFrame>,
  56. b: VirtualizedTreeNode<FlamegraphFrame>
  57. ) => {
  58. return a.node.frame.name.localeCompare(b.node.frame.name);
  59. }
  60. : (
  61. a: VirtualizedTreeNode<FlamegraphFrame>,
  62. b: VirtualizedTreeNode<FlamegraphFrame>
  63. ) => {
  64. return b.node.frame.name.localeCompare(a.node.frame.name);
  65. };
  66. }
  67. throw new Error(`Unknown sort property ${property}`);
  68. }
  69. function skipRecursiveNodes(n: VirtualizedTreeNode<FlamegraphFrame>): boolean {
  70. return n.node.node.isDirectRecursive();
  71. }
  72. interface FrameStackTableProps {
  73. canvasPoolManager: CanvasPoolManager;
  74. formatDuration: Flamegraph['formatter'];
  75. getFrameColor: (frame: FlamegraphFrame) => string;
  76. recursion: 'collapsed' | null;
  77. referenceNode: FlamegraphFrame;
  78. tree: FlamegraphFrame[];
  79. }
  80. export function FrameStackTable({
  81. tree,
  82. referenceNode,
  83. canvasPoolManager,
  84. getFrameColor,
  85. formatDuration,
  86. recursion,
  87. }: FrameStackTableProps) {
  88. const [scrollContainerRef, setScrollContainerRef] = useState<HTMLDivElement | null>(
  89. null
  90. );
  91. const [sort, setSort] = useState<'total weight' | 'self weight' | 'name'>(
  92. 'total weight'
  93. );
  94. const [direction, setDirection] = useState<'asc' | 'desc'>('desc');
  95. const sortFunction = useMemo(() => {
  96. return makeSortFunction(sort, direction);
  97. }, [sort, direction]);
  98. const [clickedContextMenuNode, setClickedContextMenuClose] =
  99. useState<VirtualizedTreeNode<FlamegraphFrame> | null>(null);
  100. const contextMenu = useContextMenu({container: scrollContainerRef});
  101. const handleZoomIntoFrameClick = useCallback(() => {
  102. if (!clickedContextMenuNode) {
  103. return;
  104. }
  105. canvasPoolManager.dispatch('zoom at frame', [clickedContextMenuNode.node, 'exact']);
  106. canvasPoolManager.dispatch('highlight frame', [
  107. clickedContextMenuNode.node,
  108. 'selected',
  109. ]);
  110. }, [canvasPoolManager, clickedContextMenuNode]);
  111. const renderRow: UseVirtualizedListProps<FlamegraphFrame>['renderRow'] = useCallback(
  112. (
  113. r,
  114. {
  115. handleRowClick,
  116. handleRowMouseEnter,
  117. handleExpandTreeNode,
  118. handleRowKeyDown,
  119. tabIndexKey,
  120. }
  121. ) => {
  122. return (
  123. <FrameStackTableRow
  124. ref={n => {
  125. r.ref = n;
  126. }}
  127. node={r.item}
  128. style={r.styles}
  129. referenceNode={referenceNode}
  130. frameColor={getFrameColor(r.item.node)}
  131. formatDuration={formatDuration}
  132. tabIndex={tabIndexKey === r.key ? 0 : 1}
  133. onClick={handleRowClick}
  134. onExpandClick={handleExpandTreeNode}
  135. onKeyDown={handleRowKeyDown}
  136. onMouseEnter={handleRowMouseEnter}
  137. onContextMenu={evt => {
  138. setClickedContextMenuClose(r.item);
  139. contextMenu.handleContextMenu(evt);
  140. }}
  141. />
  142. );
  143. },
  144. [contextMenu, formatDuration, referenceNode, getFrameColor]
  145. );
  146. const {
  147. renderedItems,
  148. scrollContainerStyles,
  149. containerStyles,
  150. handleSortingChange,
  151. clickedGhostRowRef,
  152. hoveredGhostRowRef,
  153. } = useVirtualizedTree({
  154. skipFunction: recursion === 'collapsed' ? skipRecursiveNodes : undefined,
  155. sortFunction,
  156. renderRow,
  157. scrollContainer: scrollContainerRef,
  158. rowHeight: 24,
  159. tree,
  160. });
  161. const onSortChange = useCallback(
  162. (newSort: 'total weight' | 'self weight' | 'name') => {
  163. const newDirection =
  164. newSort === sort ? (direction === 'asc' ? 'desc' : 'asc') : 'desc';
  165. setDirection(newDirection);
  166. setSort(newSort);
  167. const sortFn = makeSortFunction(newSort, newDirection);
  168. handleSortingChange(sortFn);
  169. },
  170. [sort, direction, handleSortingChange]
  171. );
  172. return (
  173. <FrameBar>
  174. <FrameCallersTable>
  175. <FrameCallersTableHeader>
  176. <FrameWeightCell>
  177. <TableHeaderButton onClick={() => onSortChange('self weight')}>
  178. {t('Self Time ')}
  179. {sort === 'self weight' ? (
  180. <IconArrow direction={direction === 'desc' ? 'down' : 'up'} />
  181. ) : null}
  182. </TableHeaderButton>
  183. </FrameWeightCell>
  184. <FrameWeightCell>
  185. <TableHeaderButton onClick={() => onSortChange('total weight')}>
  186. {t('Total Time')}{' '}
  187. {sort === 'total weight' ? (
  188. <IconArrow direction={direction === 'desc' ? 'down' : 'up'} />
  189. ) : null}
  190. </TableHeaderButton>
  191. </FrameWeightCell>
  192. <FrameNameCell>
  193. <TableHeaderButton onClick={() => onSortChange('name')}>
  194. {t('Frame')}{' '}
  195. {sort === 'name' ? (
  196. <IconArrow direction={direction === 'desc' ? 'down' : 'up'} />
  197. ) : null}
  198. </TableHeaderButton>
  199. </FrameNameCell>
  200. </FrameCallersTableHeader>
  201. <FrameStackContextMenu
  202. onZoomIntoFrameClick={handleZoomIntoFrameClick}
  203. contextMenu={contextMenu}
  204. />
  205. <TableItemsContainer>
  206. {/*
  207. The order of these two matters because we want clicked state to
  208. be on top of hover in cases where user is hovering a clicked row.
  209. */}
  210. <div ref={hoveredGhostRowRef} />
  211. <div ref={clickedGhostRowRef} />
  212. <div
  213. ref={ref => setScrollContainerRef(ref)}
  214. style={scrollContainerStyles}
  215. onContextMenu={contextMenu.handleContextMenu}
  216. >
  217. <div style={containerStyles}>
  218. {renderedItems}
  219. {/*
  220. This is a ghost row, we stretch its width and height to fit the entire table
  221. so that borders on columns are shown across the entire table and not just the rows.
  222. This is useful when number of rows does not fill up the entire table height.
  223. */}
  224. <GhostRowContainer>
  225. <FrameCallersTableCell />
  226. <FrameCallersTableCell />
  227. <FrameCallersTableCell style={{width: '100%'}} />
  228. </GhostRowContainer>
  229. </div>
  230. </div>
  231. </TableItemsContainer>
  232. </FrameCallersTable>
  233. </FrameBar>
  234. );
  235. }
  236. const TableItemsContainer = styled('div')`
  237. position: relative;
  238. height: 100%;
  239. overflow: hidden;
  240. background: ${p => p.theme.background};
  241. `;
  242. const GhostRowContainer = styled('div')`
  243. display: flex;
  244. width: 100%;
  245. pointer-events: none;
  246. position: absolute;
  247. height: 100%;
  248. z-index: -1;
  249. `;
  250. const TableHeaderButton = styled('button')`
  251. display: flex;
  252. width: 100%;
  253. align-items: center;
  254. justify-content: space-between;
  255. padding: 0 ${space(1)};
  256. border: none;
  257. background-color: ${props => props.theme.surface100};
  258. transition: background-color 100ms ease-in-out;
  259. line-height: 24px;
  260. &:hover {
  261. background-color: ${props => props.theme.surface400};
  262. }
  263. svg {
  264. width: 10px;
  265. height: 10px;
  266. }
  267. `;
  268. const FrameBar = styled('div')`
  269. overflow: auto;
  270. width: 100%;
  271. position: relative;
  272. background-color: ${p => p.theme.surface100};
  273. border-top: 1px solid ${p => p.theme.border};
  274. flex: 1 1 100%;
  275. grid-area: table;
  276. `;
  277. const FrameCallersTable = styled('div')`
  278. font-size: ${p => p.theme.fontSizeSmall};
  279. margin: 0;
  280. overflow: auto;
  281. max-height: 100%;
  282. height: 100%;
  283. width: 100%;
  284. display: flex;
  285. flex-direction: column;
  286. `;
  287. const FRAME_WEIGHT_CELL_WIDTH_PX = 164;
  288. const FrameWeightCell = styled('div')`
  289. width: ${FRAME_WEIGHT_CELL_WIDTH_PX}px;
  290. `;
  291. const FrameNameCell = styled('div')`
  292. flex: 1 1 100%;
  293. `;
  294. const FrameCallersTableHeader = styled('div')`
  295. top: 0;
  296. position: sticky;
  297. z-index: 1;
  298. display: flex;
  299. flex: 1;
  300. > div {
  301. position: relative;
  302. border-bottom: 1px solid ${p => p.theme.border};
  303. background-color: ${p => p.theme.background};
  304. white-space: nowrap;
  305. &:last-child {
  306. flex: 1;
  307. }
  308. &:not(:last-child) {
  309. border-right: 1px solid ${p => p.theme.border};
  310. }
  311. }
  312. `;