slowestProfileFunctions.tsx 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. import {useCallback, useMemo, useState} from 'react';
  2. import {browserHistory} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import type {SelectOption} from 'sentry/components/compactSelect';
  5. import {CompactSelect} from 'sentry/components/compactSelect';
  6. import Count from 'sentry/components/count';
  7. import Link from 'sentry/components/links/link';
  8. import LoadingIndicator from 'sentry/components/loadingIndicator';
  9. import Pagination from 'sentry/components/pagination';
  10. import PerformanceDuration from 'sentry/components/performanceDuration';
  11. import {TextTruncateOverflow} from 'sentry/components/profiling/textTruncateOverflow';
  12. import {t, tn} from 'sentry/locale';
  13. import {space} from 'sentry/styles/space';
  14. import type {Organization, Project} from 'sentry/types';
  15. import {trackAnalytics} from 'sentry/utils/analytics';
  16. import {Frame} from 'sentry/utils/profiling/frame';
  17. import type {EventsResultsDataRow} from 'sentry/utils/profiling/hooks/types';
  18. import {useCurrentProjectFromRouteParam} from 'sentry/utils/profiling/hooks/useCurrentProjectFromRouteParam';
  19. import {useProfileFunctions} from 'sentry/utils/profiling/hooks/useProfileFunctions';
  20. import {formatSort} from 'sentry/utils/profiling/hooks/utils';
  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. const SLOWEST_FUNCTIONS_LIMIT = 15;
  27. const SLOWEST_FUNCTIONS_CURSOR = 'functionsCursor';
  28. const functionsFields = [
  29. 'package',
  30. 'function',
  31. 'count()',
  32. 'p75()',
  33. 'sum()',
  34. 'examples()',
  35. ] as const;
  36. type FunctionsField = (typeof functionsFields)[number];
  37. interface SlowestProfileFunctionsProps {
  38. transaction: string;
  39. }
  40. export function SlowestProfileFunctions(props: SlowestProfileFunctionsProps) {
  41. const organization = useOrganization();
  42. const project = useCurrentProjectFromRouteParam();
  43. const [functionType, setFunctionType] = useState<'application' | 'system' | 'all'>(
  44. 'application'
  45. );
  46. const location = useLocation();
  47. const functionsCursor = useMemo(
  48. () => decodeScalar(location.query[SLOWEST_FUNCTIONS_CURSOR]),
  49. [location.query]
  50. );
  51. const functionsSort = useMemo(
  52. () =>
  53. formatSort<FunctionsField>(
  54. decodeScalar(location.query.functionsSort),
  55. functionsFields,
  56. {
  57. key: 'sum()',
  58. order: 'desc',
  59. }
  60. ),
  61. [location.query.functionsSort]
  62. );
  63. const handleFunctionsCursor = useCallback((cursor, pathname, query) => {
  64. browserHistory.push({
  65. pathname,
  66. query: {...query, [SLOWEST_FUNCTIONS_CURSOR]: cursor},
  67. });
  68. }, []);
  69. const query = useMemo(() => {
  70. const conditions = new MutableSearch('');
  71. conditions.setFilterValues('transaction', [props.transaction]);
  72. if (functionType === 'application') {
  73. conditions.setFilterValues('is_application', ['1']);
  74. } else if (functionType === 'system') {
  75. conditions.setFilterValues('is_application', ['0']);
  76. }
  77. return conditions.formatString();
  78. }, [functionType, props.transaction]);
  79. const functionsQuery = useProfileFunctions<FunctionsField>({
  80. fields: functionsFields,
  81. referrer: 'api.profiling.profile-summary-functions-table',
  82. sort: functionsSort,
  83. query,
  84. limit: SLOWEST_FUNCTIONS_LIMIT,
  85. cursor: functionsCursor,
  86. });
  87. const onChangeFunctionType = useCallback(v => setFunctionType(v.value), []);
  88. const functions = functionsQuery.data?.data ?? [];
  89. const onSlowestFunctionClick = useCallback(() => {
  90. trackAnalytics('profiling_views.go_to_flamegraph', {
  91. organization,
  92. source: `profiling_transaction.slowest_functions_table`,
  93. });
  94. }, [organization]);
  95. return (
  96. <SlowestFunctionsContainer>
  97. <SlowestFunctionsTitleContainer>
  98. <SlowestFunctionsTypeSelect
  99. value={functionType}
  100. options={SLOWEST_FUNCTION_OPTIONS}
  101. onChange={onChangeFunctionType}
  102. triggerProps={TRIGGER_PROPS}
  103. offset={4}
  104. />
  105. <SlowestFunctionsPagination
  106. pageLinks={functionsQuery.getResponseHeader?.('Link')}
  107. onCursor={handleFunctionsCursor}
  108. size="xs"
  109. />
  110. </SlowestFunctionsTitleContainer>
  111. <SlowestFunctionsList>
  112. {functionsQuery.isLoading ? (
  113. <SlowestFunctionsQueryState>
  114. <LoadingIndicator size={36} />
  115. </SlowestFunctionsQueryState>
  116. ) : functionsQuery.isError ? (
  117. <SlowestFunctionsQueryState>
  118. {t('Failed to fetch slowest functions')}
  119. </SlowestFunctionsQueryState>
  120. ) : !functions.length ? (
  121. <SlowestFunctionsQueryState>
  122. {t('The fastest code is one that never runs.')}
  123. </SlowestFunctionsQueryState>
  124. ) : (
  125. functions.map((fn, i) => {
  126. return (
  127. <SlowestFunctionEntry
  128. key={i}
  129. func={fn}
  130. organization={organization}
  131. project={project}
  132. onSlowestFunctionClick={onSlowestFunctionClick}
  133. />
  134. );
  135. })
  136. )}
  137. </SlowestFunctionsList>
  138. </SlowestFunctionsContainer>
  139. );
  140. }
  141. interface SlowestFunctionEntryProps {
  142. func: EventsResultsDataRow<'function' | 'package' | 'count()' | 'p75()' | 'sum()'>;
  143. onSlowestFunctionClick: () => void;
  144. organization: Organization;
  145. project: Project | null;
  146. }
  147. function SlowestFunctionEntry(props: SlowestFunctionEntryProps) {
  148. const frame = useMemo(() => {
  149. return new Frame(
  150. {
  151. key: 0,
  152. name: props.func.function as string,
  153. package: props.func.package as string,
  154. },
  155. // Ensures that the frame runs through the normalization code path
  156. props.project?.platform && /node|javascript/.test(props.project.platform)
  157. ? props.project.platform
  158. : undefined,
  159. 'aggregate'
  160. );
  161. }, [props.func, props.project]);
  162. return (
  163. <SlowestFunctionRow>
  164. <SlowestFunctionMainRow>
  165. <div>
  166. <Link
  167. onClick={props.onSlowestFunctionClick}
  168. to={generateProfileFlamechartRouteWithQuery({
  169. orgSlug: props.organization.slug,
  170. projectSlug: props.project?.slug ?? '',
  171. profileId: (props.func['examples()']?.[0] as string) ?? '',
  172. query: {
  173. // specify the frame to focus, the flamegraph will switch
  174. // to the appropriate thread when these are specified
  175. frameName: frame.name as string,
  176. framePackage: frame.package as string,
  177. },
  178. })}
  179. >
  180. <TextTruncateOverflow>{frame.name}</TextTruncateOverflow>
  181. </Link>
  182. </div>
  183. <div>
  184. <PerformanceDuration nanoseconds={props.func['sum()'] as number} abbreviation />
  185. </div>
  186. </SlowestFunctionMainRow>
  187. <SlowestFunctionMetricsRow>
  188. <div>
  189. <TextTruncateOverflow>{frame.package}</TextTruncateOverflow>
  190. </div>
  191. <div>
  192. <Count value={props.func['count()'] as number} />{' '}
  193. {tn('time', 'times', props.func['count()'])}
  194. {', '}
  195. <PerformanceDuration nanoseconds={props.func['p75()'] as number} abbreviation />
  196. </div>
  197. </SlowestFunctionMetricsRow>
  198. </SlowestFunctionRow>
  199. );
  200. }
  201. const SlowestFunctionsList = styled('div')`
  202. flex-basis: 100%;
  203. overflow: auto;
  204. min-height: 0;
  205. `;
  206. const SlowestFunctionsContainer = styled('div')`
  207. margin-top: ${space(0.5)};
  208. min-height: 0;
  209. display: flex;
  210. flex-direction: column;
  211. padding: 0 ${space(1)};
  212. border-bottom: 1px solid ${p => p.theme.border};
  213. `;
  214. const SlowestFunctionsPagination = styled(Pagination)`
  215. margin: 0;
  216. button {
  217. height: 16px;
  218. width: 16px;
  219. min-width: 16px;
  220. min-height: 16px;
  221. svg {
  222. width: 10px;
  223. height: 10px;
  224. }
  225. }
  226. `;
  227. const SlowestFunctionsTitleContainer = styled('div')`
  228. display: flex;
  229. align-items: center;
  230. justify-content: space-between;
  231. margin-bottom: ${space(1)};
  232. `;
  233. const SlowestFunctionsTypeSelect = styled(CompactSelect)`
  234. button {
  235. margin: 0;
  236. padding: 0;
  237. }
  238. `;
  239. const SlowestFunctionsQueryState = styled('div')`
  240. text-align: center;
  241. padding: ${space(2)} ${space(0.5)};
  242. color: ${p => p.theme.subText};
  243. `;
  244. const SlowestFunctionRow = styled('div')`
  245. margin-bottom: ${space(1)};
  246. `;
  247. const SlowestFunctionMainRow = styled('div')`
  248. display: flex;
  249. align-items: center;
  250. justify-content: space-between;
  251. > div:first-child {
  252. min-width: 0;
  253. }
  254. > div:last-child {
  255. white-space: nowrap;
  256. }
  257. `;
  258. const SlowestFunctionMetricsRow = styled('div')`
  259. display: flex;
  260. align-items: center;
  261. justify-content: space-between;
  262. font-size: ${p => p.theme.fontSizeSmall};
  263. color: ${p => p.theme.subText};
  264. margin-top: ${space(0.25)};
  265. `;
  266. const TRIGGER_PROPS = {borderless: true, size: 'zero' as const};
  267. const SLOWEST_FUNCTION_OPTIONS: SelectOption<'application' | 'system' | 'all'>[] = [
  268. {
  269. label: t('Slowest Application Functions'),
  270. value: 'application' as const,
  271. },
  272. {
  273. label: t('Slowest System Functions'),
  274. value: 'system' as const,
  275. },
  276. {
  277. label: t('Slowest Functions'),
  278. value: 'all' as const,
  279. },
  280. ];