frameStackTableRow.tsx 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. import {forwardRef, useCallback} from 'react';
  2. import styled from '@emotion/styled';
  3. import {IconSettings, IconUser} from 'sentry/icons';
  4. import space from 'sentry/styles/space';
  5. import {Flamegraph} from 'sentry/utils/profiling/flamegraph';
  6. import {FlamegraphFrame} from 'sentry/utils/profiling/flamegraphFrame';
  7. import {VirtualizedTreeNode} from 'sentry/utils/profiling/hooks/useVirtualizedTree/VirtualizedTreeNode';
  8. import {FrameCallersTableCell} from './frameStack';
  9. function computeRelativeWeight(base: number, value: number) {
  10. // Make sure we dont divide by zero
  11. if (!base || !value) {
  12. return 0;
  13. }
  14. return (value / base) * 100;
  15. }
  16. interface FrameStackTableRowProps {
  17. formatDuration: Flamegraph['formatter'];
  18. frameColor: string;
  19. node: VirtualizedTreeNode<FlamegraphFrame>;
  20. onClick: React.MouseEventHandler<HTMLDivElement>;
  21. onContextMenu: React.MouseEventHandler<HTMLDivElement>;
  22. onExpandClick: (
  23. node: VirtualizedTreeNode<FlamegraphFrame>,
  24. opts?: {expandChildren: boolean}
  25. ) => void;
  26. onKeyDown: React.KeyboardEventHandler<HTMLDivElement>;
  27. onMouseEnter: React.MouseEventHandler<HTMLDivElement>;
  28. referenceNode: FlamegraphFrame;
  29. style: React.CSSProperties;
  30. tabIndex: number;
  31. }
  32. export const FrameStackTableRow = forwardRef<HTMLDivElement, FrameStackTableRowProps>(
  33. (
  34. {
  35. node,
  36. referenceNode,
  37. onExpandClick,
  38. onContextMenu,
  39. formatDuration,
  40. frameColor,
  41. tabIndex,
  42. onKeyDown,
  43. onClick,
  44. onMouseEnter,
  45. style,
  46. },
  47. ref
  48. ) => {
  49. const isSelected = tabIndex === 0;
  50. const handleExpanding = useCallback(
  51. (evt: React.MouseEvent) => {
  52. evt.stopPropagation();
  53. onExpandClick(node, {expandChildren: evt.metaKey});
  54. },
  55. [node, onExpandClick]
  56. );
  57. return (
  58. <FrameCallersRow
  59. ref={ref}
  60. style={style}
  61. onContextMenu={onContextMenu}
  62. tabIndex={tabIndex}
  63. isSelected={isSelected}
  64. onKeyDown={onKeyDown}
  65. onClick={onClick}
  66. onMouseEnter={onMouseEnter}
  67. >
  68. <FrameCallersTableCell isSelected={isSelected} textAlign="right">
  69. {formatDuration(node.node.node.selfWeight)}
  70. <Weight
  71. isSelected={isSelected}
  72. weight={computeRelativeWeight(
  73. referenceNode.node.totalWeight,
  74. node.node.node.selfWeight
  75. )}
  76. />
  77. </FrameCallersTableCell>
  78. <FrameCallersTableCell isSelected={isSelected} noPadding textAlign="right">
  79. <FrameWeightTypeContainer>
  80. <FrameWeightContainer>
  81. {formatDuration(node.node.node.totalWeight)}
  82. <Weight
  83. padded
  84. isSelected={isSelected}
  85. weight={computeRelativeWeight(
  86. referenceNode.node.totalWeight,
  87. node.node.node.totalWeight
  88. )}
  89. />
  90. </FrameWeightContainer>
  91. <FrameTypeIndicator isSelected={isSelected}>
  92. {node.node.node.frame.is_application ? (
  93. <IconUser size="xs" />
  94. ) : (
  95. <IconSettings size="xs" />
  96. )}
  97. </FrameTypeIndicator>
  98. </FrameWeightTypeContainer>
  99. </FrameCallersTableCell>
  100. <FrameCallersTableCell
  101. // We stretch this table to 100% width.
  102. style={{paddingLeft: node.depth * 14 + 8, width: '100%'}}
  103. >
  104. <FrameNameContainer>
  105. {/* @TODO FIX COLOR */}
  106. <FrameColorIndicator style={{backgroundColor: frameColor}} />
  107. <FrameChildrenIndicator
  108. tabIndex={-1}
  109. onClick={handleExpanding}
  110. open={node.expanded}
  111. >
  112. {node.node.children.length > 0 ? '\u203A' : null}
  113. </FrameChildrenIndicator>
  114. <FrameName>{node.node.frame.name}</FrameName>
  115. </FrameNameContainer>
  116. </FrameCallersTableCell>
  117. </FrameCallersRow>
  118. );
  119. }
  120. );
  121. const Weight = styled(
  122. (props: {isSelected: boolean; weight: number; padded?: boolean}) => {
  123. const {weight, padded: __, isSelected: _, ...rest} = props;
  124. return (
  125. <div {...rest}>
  126. {weight.toFixed(1)}%
  127. <BackgroundWeightBar style={{transform: `scaleX(${weight / 100})`}} />
  128. </div>
  129. );
  130. }
  131. )`
  132. display: inline-block;
  133. min-width: 7ch;
  134. padding-right: ${p => (p.padded ? space(0.5) : 0)};
  135. color: ${p => (p.isSelected ? p.theme.white : p.theme.subText)};
  136. opacity: ${p => (p.isSelected ? 0.8 : 1)};
  137. `;
  138. const FrameWeightTypeContainer = styled('div')`
  139. display: flex;
  140. align-items: center;
  141. justify-content: flex-end;
  142. position: relative;
  143. `;
  144. const FrameTypeIndicator = styled('div')<{isSelected: boolean}>`
  145. flex-shrink: 0;
  146. width: 26px;
  147. height: 12px;
  148. display: flex;
  149. align-items: center;
  150. justify-content: center;
  151. color: ${p => (p.isSelected ? p.theme.white : p.theme.subText)};
  152. opacity: ${p => (p.isSelected ? 0.8 : 1)};
  153. `;
  154. const FrameWeightContainer = styled('div')`
  155. display: flex;
  156. align-items: center;
  157. position: relative;
  158. justify-content: flex-end;
  159. flex: 1 1 100%;
  160. `;
  161. const BackgroundWeightBar = styled('div')`
  162. pointer-events: none;
  163. position: absolute;
  164. right: 0;
  165. top: 0;
  166. background-color: ${props => props.theme.yellow100};
  167. border-bottom: 1px solid ${props => props.theme.yellow200};
  168. transform-origin: center right;
  169. height: 100%;
  170. width: 100%;
  171. `;
  172. const FrameCallersRow = styled('div')<{isSelected: boolean}>`
  173. display: flex;
  174. width: 100%;
  175. color: ${p => (p.isSelected ? p.theme.white : 'inherit')};
  176. scroll-margin-top: 24px;
  177. font-size: ${p => p.theme.fontSizeSmall};
  178. line-height: 24px;
  179. &:focus {
  180. outline: none;
  181. }
  182. &[data-hovered='true']:not([tabindex='0']) {
  183. > div:first-child,
  184. > div:nth-child(2) {
  185. background-color: ${p => p.theme.surface100} !important;
  186. }
  187. }
  188. `;
  189. const FrameNameContainer = styled('div')`
  190. display: flex;
  191. align-items: center;
  192. `;
  193. const FrameChildrenIndicator = styled('button')<{open: boolean}>`
  194. width: 10px;
  195. height: 10px;
  196. display: flex;
  197. padding: 0;
  198. border: none;
  199. background-color: transparent;
  200. align-items: center;
  201. justify-content: center;
  202. user-select: none;
  203. transform: ${p => (p.open ? 'rotate(90deg)' : 'rotate(0deg)')};
  204. `;
  205. const FrameName = styled('span')`
  206. margin-left: ${space(0.5)};
  207. `;
  208. const FrameColorIndicator = styled('div')`
  209. width: 12px;
  210. height: 12px;
  211. border-radius: 2px;
  212. display: inline-block;
  213. flex-shrink: 0;
  214. margin-right: ${space(0.5)};
  215. `;