slowestFunctionsTable.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348
  1. import {Fragment, useCallback, useMemo, useState} from 'react';
  2. import {Link} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import {clamp} from 'lodash';
  5. import {Button} from 'sentry/components/button';
  6. import ButtonBar from 'sentry/components/buttonBar';
  7. import EmptyStateWarning from 'sentry/components/emptyStateWarning';
  8. import ProjectBadge from 'sentry/components/idBadge/projectBadge';
  9. import LoadingIndicator from 'sentry/components/loadingIndicator';
  10. import Panel from 'sentry/components/panels/panel';
  11. import {Tooltip} from 'sentry/components/tooltip';
  12. import {IconChevron, IconWarning} from 'sentry/icons';
  13. import {t} from 'sentry/locale';
  14. import {space} from 'sentry/styles/space';
  15. import type {Project} from 'sentry/types/project';
  16. import {formatAbbreviatedNumber} from 'sentry/utils/formatters';
  17. import {useAggregateFlamegraphQuery} from 'sentry/utils/profiling/hooks/useAggregateFlamegraphQuery';
  18. import {useProfilingFunctionMetrics} from 'sentry/utils/profiling/hooks/useProfilingFunctionMetrics';
  19. import {generateProfileRouteFromProfileReference} from 'sentry/utils/profiling/routes';
  20. import useOrganization from 'sentry/utils/useOrganization';
  21. import useProjects from 'sentry/utils/useProjects';
  22. import {getPerformanceDuration} from 'sentry/views/performance/utils/getPerformanceDuration';
  23. import {ContentContainer, StatusContainer} from './styles';
  24. function sortFunctions(a: Profiling.FunctionMetric, b: Profiling.FunctionMetric) {
  25. return b.sum - a.sum;
  26. }
  27. function useMemoryPagination(items: any[], size: number) {
  28. const [pagination, setPagination] = useState({
  29. start: 0,
  30. end: size,
  31. });
  32. const page = Math.floor(pagination.start / size);
  33. const toPage = useCallback(
  34. (p: number) => {
  35. const next = clamp(p, 0, Math.floor(items.length / size));
  36. setPagination({
  37. start: clamp(next * size, 0, items.length - size),
  38. end: Math.min(next * size + size, items.length),
  39. });
  40. },
  41. [size, items]
  42. );
  43. return {
  44. page,
  45. start: pagination.start,
  46. end: pagination.end,
  47. nextButtonProps: {
  48. disabled: pagination.end >= items.length,
  49. onClick: () => toPage(page + 1),
  50. },
  51. previousButtonProps: {
  52. disabled: pagination.start <= 0,
  53. onClick: () => toPage(page - 1),
  54. },
  55. };
  56. }
  57. export function SlowestFunctionsTable() {
  58. const {projects} = useProjects();
  59. const query = useAggregateFlamegraphQuery({
  60. dataSource: 'profiles',
  61. metrics: true,
  62. });
  63. const sortedMetrics = useMemo(() => {
  64. return query.data?.metrics?.sort(sortFunctions) ?? [];
  65. }, [query.data?.metrics]);
  66. const pagination = useMemoryPagination(sortedMetrics, 5);
  67. const [expandedFingerprint, setExpandedFingerprint] = useState<
  68. Profiling.FunctionMetric['fingerprint'] | null
  69. >(null);
  70. const projectsLookupTable = useMemo(() => {
  71. return projects.reduce(
  72. (acc, project) => {
  73. acc[project.id] = project;
  74. return acc;
  75. },
  76. {} as Record<string, Project>
  77. );
  78. }, [projects]);
  79. const hasFunctions = query.data?.metrics && query.data.metrics.length > 0;
  80. return (
  81. <Fragment>
  82. <SlowestWidgetContainer>
  83. <ContentContainer>
  84. {query.isLoading && (
  85. <StatusContainer>
  86. <LoadingIndicator size={36} />
  87. </StatusContainer>
  88. )}
  89. {query.isError && (
  90. <StatusContainer>
  91. <IconWarning data-test-id="error-indicator" color="gray300" size="lg" />
  92. </StatusContainer>
  93. )}
  94. {!query.isError && !query.isLoading && !hasFunctions && (
  95. <EmptyStateWarning>
  96. <p>{t('No functions found')}</p>
  97. </EmptyStateWarning>
  98. )}
  99. {hasFunctions && query.isFetched && (
  100. <Fragment>
  101. <SlowestFunctionsContainer>
  102. <SlowestFunctionHeader>
  103. <SlowestFunctionCell>{t('Slowest functions')}</SlowestFunctionCell>
  104. <SlowestFunctionCell>{t('Package')}</SlowestFunctionCell>
  105. <SlowestFunctionCell>{t('Project')}</SlowestFunctionCell>
  106. <SlowestFunctionCell>{t('Count()')}</SlowestFunctionCell>
  107. <SlowestFunctionCell>{t('p75()')}</SlowestFunctionCell>
  108. <SlowestFunctionCell>{t('p95()')}</SlowestFunctionCell>
  109. <SlowestFunctionCell>{t('p99()')}</SlowestFunctionCell>
  110. {/* @TODO remove sum before relasing */}
  111. <SlowestFunctionCell>{t('Sum()')}</SlowestFunctionCell>
  112. <SlowestFunctionCell />
  113. </SlowestFunctionHeader>
  114. {sortedMetrics.slice(pagination.start, pagination.end).map((f, i) => {
  115. return (
  116. <SlowestFunction
  117. key={i}
  118. function={f}
  119. projectsLookupTable={projectsLookupTable}
  120. expanded={f.fingerprint === expandedFingerprint}
  121. onExpandClick={setExpandedFingerprint}
  122. />
  123. );
  124. })}
  125. </SlowestFunctionsContainer>
  126. </Fragment>
  127. )}
  128. </ContentContainer>
  129. </SlowestWidgetContainer>
  130. <SlowestFunctionsPaginationContainer>
  131. <ButtonBar merged>
  132. <Button
  133. icon={<IconChevron direction="left" />}
  134. aria-label={t('Previous')}
  135. size={'sm'}
  136. {...pagination.previousButtonProps}
  137. />
  138. <Button
  139. icon={<IconChevron direction="right" />}
  140. aria-label={t('Next')}
  141. size={'sm'}
  142. {...pagination.nextButtonProps}
  143. />
  144. </ButtonBar>
  145. </SlowestFunctionsPaginationContainer>
  146. </Fragment>
  147. );
  148. }
  149. interface SlowestFunctionProps {
  150. expanded: boolean;
  151. function: NonNullable<Profiling.Schema['metrics']>[0];
  152. onExpandClick: React.Dispatch<
  153. React.SetStateAction<Profiling.FunctionMetric['fingerprint'] | null>
  154. >;
  155. projectsLookupTable: Record<string, Project>;
  156. }
  157. function SlowestFunction(props: SlowestFunctionProps) {
  158. const organization = useOrganization();
  159. const example = props.function.examples[0];
  160. const exampleLink =
  161. example && typeof example !== 'string' && 'project_id' in example
  162. ? generateProfileRouteFromProfileReference({
  163. frameName: props.function.name,
  164. framePackage: props.function.package,
  165. orgSlug: organization.slug,
  166. projectSlug: props.projectsLookupTable[example.project_id]?.slug ?? '',
  167. reference: props.function.examples[0],
  168. })
  169. : null;
  170. return (
  171. <SlowestFunctionContainer>
  172. <SlowestFunctionCell>
  173. <Tooltip title={props.function.name}>
  174. {exampleLink ? (
  175. <Link to={exampleLink}>{props.function.name || t('<unknown function>')}</Link>
  176. ) : (
  177. props.function.name || t('<unknown function>')
  178. )}
  179. </Tooltip>
  180. </SlowestFunctionCell>
  181. <SlowestFunctionCell>
  182. <Tooltip title={props.function.package || t('<unknown package>')}>
  183. {props.function.package}
  184. </Tooltip>
  185. </SlowestFunctionCell>
  186. <SlowestFunctionCell>
  187. <SlowestFunctionsProjectBadge
  188. examples={props.function.examples}
  189. projectsLookupTable={props.projectsLookupTable}
  190. />{' '}
  191. </SlowestFunctionCell>
  192. <SlowestFunctionCell>
  193. {formatAbbreviatedNumber(props.function.count)}
  194. </SlowestFunctionCell>
  195. <SlowestFunctionCell>
  196. {getPerformanceDuration(props.function.p75 / 1e6)}
  197. </SlowestFunctionCell>
  198. <SlowestFunctionCell>
  199. {getPerformanceDuration(props.function.p95 / 1e6)}
  200. </SlowestFunctionCell>
  201. <SlowestFunctionCell>
  202. {getPerformanceDuration(props.function.p99 / 1e6)}
  203. </SlowestFunctionCell>
  204. <SlowestFunctionCell>
  205. {/* @TODO remove sum before relasing */}
  206. {getPerformanceDuration(props.function.sum / 1e6)}
  207. </SlowestFunctionCell>
  208. <SlowestFunctionCell>
  209. <Button
  210. icon={<IconChevron direction={props.expanded ? 'up' : 'down'} />}
  211. aria-label={t('View Function Metrics')}
  212. onClick={() => props.onExpandClick(props.function.fingerprint)}
  213. size="xs"
  214. />
  215. </SlowestFunctionCell>
  216. {props.expanded ? <SlowestFunctionTimeSeries function={props.function} /> : null}
  217. </SlowestFunctionContainer>
  218. );
  219. }
  220. interface SlowestFunctionsProjectBadgeProps {
  221. examples: Profiling.FunctionMetric['examples'];
  222. projectsLookupTable: Record<string, Project>;
  223. }
  224. function SlowestFunctionsProjectBadge(props: SlowestFunctionsProjectBadgeProps) {
  225. const resolvedProjects = useMemo(() => {
  226. const projects: Project[] = [];
  227. for (const example of props.examples) {
  228. if (typeof example !== 'string' && 'project_id' in example) {
  229. const project = props.projectsLookupTable[example.project_id];
  230. if (project) projects.push(project);
  231. }
  232. }
  233. return projects;
  234. }, [props.examples, props.projectsLookupTable]);
  235. return resolvedProjects[0] ? (
  236. <ProjectBadge avatarSize={16} project={resolvedProjects[0]} />
  237. ) : null;
  238. }
  239. interface SlowestFunctionTimeSeriesProps {
  240. function: Profiling.FunctionMetric;
  241. }
  242. function SlowestFunctionTimeSeries(props: SlowestFunctionTimeSeriesProps) {
  243. const projects = useMemo(() => {
  244. const projectsMap = props.function.examples.reduce<Record<string, number>>(
  245. (acc, f) => {
  246. if (typeof f !== 'string' && 'project_id' in f) {
  247. acc[f.project_id] = f.project_id;
  248. }
  249. return acc;
  250. },
  251. {}
  252. );
  253. return Object.values(projectsMap);
  254. }, [props.function]);
  255. useProfilingFunctionMetrics({
  256. fingerprint: props.function.fingerprint,
  257. projects,
  258. });
  259. // @TODO add chart
  260. return null;
  261. }
  262. const SlowestFunctionsPaginationContainer = styled('div')`
  263. display: flex;
  264. justify-content: flex-end;
  265. margin-bottom: ${space(2)};
  266. `;
  267. const SlowestWidgetContainer = styled(Panel)`
  268. display: flex;
  269. flex-direction: column;
  270. overflow: hidden;
  271. `;
  272. const SlowestFunctionHeader = styled('div')`
  273. display: grid;
  274. grid-template-columns: subgrid;
  275. grid-column: 1 / -1;
  276. background-color: ${p => p.theme.backgroundSecondary};
  277. border-bottom: 1px solid ${p => p.theme.border};
  278. color: ${p => p.theme.subText};
  279. text-transform: uppercase;
  280. font-size: ${p => p.theme.fontSizeSmall};
  281. font-weight: 600;
  282. > div:nth-child(n + 4) {
  283. text-align: right;
  284. }
  285. `;
  286. const SlowestFunctionsContainer = styled('div')`
  287. display: grid;
  288. grid-template-columns:
  289. minmax(90px, auto) minmax(90px, auto) minmax(40px, 140px) min-content min-content
  290. min-content min-content min-content min-content;
  291. border-collapse: collapse;
  292. `;
  293. const SlowestFunctionCell = styled('div')`
  294. padding: ${space(1)} ${space(2)};
  295. overflow: hidden;
  296. text-overflow: ellipsis;
  297. white-space: nowrap;
  298. `;
  299. const SlowestFunctionContainer = styled('div')`
  300. display: grid;
  301. grid-template-columns: subgrid;
  302. grid-column: 1 / -1;
  303. font-size: ${p => p.theme.fontSizeSmall};
  304. border-bottom: 1px solid ${p => p.theme.border};
  305. &:last-child {
  306. border-bottom: 0;
  307. }
  308. > div:nth-child(n + 4) {
  309. text-align: right;
  310. }
  311. `;