frameStackTableRow.tsx 6.5 KB

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