profilingSlowestTransactionsPanel.tsx 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. import {useMemo, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import {PlatformIcon} from 'platformicons';
  4. import {Button} from 'sentry/components/button';
  5. import EmptyStateWarning from 'sentry/components/emptyStateWarning';
  6. import Link from 'sentry/components/links/link';
  7. import LoadingIndicator from 'sentry/components/loadingIndicator';
  8. import Panel from 'sentry/components/panels/panel';
  9. import PerformanceDuration from 'sentry/components/performanceDuration';
  10. import {Flex} from 'sentry/components/profiling/flex';
  11. import {
  12. FunctionsMiniGrid,
  13. FunctionsMiniGridEmptyState,
  14. FunctionsMiniGridLoading,
  15. } from 'sentry/components/profiling/functionsMiniGrid';
  16. import {TextTruncateOverflow} from 'sentry/components/profiling/textTruncateOverflow';
  17. import {IconChevron} from 'sentry/icons';
  18. import {t} from 'sentry/locale';
  19. import {space} from 'sentry/styles/space';
  20. import type {Organization, Project} from 'sentry/types';
  21. import {trackAnalytics} from 'sentry/utils/analytics';
  22. import {getAggregateAlias} from 'sentry/utils/discover/fields';
  23. import type {
  24. EventsResults,
  25. EventsResultsDataRow,
  26. } from 'sentry/utils/profiling/hooks/types';
  27. import {useProfileEvents} from 'sentry/utils/profiling/hooks/useProfileEvents';
  28. import {useProfilingTransactionQuickSummary} from 'sentry/utils/profiling/hooks/useProfilingTransactionQuickSummary';
  29. import {generateProfileSummaryRouteWithQuery} from 'sentry/utils/profiling/routes';
  30. import {makeFormatTo} from 'sentry/utils/profiling/units/units';
  31. import {useLocation} from 'sentry/utils/useLocation';
  32. import useOrganization from 'sentry/utils/useOrganization';
  33. import useProjects from 'sentry/utils/useProjects';
  34. const fields = ['transaction', 'project.id', 'last_seen()', 'p95()', 'count()'] as const;
  35. type SlowestTransactionsFields = (typeof fields)[number];
  36. export function ProfilingSlowestTransactionsPanel() {
  37. const profilingTransactionsQuery = useProfileEvents({
  38. fields,
  39. sort: {
  40. key: 'p95()',
  41. order: 'desc',
  42. },
  43. limit: 3,
  44. query: 'count():>3',
  45. referrer: 'api.profiling.landing-slowest-transaction-panel',
  46. });
  47. const [openPanel, setOpenPanel] = useState<null | string>(null);
  48. const profilingTransactions = useMemo(
  49. () => profilingTransactionsQuery.data?.data ?? [],
  50. [profilingTransactionsQuery.data]
  51. );
  52. const transactionNames = useMemo(
  53. () => profilingTransactions.map(txn => txn.transaction),
  54. [profilingTransactions]
  55. );
  56. if (transactionNames.length > 0 && !transactionNames.includes(openPanel)) {
  57. const firstTransaction = transactionNames[0];
  58. setOpenPanel(firstTransaction as string);
  59. }
  60. const {isLoading} = profilingTransactionsQuery;
  61. const hasProfilingTransactions =
  62. !isLoading && profilingTransactions && profilingTransactions.length > 0;
  63. return (
  64. <FlexPanel>
  65. <Flex column h="100%">
  66. <Flex column p={space(1.5)}>
  67. <PanelHeading>{t('Slowest Transactions')}</PanelHeading>
  68. <PanelSubheading>
  69. {t('Slowest transactions that could use some optimization.')}
  70. </PanelSubheading>
  71. </Flex>
  72. {(isLoading || !hasProfilingTransactions) && (
  73. <Flex column align="center" justify="center" h="100%">
  74. {isLoading ? (
  75. <LoadingIndicator />
  76. ) : (
  77. !hasProfilingTransactions && (
  78. <Flex.Item>
  79. <EmptyStateWarning>
  80. <p>{t('No results found')}</p>
  81. <EmptyStateDescription>
  82. {t(
  83. 'Transactions may not be listed due to the filters above or a low number of profiles.'
  84. )}
  85. </EmptyStateDescription>
  86. </EmptyStateWarning>
  87. </Flex.Item>
  88. )
  89. )}
  90. </Flex>
  91. )}
  92. {profilingTransactions?.map(transaction => {
  93. return (
  94. <SlowestTransactionPanelItem
  95. key={transaction.transaction as string}
  96. transaction={transaction}
  97. open={transaction.transaction === openPanel}
  98. onOpen={() => setOpenPanel(transaction.transaction as string)}
  99. units={profilingTransactionsQuery.data?.meta.units}
  100. />
  101. );
  102. })}
  103. </Flex>
  104. </FlexPanel>
  105. );
  106. }
  107. interface SlowestTransactionPanelItemProps {
  108. onOpen: () => void;
  109. open: boolean;
  110. transaction: EventsResultsDataRow<SlowestTransactionsFields>;
  111. units?: EventsResults<SlowestTransactionsFields>['meta']['units'];
  112. }
  113. function SlowestTransactionPanelItem({
  114. transaction,
  115. open,
  116. onOpen,
  117. units,
  118. }: SlowestTransactionPanelItemProps) {
  119. const {query} = useLocation();
  120. const organization = useOrganization();
  121. const projects = useProjects();
  122. const transactionProject = useMemo(
  123. () => projects.projects.find(p => p.id === String(transaction['project.id'])),
  124. [projects.projects, transaction]
  125. );
  126. if (!transactionProject && !projects.fetching && projects.projects.length > 0) {
  127. return null;
  128. }
  129. const key: SlowestTransactionsFields = 'p95()';
  130. const formatter = makeFormatTo(
  131. units?.[key] ?? units?.[getAggregateAlias(key)] ?? 'nanoseconds',
  132. 'milliseconds'
  133. );
  134. return (
  135. <PanelItem key={transaction.transaction as string}>
  136. <Flex justify="space-between" gap={space(1)}>
  137. <PlatformIcon platform={transactionProject?.platform ?? 'default'} />
  138. <Flex.Item
  139. grow={1}
  140. onClick={onOpen}
  141. css={{
  142. cursor: 'pointer',
  143. }}
  144. >
  145. <div
  146. css={{
  147. maxWidth: 'fit-content',
  148. }}
  149. >
  150. <Link
  151. to={generateProfileSummaryRouteWithQuery({
  152. query,
  153. orgSlug: organization.slug,
  154. projectSlug: transactionProject?.slug!,
  155. transaction: transaction.transaction as string,
  156. })}
  157. onClick={() => {
  158. trackAnalytics('profiling_views.go_to_transaction', {
  159. source: 'slowest_transaction_panel',
  160. organization,
  161. });
  162. }}
  163. >
  164. <TextTruncateOverflow>
  165. {transaction.transaction as string}
  166. </TextTruncateOverflow>
  167. </Link>
  168. </div>
  169. </Flex.Item>
  170. <PerformanceDuration
  171. milliseconds={formatter(transaction[key] as number)}
  172. abbreviation
  173. />
  174. <Button borderless size="zero" onClick={onOpen}>
  175. <IconChevron direction={open ? 'up' : 'down'} size="xs" />
  176. </Button>
  177. </Flex>
  178. <PanelItemBody
  179. style={{
  180. height: open ? 160 : 0,
  181. }}
  182. >
  183. {open && transactionProject && (
  184. <PanelItemFunctionsMiniGrid
  185. transaction={String(transaction.transaction)}
  186. organization={organization}
  187. project={transactionProject}
  188. />
  189. )}
  190. </PanelItemBody>
  191. </PanelItem>
  192. );
  193. }
  194. interface PanelItemFunctionsMiniGridProps {
  195. organization: Organization;
  196. project: Project;
  197. transaction: string;
  198. }
  199. function PanelItemFunctionsMiniGrid(props: PanelItemFunctionsMiniGridProps) {
  200. const {transaction, project, organization} = props;
  201. const {functionsQuery, functions} = useProfilingTransactionQuickSummary({
  202. transaction,
  203. project,
  204. referrer: 'api.profiling.landing-slowest-transaction-panel',
  205. skipLatestProfile: true,
  206. skipSlowestProfile: true,
  207. });
  208. if (functionsQuery.isLoading) {
  209. return <FunctionsMiniGridLoading />;
  210. }
  211. if (!functions || (functions && functions.length === 0)) {
  212. return <FunctionsMiniGridEmptyState />;
  213. }
  214. return (
  215. <PanelItemBodyInner>
  216. <FunctionsMiniGrid
  217. functions={functions}
  218. organization={organization}
  219. project={project}
  220. onLinkClick={() =>
  221. trackAnalytics('profiling_views.go_to_flamegraph', {
  222. organization,
  223. source: 'slowest_transaction_panel',
  224. })
  225. }
  226. />
  227. </PanelItemBodyInner>
  228. );
  229. }
  230. const FlexPanel = styled(Panel)`
  231. display: flex;
  232. flex-direction: column;
  233. `;
  234. const PanelHeading = styled('span')`
  235. font-size: ${p => p.theme.text.cardTitle.fontSize};
  236. font-weight: ${p => p.theme.text.cardTitle.fontWeight};
  237. line-height: ${p => p.theme.text.cardTitle.lineHeight};
  238. `;
  239. const PanelSubheading = styled('span')`
  240. color: ${p => p.theme.subText};
  241. `;
  242. const PanelItem = styled('div')`
  243. padding: ${space(1)} ${space(1.5)};
  244. border-top: 1px solid ${p => p.theme.border};
  245. `;
  246. const PanelItemBody = styled('div')`
  247. transition: height 0.1s ease;
  248. width: 100%;
  249. overflow: hidden;
  250. `;
  251. // TODO: simple layout stuff like this should come from a primitive component and we should really stop this `styled` nonsense
  252. const PanelItemBodyInner = styled('div')`
  253. padding-top: ${space(1.5)};
  254. `;
  255. const EmptyStateDescription = styled('div')`
  256. font-size: ${p => p.theme.fontSizeMedium};
  257. `;