functionsTable.tsx 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. import {useCallback, useMemo} from 'react';
  2. import styled from '@emotion/styled';
  3. import Count from 'sentry/components/count';
  4. import GridEditable, {
  5. COL_WIDTH_UNDEFINED,
  6. GridColumnOrder,
  7. } from 'sentry/components/gridEditable';
  8. import PerformanceDuration from 'sentry/components/performanceDuration';
  9. import {ArrayLinks} from 'sentry/components/profiling/arrayLinks';
  10. import {t} from 'sentry/locale';
  11. import {Project} from 'sentry/types';
  12. import {SuspectFunction} from 'sentry/types/profiling/core';
  13. import {Container, NumberContainer} from 'sentry/utils/discover/styles';
  14. import {getShortEventId} from 'sentry/utils/events';
  15. import {generateProfileFlamechartRouteWithQuery} from 'sentry/utils/profiling/routes';
  16. import {renderTableHead} from 'sentry/utils/profiling/tableRenderer';
  17. import {useLocation} from 'sentry/utils/useLocation';
  18. import useOrganization from 'sentry/utils/useOrganization';
  19. interface FunctionsTableProps {
  20. error: string | null;
  21. functions: SuspectFunction[];
  22. isLoading: boolean;
  23. project: Project;
  24. sort: string;
  25. }
  26. function FunctionsTable(props: FunctionsTableProps) {
  27. const location = useLocation();
  28. const organization = useOrganization();
  29. const sort = useMemo(() => {
  30. let column = props.sort;
  31. let direction: 'asc' | 'desc' = 'asc' as const;
  32. if (props.sort.startsWith('-')) {
  33. column = props.sort.substring(1);
  34. direction = 'desc' as const;
  35. }
  36. if (!SORTABLE_COLUMNS.has(column as any)) {
  37. column = 'p99';
  38. }
  39. return {
  40. column: column as TableColumnKey,
  41. direction,
  42. };
  43. }, [props.sort]);
  44. const functions: TableDataRow[] = useMemo(() => {
  45. return props.functions.map(func => {
  46. const {worst, examples, ...rest} = func;
  47. const allExamples = examples.filter(example => example !== worst);
  48. allExamples.unshift(worst);
  49. return {
  50. ...rest,
  51. examples: allExamples.map(example => {
  52. const profileId = example.replaceAll('-', '');
  53. return {
  54. value: getShortEventId(profileId),
  55. target: generateProfileFlamechartRouteWithQuery({
  56. orgSlug: organization.slug,
  57. projectSlug: props.project.slug,
  58. profileId,
  59. query: {
  60. // specify the frame to focus, the flamegraph will switch
  61. // to the appropriate thread when these are specified
  62. frameName: func.name,
  63. framePackage: func.package,
  64. },
  65. }),
  66. };
  67. }),
  68. };
  69. });
  70. }, [organization.slug, props.project.slug, props.functions]);
  71. const generateSortLink = useCallback(
  72. (column: TableColumnKey) => {
  73. if (!SORTABLE_COLUMNS.has(column)) {
  74. return () => undefined;
  75. }
  76. const direction =
  77. sort.column !== column ? 'desc' : sort.direction === 'desc' ? 'asc' : 'desc';
  78. return () => ({
  79. ...location,
  80. query: {
  81. ...location.query,
  82. functionsSort: `${direction === 'desc' ? '-' : ''}${column}`,
  83. },
  84. });
  85. },
  86. [location, sort]
  87. );
  88. return (
  89. <GridEditable
  90. isLoading={props.isLoading}
  91. error={props.error}
  92. data={functions}
  93. columnOrder={COLUMN_ORDER.map(key => COLUMNS[key])}
  94. columnSortBy={[]}
  95. grid={{
  96. renderHeadCell: renderTableHead({
  97. currentSort: sort,
  98. rightAlignedColumns: RIGHT_ALIGNED_COLUMNS,
  99. sortableColumns: SORTABLE_COLUMNS,
  100. generateSortLink,
  101. }),
  102. renderBodyCell: renderFunctionsTableCell,
  103. }}
  104. location={location}
  105. />
  106. );
  107. }
  108. const RIGHT_ALIGNED_COLUMNS = new Set<TableColumnKey>(['p75', 'p99', 'count']);
  109. const SORTABLE_COLUMNS = RIGHT_ALIGNED_COLUMNS;
  110. function renderFunctionsTableCell(
  111. column: TableColumn,
  112. dataRow: TableDataRow,
  113. rowIndex: number,
  114. columnIndex: number
  115. ) {
  116. return (
  117. <ProfilingFunctionsTableCell
  118. column={column}
  119. dataRow={dataRow}
  120. rowIndex={rowIndex}
  121. columnIndex={columnIndex}
  122. />
  123. );
  124. }
  125. interface ProfilingFunctionsTableCellProps {
  126. column: TableColumn;
  127. columnIndex: number;
  128. dataRow: TableDataRow;
  129. rowIndex: number;
  130. }
  131. const EmptyValueContainer = styled('span')`
  132. color: ${p => p.theme.gray300};
  133. `;
  134. function ProfilingFunctionsTableCell({
  135. column,
  136. dataRow,
  137. }: ProfilingFunctionsTableCellProps) {
  138. const value = dataRow[column.key];
  139. switch (column.key) {
  140. case 'count':
  141. return (
  142. <NumberContainer>
  143. <Count value={value} />
  144. </NumberContainer>
  145. );
  146. case 'p75':
  147. case 'p99':
  148. return (
  149. <NumberContainer>
  150. <PerformanceDuration nanoseconds={value} abbreviation />
  151. </NumberContainer>
  152. );
  153. case 'examples':
  154. return <ArrayLinks items={value} />;
  155. case 'name':
  156. case 'package':
  157. const name = value || <EmptyValueContainer>{t('Unknown')}</EmptyValueContainer>;
  158. return <Container>{name}</Container>;
  159. default:
  160. return <Container>{value}</Container>;
  161. }
  162. }
  163. type TableColumnKey = keyof Omit<SuspectFunction, 'fingerprint' | 'worst'>;
  164. type TableDataRow = Record<TableColumnKey, any>;
  165. type TableColumn = GridColumnOrder<TableColumnKey>;
  166. const COLUMN_ORDER: TableColumnKey[] = [
  167. 'name',
  168. 'package',
  169. 'count',
  170. 'p75',
  171. 'p99',
  172. 'examples',
  173. ];
  174. const COLUMNS: Record<TableColumnKey, TableColumn> = {
  175. name: {
  176. key: 'name',
  177. name: t('Name'),
  178. width: COL_WIDTH_UNDEFINED,
  179. },
  180. package: {
  181. key: 'package',
  182. name: t('Package'),
  183. width: COL_WIDTH_UNDEFINED,
  184. },
  185. path: {
  186. key: 'path',
  187. name: t('Path'),
  188. width: COL_WIDTH_UNDEFINED,
  189. },
  190. p75: {
  191. key: 'p75',
  192. name: t('P75 Duration'),
  193. width: COL_WIDTH_UNDEFINED,
  194. },
  195. p99: {
  196. key: 'p99',
  197. name: t('P99 Duration'),
  198. width: COL_WIDTH_UNDEFINED,
  199. },
  200. count: {
  201. key: 'count',
  202. name: t('Count'),
  203. width: COL_WIDTH_UNDEFINED,
  204. },
  205. examples: {
  206. key: 'examples',
  207. name: t('Example Profiles'),
  208. width: COL_WIDTH_UNDEFINED,
  209. },
  210. };
  211. export {FunctionsTable};