import {Fragment, useCallback, useEffect, useMemo, useState} from 'react'; import {browserHistory, Link} from 'react-router'; import styled from '@emotion/styled'; import Fuse from 'fuse.js'; import * as qs from 'query-string'; import GridEditable, { COL_WIDTH_UNDEFINED, GridColumnOrder, } from 'sentry/components/gridEditable'; import * as Layout from 'sentry/components/layouts/thirds'; import Pagination from 'sentry/components/pagination'; import SearchBar from 'sentry/components/searchBar'; import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle'; import {t} from 'sentry/locale'; import space from 'sentry/styles/space'; import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent'; import {Container, NumberContainer} from 'sentry/utils/discover/styles'; import {CallTreeNode} from 'sentry/utils/profiling/callTreeNode'; import {Profile} from 'sentry/utils/profiling/profile/profile'; import {generateProfileFlamechartRouteWithQuery} from 'sentry/utils/profiling/routes'; import {renderTableHead} from 'sentry/utils/profiling/tableRenderer'; import {makeFormatter} from 'sentry/utils/profiling/units/units'; import {decodeScalar} from 'sentry/utils/queryString'; import {useEffectAfterFirstRender} from 'sentry/utils/useEffectAfterFirstRender'; import {useLocation} from 'sentry/utils/useLocation'; import useOrganization from 'sentry/utils/useOrganization'; import {useParams} from 'sentry/utils/useParams'; import {useProfileGroup} from './profileGroupProvider'; function collectTopProfileFrames(profile: Profile) { const nodes: CallTreeNode[] = []; profile.forEach( node => { if (node.selfWeight > 0) { nodes.push(node); } }, () => {} ); return ( nodes .sort((a, b) => b.selfWeight - a.selfWeight) // take only the slowest nodes from each thread because the rest // aren't useful to display .slice(0, 500) .map(node => ({ symbol: node.frame.name, image: node.frame.image, thread: profile.threadId, type: node.frame.is_application ? 'application' : 'system', 'self weight': node.selfWeight, 'total weight': node.totalWeight, })) ); } const RESULTS_PER_PAGE = 50; function ProfileDetails() { const location = useLocation(); const [state] = useProfileGroup(); const organization = useOrganization(); useEffect(() => { trackAdvancedAnalyticsEvent('profiling_views.profile_summary', { organization, }); }, [organization]); const cursor = useMemo(() => { const cursorQuery = decodeScalar(location.query.cursor, ''); return parseInt(cursorQuery, 10) || 0; }, [location.query.cursor]); const query = useMemo(() => decodeScalar(location.query.query, ''), [location]); const allFunctions: TableDataRow[] = useMemo(() => { return state.type === 'resolved' ? state.data.profiles .flatMap(collectTopProfileFrames) // Self weight desc sort .sort((a, b) => b['self weight'] - a['self weight']) : []; }, [state]); const searchIndex = useMemo(() => { return new Fuse(allFunctions, { keys: ['symbol'], threshold: 0.3, }); }, [allFunctions]); const search = useCallback( (queryString: string) => { if (!queryString) { return allFunctions; } return searchIndex .search(queryString) .map(result => result.item) .sort((a, b) => b['self weight'] - a['self weight']); }, [searchIndex, allFunctions] ); const [slowestFunctions, setSlowestFunctions] = useState(() => { return search(query); }); useEffectAfterFirstRender(() => { setSlowestFunctions(search(query)); }, [allFunctions, query, search]); const pageLinks = useMemo(() => { const prevResults = cursor >= RESULTS_PER_PAGE ? 'true' : 'false'; const prevCursor = cursor >= RESULTS_PER_PAGE ? cursor - RESULTS_PER_PAGE : 0; const prevQuery = {...location.query, cursor: prevCursor}; const prevHref = `${location.pathname}${qs.stringify(prevQuery)}`; const prev = `<${prevHref}>; rel="previous"; results="${prevResults}"; cursor="${prevCursor}"`; const nextResults = cursor + RESULTS_PER_PAGE < slowestFunctions.length ? 'true' : 'false'; const nextCursor = cursor + RESULTS_PER_PAGE < slowestFunctions.length ? cursor + RESULTS_PER_PAGE : 0; const nextQuery = {...location.query, cursor: nextCursor}; const nextHref = `${location.pathname}${qs.stringify(nextQuery)}`; const next = `<${nextHref}>; rel="next"; results="${nextResults}"; cursor="${nextCursor}"`; return `${prev},${next}`; }, [cursor, location, slowestFunctions]); const handleSearch = useCallback( searchString => { browserHistory.replace({ ...location, query: { ...location.query, query: searchString, cursor: undefined, }, }); setSlowestFunctions(search(searchString)); }, [location, search] ); return ( COLUMNS[key])} columnSortBy={[]} grid={{ renderHeadCell: renderTableHead({ rightAlignedColumns: RIGHT_ALIGNED_COLUMNS, }), renderBodyCell: renderFunctionCell, }} location={location} /> ); } const RIGHT_ALIGNED_COLUMNS = new Set(['self weight', 'total weight']); const ActionBar = styled('div')` display: grid; gap: ${space(2)}; grid-template-columns: auto; margin-bottom: ${space(2)}; `; function renderFunctionCell( column: TableColumn, dataRow: TableDataRow, rowIndex: number, columnIndex: number ) { return ( ); } interface ProfilingFunctionsTableCellProps { column: TableColumn; columnIndex: number; dataRow: TableDataRow; rowIndex: number; } const formatter = makeFormatter('nanoseconds'); function ProfilingFunctionsTableCell({ column, dataRow, }: ProfilingFunctionsTableCellProps) { const value = dataRow[column.key]; const {orgId, projectId, eventId} = useParams(); switch (column.key) { case 'self weight': return {formatter(value)}; case 'total weight': return {formatter(value)}; case 'image': return {value ?? 'Unknown'}; case 'thread': { return ( {value} ); } default: return {value}; } } type TableColumnKey = | 'symbol' | 'image' | 'self weight' | 'total weight' | 'thread' | 'type'; type TableDataRow = Record; type TableColumn = GridColumnOrder; const COLUMN_ORDER: TableColumnKey[] = [ 'symbol', 'image', 'thread', 'type', 'self weight', 'total weight', ]; // TODO: looks like these column names change depending on the platform? const COLUMNS: Record = { symbol: { key: 'symbol', name: t('Symbol'), width: COL_WIDTH_UNDEFINED, }, image: { key: 'image', name: t('Binary'), width: COL_WIDTH_UNDEFINED, }, thread: { key: 'thread', name: t('Thread'), width: COL_WIDTH_UNDEFINED, }, type: { key: 'type', name: t('Type'), width: COL_WIDTH_UNDEFINED, }, 'self weight': { key: 'self weight', name: t('Self Weight'), width: COL_WIDTH_UNDEFINED, }, 'total weight': { key: 'total weight', name: t('Total Weight'), width: COL_WIDTH_UNDEFINED, }, }; export default ProfileDetails;