profileDetails.tsx 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. import {Fragment, useCallback, useEffect, useMemo, useState} from 'react';
  2. import {browserHistory, Link} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import Fuse from 'fuse.js';
  5. import * as qs from 'query-string';
  6. import GridEditable, {
  7. COL_WIDTH_UNDEFINED,
  8. GridColumnOrder,
  9. } from 'sentry/components/gridEditable';
  10. import * as Layout from 'sentry/components/layouts/thirds';
  11. import Pagination from 'sentry/components/pagination';
  12. import SearchBar from 'sentry/components/searchBar';
  13. import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
  14. import {t} from 'sentry/locale';
  15. import space from 'sentry/styles/space';
  16. import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
  17. import {Container, NumberContainer} from 'sentry/utils/discover/styles';
  18. import {CallTreeNode} from 'sentry/utils/profiling/callTreeNode';
  19. import {Profile} from 'sentry/utils/profiling/profile/profile';
  20. import {generateProfileFlamegraphRouteWithQuery} from 'sentry/utils/profiling/routes';
  21. import {makeFormatter} from 'sentry/utils/profiling/units/units';
  22. import {decodeScalar} from 'sentry/utils/queryString';
  23. import {useEffectAfterFirstRender} from 'sentry/utils/useEffectAfterFirstRender';
  24. import {useLocation} from 'sentry/utils/useLocation';
  25. import useOrganization from 'sentry/utils/useOrganization';
  26. import {useParams} from 'sentry/utils/useParams';
  27. import {useProfileGroup} from './profileGroupProvider';
  28. function collectTopProfileFrames(profile: Profile) {
  29. const nodes: CallTreeNode[] = [];
  30. profile.forEach(
  31. node => {
  32. if (node.selfWeight > 0) {
  33. nodes.push(node);
  34. }
  35. },
  36. () => {}
  37. );
  38. return (
  39. nodes
  40. .sort((a, b) => b.selfWeight - a.selfWeight)
  41. // take only the slowest nodes from each thread because the rest
  42. // aren't useful to display
  43. .slice(0, 500)
  44. .map(node => ({
  45. symbol: node.frame.name,
  46. image: node.frame.image,
  47. thread: profile.threadId,
  48. type: node.frame.is_application ? 'application' : 'system',
  49. 'self weight': node.selfWeight,
  50. 'total weight': node.totalWeight,
  51. }))
  52. );
  53. }
  54. const RESULTS_PER_PAGE = 50;
  55. function ProfileDetails() {
  56. const location = useLocation();
  57. const [state] = useProfileGroup();
  58. const organization = useOrganization();
  59. useEffect(() => {
  60. trackAdvancedAnalyticsEvent('profiling_views.profile_summary', {
  61. organization,
  62. });
  63. }, [organization]);
  64. const cursor = useMemo<number>(() => {
  65. const cursorQuery = decodeScalar(location.query.cursor, '');
  66. return parseInt(cursorQuery, 10) || 0;
  67. }, [location.query.cursor]);
  68. const query = useMemo<string>(() => decodeScalar(location.query.query, ''), [location]);
  69. const allFunctions: TableDataRow[] = useMemo(() => {
  70. return state.type === 'resolved'
  71. ? state.data.profiles
  72. .flatMap(collectTopProfileFrames)
  73. // Self weight desc sort
  74. .sort((a, b) => b['self weight'] - a['self weight'])
  75. : [];
  76. }, [state]);
  77. const searchIndex = useMemo(() => {
  78. return new Fuse(allFunctions, {
  79. keys: ['symbol'],
  80. threshold: 0.3,
  81. });
  82. }, [allFunctions]);
  83. const search = useCallback(
  84. (queryString: string) => {
  85. if (!queryString) {
  86. return allFunctions;
  87. }
  88. return searchIndex
  89. .search(queryString)
  90. .map(result => result.item)
  91. .sort((a, b) => b['self weight'] - a['self weight']);
  92. },
  93. [searchIndex, allFunctions]
  94. );
  95. const [slowestFunctions, setSlowestFunctions] = useState<TableDataRow[]>(() => {
  96. return search(query);
  97. });
  98. useEffectAfterFirstRender(() => {
  99. setSlowestFunctions(search(query));
  100. }, [allFunctions]);
  101. const pageLinks = useMemo(() => {
  102. const prevResults = cursor >= RESULTS_PER_PAGE ? 'true' : 'false';
  103. const prevCursor = cursor >= RESULTS_PER_PAGE ? cursor - RESULTS_PER_PAGE : 0;
  104. const prevQuery = {...location.query, cursor: prevCursor};
  105. const prevHref = `${location.pathname}${qs.stringify(prevQuery)}`;
  106. const prev = `<${prevHref}>; rel="previous"; results="${prevResults}"; cursor="${prevCursor}"`;
  107. const nextResults =
  108. cursor + RESULTS_PER_PAGE < slowestFunctions.length ? 'true' : 'false';
  109. const nextCursor =
  110. cursor + RESULTS_PER_PAGE < slowestFunctions.length ? cursor + RESULTS_PER_PAGE : 0;
  111. const nextQuery = {...location.query, cursor: nextCursor};
  112. const nextHref = `${location.pathname}${qs.stringify(nextQuery)}`;
  113. const next = `<${nextHref}>; rel="next"; results="${nextResults}"; cursor="${nextCursor}"`;
  114. return `${prev},${next}`;
  115. }, [cursor, location, slowestFunctions]);
  116. const handleSearch = useCallback(
  117. searchString => {
  118. browserHistory.replace({
  119. ...location,
  120. query: {
  121. ...location.query,
  122. query: searchString,
  123. cursor: undefined,
  124. },
  125. });
  126. setSlowestFunctions(search(searchString));
  127. },
  128. [location, search]
  129. );
  130. return (
  131. <Fragment>
  132. <SentryDocumentTitle
  133. title={t('Profiling \u2014 Details')}
  134. orgSlug={organization.slug}
  135. >
  136. <Layout.Body>
  137. <Layout.Main fullWidth>
  138. <ActionBar>
  139. <SearchBar
  140. defaultQuery=""
  141. query={query}
  142. placeholder={t('Search for frames')}
  143. onChange={handleSearch}
  144. />
  145. </ActionBar>
  146. <GridEditable
  147. title={t('Slowest Functions')}
  148. isLoading={state.type === 'loading'}
  149. error={state.type === 'errored'}
  150. data={slowestFunctions.slice(cursor, cursor + RESULTS_PER_PAGE)}
  151. columnOrder={COLUMN_ORDER.map(key => COLUMNS[key])}
  152. columnSortBy={[]}
  153. grid={{renderBodyCell: renderFunctionCell}}
  154. location={location}
  155. />
  156. <Pagination pageLinks={pageLinks} />
  157. </Layout.Main>
  158. </Layout.Body>
  159. </SentryDocumentTitle>
  160. </Fragment>
  161. );
  162. }
  163. const ActionBar = styled('div')`
  164. display: grid;
  165. gap: ${space(2)};
  166. grid-template-columns: auto;
  167. margin-bottom: ${space(2)};
  168. `;
  169. function renderFunctionCell(
  170. column: TableColumn,
  171. dataRow: TableDataRow,
  172. rowIndex: number,
  173. columnIndex: number
  174. ) {
  175. return (
  176. <ProfilingFunctionsTableCell
  177. column={column}
  178. dataRow={dataRow}
  179. rowIndex={rowIndex}
  180. columnIndex={columnIndex}
  181. />
  182. );
  183. }
  184. interface ProfilingFunctionsTableCellProps {
  185. column: TableColumn;
  186. columnIndex: number;
  187. dataRow: TableDataRow;
  188. rowIndex: number;
  189. }
  190. const formatter = makeFormatter('nanoseconds');
  191. function ProfilingFunctionsTableCell({
  192. column,
  193. dataRow,
  194. }: ProfilingFunctionsTableCellProps) {
  195. const value = dataRow[column.key];
  196. const {orgId, projectId, eventId} = useParams();
  197. switch (column.key) {
  198. case 'self weight':
  199. return <NumberContainer>{formatter(value)}</NumberContainer>;
  200. case 'total weight':
  201. return <NumberContainer>{formatter(value)}</NumberContainer>;
  202. case 'image':
  203. return <Container>{value ?? 'Unknown'}</Container>;
  204. case 'thread': {
  205. return (
  206. <Container>
  207. <Link
  208. to={generateProfileFlamegraphRouteWithQuery({
  209. orgSlug: orgId,
  210. projectSlug: projectId,
  211. profileId: eventId,
  212. query: {tid: dataRow.thread},
  213. })}
  214. >
  215. {value}
  216. </Link>
  217. </Container>
  218. );
  219. }
  220. default:
  221. return <Container>{value}</Container>;
  222. }
  223. }
  224. type TableColumnKey =
  225. | 'symbol'
  226. | 'image'
  227. | 'self weight'
  228. | 'total weight'
  229. | 'thread'
  230. | 'type';
  231. type TableDataRow = Record<TableColumnKey, any>;
  232. type TableColumn = GridColumnOrder<TableColumnKey>;
  233. const COLUMN_ORDER: TableColumnKey[] = [
  234. 'symbol',
  235. 'image',
  236. 'thread',
  237. 'type',
  238. 'self weight',
  239. 'total weight',
  240. ];
  241. // TODO: looks like these column names change depending on the platform?
  242. const COLUMNS: Record<TableColumnKey, TableColumn> = {
  243. symbol: {
  244. key: 'symbol',
  245. name: t('Symbol'),
  246. width: COL_WIDTH_UNDEFINED,
  247. },
  248. image: {
  249. key: 'image',
  250. name: t('Binary'),
  251. width: COL_WIDTH_UNDEFINED,
  252. },
  253. thread: {
  254. key: 'thread',
  255. name: t('Thread'),
  256. width: COL_WIDTH_UNDEFINED,
  257. },
  258. type: {
  259. key: 'type',
  260. name: t('Type'),
  261. width: COL_WIDTH_UNDEFINED,
  262. },
  263. 'self weight': {
  264. key: 'self weight',
  265. name: t('Self Weight'),
  266. width: COL_WIDTH_UNDEFINED,
  267. },
  268. 'total weight': {
  269. key: 'total weight',
  270. name: t('Total Weight'),
  271. width: COL_WIDTH_UNDEFINED,
  272. },
  273. };
  274. export default ProfileDetails;