slowestFunctionsWidget.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348
  1. import {CSSProperties, Fragment, useCallback, useMemo, useState} from 'react';
  2. import {browserHistory} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import {Button} from 'sentry/components/button';
  5. import Count from 'sentry/components/count';
  6. import EmptyStateWarning from 'sentry/components/emptyStateWarning';
  7. import IdBadge from 'sentry/components/idBadge';
  8. import Link from 'sentry/components/links/link';
  9. import LoadingIndicator from 'sentry/components/loadingIndicator';
  10. import Pagination from 'sentry/components/pagination';
  11. import PerformanceDuration from 'sentry/components/performanceDuration';
  12. import ScoreBar from 'sentry/components/scoreBar';
  13. import TextOverflow from 'sentry/components/textOverflow';
  14. import {Tooltip} from 'sentry/components/tooltip';
  15. import {CHART_PALETTE} from 'sentry/constants/chartPalette';
  16. import {IconChevron, IconWarning} from 'sentry/icons';
  17. import {t, tct} from 'sentry/locale';
  18. import {space} from 'sentry/styles/space';
  19. import {EventsResultsDataRow} from 'sentry/utils/profiling/hooks/types';
  20. import {useProfileFunctions} from 'sentry/utils/profiling/hooks/useProfileFunctions';
  21. import {generateProfileFlamechartRouteWithQuery} from 'sentry/utils/profiling/routes';
  22. import {decodeScalar} from 'sentry/utils/queryString';
  23. import {MutableSearch} from 'sentry/utils/tokenizeSearch';
  24. import {useLocation} from 'sentry/utils/useLocation';
  25. import useOrganization from 'sentry/utils/useOrganization';
  26. import useProjects from 'sentry/utils/useProjects';
  27. import {
  28. Accordion,
  29. AccordionItem,
  30. ContentContainer,
  31. HeaderContainer,
  32. HeaderTitleLegend,
  33. StatusContainer,
  34. Subtitle,
  35. WidgetContainer,
  36. } from './styles';
  37. const MAX_FUNCTIONS = 3;
  38. const CURSOR_NAME = 'slowFnCursor';
  39. interface SlowestFunctionsWidgetProps {
  40. userQuery?: string;
  41. }
  42. export function SlowestFunctionsWidget({userQuery}: SlowestFunctionsWidgetProps) {
  43. const location = useLocation();
  44. const [expandedIndex, setExpandedIndex] = useState(0);
  45. const slowFnCursor = useMemo(
  46. () => decodeScalar(location.query[CURSOR_NAME]),
  47. [location.query]
  48. );
  49. const handleCursor = useCallback((cursor, pathname, query) => {
  50. browserHistory.push({
  51. pathname,
  52. query: {...query, [CURSOR_NAME]: cursor},
  53. });
  54. }, []);
  55. const functionsQuery = useProfileFunctions<FunctionsField>({
  56. fields: functionsFields,
  57. referrer: 'api.profiling.suspect-functions.list',
  58. sort: {
  59. key: 'sum()',
  60. order: 'desc',
  61. },
  62. query: userQuery,
  63. limit: MAX_FUNCTIONS,
  64. cursor: slowFnCursor,
  65. });
  66. const hasFunctions = (functionsQuery.data?.data?.length || 0) > 0;
  67. const totalsQuery = useProfileFunctions<TotalsField>({
  68. fields: totalsFields,
  69. referrer: 'api.profiling.suspect-functions.totals',
  70. sort: {
  71. key: 'sum()',
  72. order: 'desc',
  73. },
  74. query: userQuery,
  75. limit: MAX_FUNCTIONS,
  76. // make sure to query for the projects from the top functions
  77. projects: functionsQuery.isFetched
  78. ? [
  79. ...new Set(
  80. (functionsQuery.data?.data ?? []).map(func => func['project.id'] as number)
  81. ),
  82. ]
  83. : [],
  84. enabled: functionsQuery.isFetched && hasFunctions,
  85. });
  86. const isLoading = functionsQuery.isLoading || (hasFunctions && totalsQuery.isLoading);
  87. const isError = functionsQuery.isError || totalsQuery.isError;
  88. return (
  89. <WidgetContainer>
  90. <HeaderContainer>
  91. <HeaderTitleLegend>{t('Suspect Functions')}</HeaderTitleLegend>
  92. <Subtitle>{t('Slowest functions by total time spent.')}</Subtitle>
  93. <StyledPagination
  94. pageLinks={functionsQuery.getResponseHeader?.('Link') ?? null}
  95. size="xs"
  96. onCursor={handleCursor}
  97. />
  98. </HeaderContainer>
  99. <ContentContainer>
  100. {isLoading && (
  101. <StatusContainer>
  102. <LoadingIndicator />
  103. </StatusContainer>
  104. )}
  105. {isError && (
  106. <StatusContainer>
  107. <IconWarning data-test-id="error-indicator" color="gray300" size="lg" />
  108. </StatusContainer>
  109. )}
  110. {!isError && !isLoading && !hasFunctions && (
  111. <EmptyStateWarning>
  112. <p>{t('No functions found')}</p>
  113. </EmptyStateWarning>
  114. )}
  115. {hasFunctions && totalsQuery.isFetched && (
  116. <Accordion>
  117. {(functionsQuery.data?.data ?? []).map((f, i) => {
  118. const projectEntry = totalsQuery.data?.data?.find(
  119. row => row['project.id'] === f['project.id']
  120. );
  121. const projectTotalDuration = projectEntry?.['sum()'] ?? f['sum()'];
  122. return (
  123. <SlowestFunctionEntry
  124. key={`${f['project.id']}-${f.package}-${f.function}`}
  125. isExpanded={i === expandedIndex}
  126. setExpanded={() => setExpandedIndex(i)}
  127. func={f}
  128. totalDuration={projectTotalDuration as number}
  129. query={userQuery ?? ''}
  130. />
  131. );
  132. })}
  133. </Accordion>
  134. )}
  135. </ContentContainer>
  136. </WidgetContainer>
  137. );
  138. }
  139. interface SlowestFunctionEntryProps {
  140. func: EventsResultsDataRow<FunctionsField>;
  141. isExpanded: boolean;
  142. query: string;
  143. setExpanded: () => void;
  144. totalDuration: number;
  145. }
  146. const BARS = 10;
  147. function SlowestFunctionEntry({
  148. func,
  149. isExpanded,
  150. query,
  151. setExpanded,
  152. totalDuration,
  153. }: SlowestFunctionEntryProps) {
  154. const organization = useOrganization();
  155. const {projects} = useProjects();
  156. const project = projects.find(p => p.id === String(func['project.id']));
  157. const score = Math.ceil((((func['sum()'] as number) ?? 0) / totalDuration) * BARS);
  158. const palette = new Array(BARS).fill([CHART_PALETTE[0][0]]);
  159. const userQuery = useMemo(() => {
  160. const conditions = new MutableSearch(query);
  161. conditions.setFilterValues('project.id', [String(func['project.id'])]);
  162. conditions.setFilterValues('package', [String(func.package)]);
  163. conditions.setFilterValues('function', [String(func.function)]);
  164. return conditions.formatString();
  165. }, [func, query]);
  166. const functionTransactionsQuery = useProfileFunctions<FunctionTransactionField>({
  167. fields: functionTransactionsFields,
  168. referrer: 'api.profiling.suspect-functions.transactions',
  169. sort: {
  170. key: 'sum()',
  171. order: 'desc',
  172. },
  173. query: userQuery,
  174. limit: 5,
  175. enabled: isExpanded,
  176. });
  177. return (
  178. <Fragment>
  179. <AccordionItem>
  180. {project && (
  181. <Tooltip title={project.name}>
  182. <IdBadge project={project} avatarSize={16} hideName />
  183. </Tooltip>
  184. )}
  185. <FunctionName>
  186. <Tooltip title={func.package}>{func.function}</Tooltip>
  187. </FunctionName>
  188. <Tooltip
  189. title={tct('Appeared [count] times for a total self time of [totalSelfTime]', {
  190. count: <Count value={func['count()'] as number} />,
  191. totalSelfTime: (
  192. <PerformanceDuration nanoseconds={func['sum()'] as number} abbreviation />
  193. ),
  194. })}
  195. >
  196. <ScoreBar score={score} palette={palette} size={20} radius={0} />
  197. </Tooltip>
  198. <Button
  199. icon={<IconChevron size="xs" direction={isExpanded ? 'up' : 'down'} />}
  200. aria-label={t('Expand')}
  201. aria-expanded={isExpanded}
  202. size="zero"
  203. borderless
  204. onClick={() => setExpanded()}
  205. />
  206. </AccordionItem>
  207. {isExpanded && (
  208. <Fragment>
  209. {functionTransactionsQuery.isError && (
  210. <StatusContainer>
  211. <IconWarning data-test-id="error-indicator" color="gray300" size="lg" />
  212. </StatusContainer>
  213. )}
  214. {functionTransactionsQuery.isLoading && (
  215. <StatusContainer>
  216. <LoadingIndicator />
  217. </StatusContainer>
  218. )}
  219. {functionTransactionsQuery.isFetched && (
  220. <TransactionsList data-test-id="transactions-list">
  221. <TransactionsListHeader>
  222. <TextOverflow>{t('Transaction')}</TextOverflow>
  223. </TransactionsListHeader>
  224. <TransactionsListHeader align="right">
  225. <TextOverflow>{t('Count')}</TextOverflow>
  226. </TransactionsListHeader>
  227. <TransactionsListHeader align="right">
  228. <TextOverflow>{t('Total Self Time')}</TextOverflow>
  229. </TransactionsListHeader>
  230. {(functionTransactionsQuery.data?.data ?? []).map(transaction => {
  231. const examples = transaction['examples()'] as string[];
  232. let transactionCol = <Fragment>{transaction.transaction}</Fragment>;
  233. if (project && examples.length) {
  234. const target = generateProfileFlamechartRouteWithQuery({
  235. orgSlug: organization.slug,
  236. projectSlug: project.slug,
  237. profileId: examples[0],
  238. query: {
  239. frameName: func.function as string,
  240. framePackage: func.package as string,
  241. },
  242. });
  243. transactionCol = <Link to={target}>{transactionCol}</Link>;
  244. }
  245. return (
  246. <Fragment key={transaction.transaction as string}>
  247. <TransactionsListCell>
  248. <TextOverflow>{transactionCol}</TextOverflow>
  249. </TransactionsListCell>
  250. <TransactionsListCell align="right">
  251. <Count value={transaction['count()'] as number} />
  252. </TransactionsListCell>
  253. <TransactionsListCell align="right">
  254. <PerformanceDuration
  255. nanoseconds={transaction['sum()'] as number}
  256. abbreviation
  257. />
  258. </TransactionsListCell>
  259. </Fragment>
  260. );
  261. })}
  262. </TransactionsList>
  263. )}
  264. </Fragment>
  265. )}
  266. </Fragment>
  267. );
  268. }
  269. const functionsFields = [
  270. 'project.id',
  271. 'package',
  272. 'function',
  273. 'count()',
  274. 'sum()',
  275. ] as const;
  276. type FunctionsField = (typeof functionsFields)[number];
  277. const totalsFields = ['project.id', 'sum()'] as const;
  278. type TotalsField = (typeof totalsFields)[number];
  279. const functionTransactionsFields = [
  280. 'transaction',
  281. 'count()',
  282. 'sum()',
  283. 'examples()',
  284. ] as const;
  285. type FunctionTransactionField = (typeof functionTransactionsFields)[number];
  286. const StyledPagination = styled(Pagination)`
  287. margin: 0;
  288. `;
  289. const FunctionName = styled(TextOverflow)`
  290. flex: 1 1 auto;
  291. `;
  292. const TransactionsList = styled('div')`
  293. flex: 1 1 auto;
  294. display: grid;
  295. grid-template-columns: 65% 10% 25%;
  296. grid-template-rows: 18px auto auto auto auto auto;
  297. padding: ${space(0)} ${space(2)};
  298. `;
  299. const TransactionsListHeader = styled('span')<{
  300. align?: CSSProperties['textAlign'];
  301. }>`
  302. text-transform: uppercase;
  303. font-size: ${p => p.theme.fontSizeExtraSmall};
  304. font-weight: 600;
  305. color: ${p => p.theme.subText};
  306. text-align: ${p => p.align};
  307. `;
  308. const TransactionsListCell = styled('div')<{align?: CSSProperties['textAlign']}>`
  309. font-size: ${p => p.theme.fontSizeSmall};
  310. text-align: ${p => p.align};
  311. padding: ${space(0.5)} 0px;
  312. `;