import type {CSSProperties, ReactNode} from 'react'; import {Fragment, useCallback, useMemo, useState} from 'react'; import {browserHistory} from 'react-router'; import styled from '@emotion/styled'; import {Button} from 'sentry/components/button'; import Count from 'sentry/components/count'; import EmptyStateWarning from 'sentry/components/emptyStateWarning'; import IdBadge from 'sentry/components/idBadge'; import Link from 'sentry/components/links/link'; import LoadingIndicator from 'sentry/components/loadingIndicator'; import Pagination from 'sentry/components/pagination'; import PerformanceDuration from 'sentry/components/performanceDuration'; import ScoreBar from 'sentry/components/scoreBar'; import TextOverflow from 'sentry/components/textOverflow'; import {Tooltip} from 'sentry/components/tooltip'; import {CHART_PALETTE} from 'sentry/constants/chartPalette'; import {IconChevron, IconWarning} from 'sentry/icons'; import {t, tct} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import {trackAnalytics} from 'sentry/utils/analytics'; import {Frame} from 'sentry/utils/profiling/frame'; import type {EventsResultsDataRow} from 'sentry/utils/profiling/hooks/types'; import {useProfileFunctions} from 'sentry/utils/profiling/hooks/useProfileFunctions'; import {generateProfileFlamechartRouteWithQuery} from 'sentry/utils/profiling/routes'; import {decodeScalar} from 'sentry/utils/queryString'; import {MutableSearch} from 'sentry/utils/tokenizeSearch'; import {useLocation} from 'sentry/utils/useLocation'; import useOrganization from 'sentry/utils/useOrganization'; import useProjects from 'sentry/utils/useProjects'; import { Accordion, AccordionItem, ContentContainer, HeaderContainer, HeaderTitleLegend, StatusContainer, Subtitle, WidgetContainer, } from './styles'; const MAX_FUNCTIONS = 3; const DEFAULT_CURSOR_NAME = 'slowFnCursor'; interface SlowestFunctionsWidgetProps { cursorName?: string; header?: ReactNode; userQuery?: string; widgetHeight?: string; } export function SlowestFunctionsWidget({ cursorName = DEFAULT_CURSOR_NAME, header, userQuery, widgetHeight, }: SlowestFunctionsWidgetProps) { const location = useLocation(); const [expandedIndex, setExpandedIndex] = useState(0); const slowFnCursor = useMemo( () => decodeScalar(location.query[cursorName]), [cursorName, location.query] ); const handleCursor = useCallback( (cursor, pathname, query) => { browserHistory.push({ pathname, query: {...query, [cursorName]: cursor}, }); }, [cursorName] ); const functionsQuery = useProfileFunctions({ fields: functionsFields, referrer: 'api.profiling.suspect-functions.list', sort: { key: 'sum()', order: 'desc', }, query: userQuery, limit: MAX_FUNCTIONS, cursor: slowFnCursor, }); const hasFunctions = (functionsQuery.data?.data?.length || 0) > 0; const totalsQuery = useProfileFunctions({ fields: totalsFields, referrer: 'api.profiling.suspect-functions.totals', sort: { key: 'sum()', order: 'desc', }, query: userQuery, limit: MAX_FUNCTIONS, // make sure to query for the projects from the top functions projects: functionsQuery.isFetched ? [ ...new Set( (functionsQuery.data?.data ?? []).map(func => func['project.id'] as number) ), ] : [], enabled: functionsQuery.isFetched && hasFunctions, }); const isLoading = functionsQuery.isLoading || (hasFunctions && totalsQuery.isLoading); const isError = functionsQuery.isError || totalsQuery.isError; return ( {header ?? {t('Slowest Functions')}} {t('Slowest functions by total self time spent.')} {isLoading && ( )} {isError && ( )} {!isError && !isLoading && !hasFunctions && (

{t('No functions found')}

)} {hasFunctions && totalsQuery.isFetched && ( {(functionsQuery.data?.data ?? []).map((f, i, l) => { const projectEntry = totalsQuery.data?.data?.find( row => row['project.id'] === f['project.id'] ); const projectTotalDuration = projectEntry?.['sum()'] ?? f['sum()']; return ( { const nextIndex = expandedIndex !== i ? i : (i + 1) % l.length; setExpandedIndex(nextIndex); }} func={f} totalDuration={projectTotalDuration as number} query={userQuery ?? ''} /> ); })} )}
); } interface SlowestFunctionEntryProps { func: EventsResultsDataRow; isExpanded: boolean; query: string; setExpanded: () => void; totalDuration: number; } const BARS = 10; function SlowestFunctionEntry({ func, isExpanded, query, setExpanded, totalDuration, }: SlowestFunctionEntryProps) { const organization = useOrganization(); const {projects} = useProjects(); const project = projects.find(p => p.id === String(func['project.id'])); const score = Math.ceil((((func['sum()'] as number) ?? 0) / totalDuration) * BARS); const palette = new Array(BARS).fill([CHART_PALETTE[0][0]]); const frame = useMemo(() => { return new Frame( { key: 0, name: func.function as string, package: func.package as string, }, // Ensures that the frame runs through the normalization code path project?.platform && /node|javascript/.test(project.platform) ? project.platform : undefined, 'aggregate' ); }, [func, project]); const userQuery = useMemo(() => { const conditions = new MutableSearch(query); conditions.setFilterValues('project.id', [String(func['project.id'])]); // it is more efficient to filter on the fingerprint // than it is to filter on the package + function conditions.setFilterValues('fingerprint', [String(func.fingerprint)]); return conditions.formatString(); }, [func, query]); const functionTransactionsQuery = useProfileFunctions({ fields: functionTransactionsFields, referrer: 'api.profiling.suspect-functions.transactions', sort: { key: 'sum()', order: 'desc', }, query: userQuery, limit: 5, enabled: isExpanded, }); return ( {project && ( )} {frame.name} , totalSelfTime: ( ), })} >