legacyFunctionsTable.tsx 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  1. import {Fragment, useMemo, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import Button from 'sentry/components/button';
  4. import ButtonBar from 'sentry/components/buttonBar';
  5. import {SectionHeading} from 'sentry/components/charts/styles';
  6. import Count from 'sentry/components/count';
  7. import GridEditable, {
  8. COL_WIDTH_UNDEFINED,
  9. GridColumnOrder,
  10. } from 'sentry/components/gridEditable';
  11. import PerformanceDuration from 'sentry/components/performanceDuration';
  12. import {ArrayLinks} from 'sentry/components/profiling/arrayLinks';
  13. import {IconChevron} from 'sentry/icons';
  14. import {t} from 'sentry/locale';
  15. import space from 'sentry/styles/space';
  16. import {Project} from 'sentry/types';
  17. import {FunctionCall} from 'sentry/types/profiling/core';
  18. import {Container, NumberContainer} from 'sentry/utils/discover/styles';
  19. import {getShortEventId} from 'sentry/utils/events';
  20. import {formatPercentage} from 'sentry/utils/formatters';
  21. import {generateProfileFlamegraphRouteWithQuery} from 'sentry/utils/profiling/routes';
  22. import {renderTableHead} from 'sentry/utils/profiling/tableRenderer';
  23. import {useLocation} from 'sentry/utils/useLocation';
  24. import useOrganization from 'sentry/utils/useOrganization';
  25. interface LegacyFunctionsTableProps {
  26. error: string | null;
  27. functionCalls: FunctionCall[];
  28. isLoading: boolean;
  29. project: Project;
  30. limit?: number;
  31. }
  32. function LegacyFunctionsTable(props: LegacyFunctionsTableProps) {
  33. const limit = props.limit ?? 5;
  34. const [offset, setOffset] = useState(0);
  35. const location = useLocation();
  36. const organization = useOrganization();
  37. const allFunctions: TableDataRow[] = useMemo(() => {
  38. return props.functionCalls.map(functionCall => ({
  39. symbol: functionCall.symbol,
  40. image: functionCall.image,
  41. p50Duration: functionCall.duration_ns.p50,
  42. p75Duration: functionCall.duration_ns.p75,
  43. p90Duration: functionCall.duration_ns.p90,
  44. p95Duration: functionCall.duration_ns.p95,
  45. p99Duration: functionCall.duration_ns.p99,
  46. mainThreadPercent: functionCall.main_thread_percent,
  47. p50Frequency: functionCall.frequency.p50,
  48. p75Frequency: functionCall.frequency.p75,
  49. p90Frequency: functionCall.frequency.p90,
  50. p95Frequency: functionCall.frequency.p95,
  51. p99Frequency: functionCall.frequency.p99,
  52. profileIdToThreadId: Object.entries(functionCall.profile_id_to_thread_id).map(
  53. ([profileId, threadId]) => {
  54. return {
  55. value: getShortEventId(profileId),
  56. target: generateProfileFlamegraphRouteWithQuery({
  57. orgSlug: organization.slug,
  58. projectSlug: props.project.slug,
  59. profileId,
  60. query: {tid: threadId.toString()},
  61. }),
  62. };
  63. }
  64. ),
  65. }));
  66. }, [organization.slug, props.project.slug, props.functionCalls]);
  67. const functions: TableDataRow[] = useMemo(() => {
  68. return allFunctions.slice(offset, offset + limit);
  69. }, [allFunctions, limit, offset]);
  70. return (
  71. <Fragment>
  72. <TableHeader>
  73. <SectionHeading>{t('Suspect Functions')}</SectionHeading>
  74. <ButtonBar merged>
  75. <Button
  76. icon={<IconChevron direction="left" size="sm" />}
  77. aria-label={t('Previous')}
  78. size="xs"
  79. disabled={offset === 0}
  80. onClick={() => setOffset(offset - limit)}
  81. />
  82. <Button
  83. icon={<IconChevron direction="right" size="sm" />}
  84. aria-label={t('Next')}
  85. size="xs"
  86. disabled={offset + limit >= allFunctions.length}
  87. onClick={() => setOffset(offset + limit)}
  88. />
  89. </ButtonBar>
  90. </TableHeader>
  91. <GridEditable
  92. isLoading={props.isLoading}
  93. error={props.error}
  94. data={functions}
  95. columnOrder={COLUMN_ORDER.map(key => COLUMNS[key])}
  96. columnSortBy={[]}
  97. grid={{
  98. renderHeadCell: renderTableHead({rightAlignedColumns: RIGHT_ALIGNED_COLUMNS}),
  99. renderBodyCell: renderFunctionsTableCell,
  100. }}
  101. location={location}
  102. />
  103. </Fragment>
  104. );
  105. }
  106. const RIGHT_ALIGNED_COLUMNS = new Set<TableColumnKey>([
  107. 'p50Duration',
  108. 'p75Duration',
  109. 'p90Duration',
  110. 'p95Duration',
  111. 'p99Duration',
  112. 'mainThreadPercent',
  113. 'p50Frequency',
  114. 'p75Frequency',
  115. 'p90Frequency',
  116. 'p95Frequency',
  117. 'p99Frequency',
  118. ]);
  119. function renderFunctionsTableCell(
  120. column: TableColumn,
  121. dataRow: TableDataRow,
  122. rowIndex: number,
  123. columnIndex: number
  124. ) {
  125. return (
  126. <ProfilingFunctionsTableCell
  127. column={column}
  128. dataRow={dataRow}
  129. rowIndex={rowIndex}
  130. columnIndex={columnIndex}
  131. />
  132. );
  133. }
  134. interface ProfilingFunctionsTableCellProps {
  135. column: TableColumn;
  136. columnIndex: number;
  137. dataRow: TableDataRow;
  138. rowIndex: number;
  139. }
  140. function ProfilingFunctionsTableCell({
  141. column,
  142. dataRow,
  143. }: ProfilingFunctionsTableCellProps) {
  144. const value = dataRow[column.key];
  145. switch (column.key) {
  146. case 'p50Frequency':
  147. case 'p75Frequency':
  148. case 'p90Frequency':
  149. case 'p95Frequency':
  150. case 'p99Frequency':
  151. return (
  152. <NumberContainer>
  153. <Count value={value} />
  154. </NumberContainer>
  155. );
  156. case 'mainThreadPercent':
  157. return <NumberContainer>{formatPercentage(value)}</NumberContainer>;
  158. case 'p50Duration':
  159. case 'p75Duration':
  160. case 'p90Duration':
  161. case 'p95Duration':
  162. case 'p99Duration':
  163. return (
  164. <NumberContainer>
  165. <PerformanceDuration nanoseconds={value} abbreviation />
  166. </NumberContainer>
  167. );
  168. case 'profileIdToThreadId':
  169. return <ArrayLinks items={value} />;
  170. default:
  171. return <Container>{value}</Container>;
  172. }
  173. }
  174. type TableColumnKey =
  175. | 'symbol'
  176. | 'image'
  177. | 'p50Duration'
  178. | 'p75Duration'
  179. | 'p90Duration'
  180. | 'p95Duration'
  181. | 'p99Duration'
  182. | 'mainThreadPercent'
  183. | 'p50Frequency'
  184. | 'p75Frequency'
  185. | 'p90Frequency'
  186. | 'p95Frequency'
  187. | 'p99Frequency'
  188. | 'profileIdToThreadId';
  189. type TableDataRow = Record<TableColumnKey, any>;
  190. type TableColumn = GridColumnOrder<TableColumnKey>;
  191. const COLUMN_ORDER: TableColumnKey[] = [
  192. 'symbol',
  193. 'image',
  194. 'p75Duration',
  195. 'p99Duration',
  196. 'mainThreadPercent',
  197. 'p75Frequency',
  198. 'p99Frequency',
  199. 'profileIdToThreadId',
  200. ];
  201. // TODO: looks like these column names change depending on the platform?
  202. const COLUMNS: Record<TableColumnKey, TableColumn> = {
  203. symbol: {
  204. key: 'symbol',
  205. name: t('Symbol'),
  206. width: COL_WIDTH_UNDEFINED,
  207. },
  208. image: {
  209. key: 'image',
  210. name: t('Binary'),
  211. width: COL_WIDTH_UNDEFINED,
  212. },
  213. p50Duration: {
  214. key: 'p50Duration',
  215. name: t('P50 Duration'),
  216. width: COL_WIDTH_UNDEFINED,
  217. },
  218. p75Duration: {
  219. key: 'p75Duration',
  220. name: t('P75 Duration'),
  221. width: COL_WIDTH_UNDEFINED,
  222. },
  223. p90Duration: {
  224. key: 'p90Duration',
  225. name: t('P90 Duration'),
  226. width: COL_WIDTH_UNDEFINED,
  227. },
  228. p95Duration: {
  229. key: 'p95Duration',
  230. name: t('P95 Duration'),
  231. width: COL_WIDTH_UNDEFINED,
  232. },
  233. p99Duration: {
  234. key: 'p99Duration',
  235. name: t('P99 Duration'),
  236. width: COL_WIDTH_UNDEFINED,
  237. },
  238. mainThreadPercent: {
  239. key: 'mainThreadPercent',
  240. name: t('Main Thread %'),
  241. width: COL_WIDTH_UNDEFINED,
  242. },
  243. p50Frequency: {
  244. key: 'p50Frequency',
  245. name: t('P50 Frequency'),
  246. width: COL_WIDTH_UNDEFINED,
  247. },
  248. p75Frequency: {
  249. key: 'p75Frequency',
  250. name: t('P75 Frequency'),
  251. width: COL_WIDTH_UNDEFINED,
  252. },
  253. p90Frequency: {
  254. key: 'p90Frequency',
  255. name: t('P90 Frequency'),
  256. width: COL_WIDTH_UNDEFINED,
  257. },
  258. p95Frequency: {
  259. key: 'p95Frequency',
  260. name: t('P95 Frequency'),
  261. width: COL_WIDTH_UNDEFINED,
  262. },
  263. p99Frequency: {
  264. key: 'p99Frequency',
  265. name: t('P99 Frequency'),
  266. width: COL_WIDTH_UNDEFINED,
  267. },
  268. profileIdToThreadId: {
  269. key: 'profileIdToThreadId',
  270. name: t('Example Profiles'),
  271. width: COL_WIDTH_UNDEFINED,
  272. },
  273. };
  274. const TableHeader = styled('div')`
  275. display: flex;
  276. justify-content: space-between;
  277. margin-bottom: ${space(1)};
  278. `;
  279. export {LegacyFunctionsTable};