import type {ReactNode} from 'react'; import {Fragment, useCallback, useMemo, useState} from 'react'; import {useTheme} from '@emotion/react'; import styled from '@emotion/styled'; import {Button} from 'sentry/components/button'; import ChartZoom from 'sentry/components/charts/chartZoom'; import {LineChart} from 'sentry/components/charts/lineChart'; import Count from 'sentry/components/count'; import type {MenuItemProps} from 'sentry/components/dropdownMenu'; import {DropdownMenu} from 'sentry/components/dropdownMenu'; import EmptyStateWarning from 'sentry/components/emptyStateWarning'; import IdBadge from 'sentry/components/idBadge'; 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 TimeSince from 'sentry/components/timeSince'; import {Tooltip} from 'sentry/components/tooltip'; import {CHART_PALETTE} from 'sentry/constants/chartPalette'; import {IconChevron} from 'sentry/icons/iconChevron'; import {IconEllipsis} from 'sentry/icons/iconEllipsis'; import {IconWarning} from 'sentry/icons/iconWarning'; import {t, tct} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import type {Series} from 'sentry/types/echarts'; import type {EventsStatsSeries} from 'sentry/types/organization'; import {defined} from 'sentry/utils'; import {browserHistory} from 'sentry/utils/browserHistory'; import {axisLabelFormatter, tooltipFormatter} from 'sentry/utils/discover/charts'; import {getShortEventId} from 'sentry/utils/events'; 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 {useProfileTopEventsStats} from 'sentry/utils/profiling/hooks/useProfileTopEventsStats'; import {generateProfileRouteFromProfileReference} from 'sentry/utils/profiling/routes'; import type {UseApiQueryResult} from 'sentry/utils/queryClient'; import {decodeScalar} from 'sentry/utils/queryString'; import type RequestError from 'sentry/utils/requestError/requestError'; import {useLocation} from 'sentry/utils/useLocation'; import useOrganization from 'sentry/utils/useOrganization'; import usePageFilters from 'sentry/utils/usePageFilters'; import useProjects from 'sentry/utils/useProjects'; import {getProfileTargetId} from 'sentry/views/profiling/utils'; import { Accordion, AccordionItem, ContentContainer, HeaderContainer, HeaderTitleLegend, StatusContainer, Subtitle, WidgetContainer, } from './styles'; const MAX_FUNCTIONS = 3; const DEFAULT_CURSOR_NAME = 'slowFnCursor'; type BreakdownFunction = 'avg()' | 'p50()' | 'p75()' | 'p95()' | 'p99()'; type ChartFunctions = F | 'all_examples()'; interface SlowestFunctionsWidgetProps { breakdownFunction: F; cursorName?: string; header?: ReactNode; userQuery?: string; widgetHeight?: string; } export function SlowestFunctionsWidget({ breakdownFunction, 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 functionsData = functionsQuery.data?.data || []; const hasFunctions = (functionsData.length || 0) > 0; // make sure to query for the projects from the top functions const projects = functionsQuery.isFetched ? [ ...new Set( (functionsQuery.data?.data ?? []).map(func => func['project.id'] as number) ), ] : []; const totalsQuery = useProfileFunctions({ fields: totalsFields, referrer: 'api.profiling.suspect-functions.totals', sort: { key: 'sum()', order: 'desc', }, query: userQuery, limit: MAX_FUNCTIONS, projects, enabled: functionsQuery.isFetched && hasFunctions, }); const isLoading = functionsQuery.isPending || (hasFunctions && totalsQuery.isPending); const isError = functionsQuery.isError || totalsQuery.isError; const functionStats = useProfileTopEventsStats({ dataset: 'profileFunctions', fields: ['fingerprint', 'all_examples()', breakdownFunction], query: functionsData.map(f => `fingerprint:${f.fingerprint}`).join(' OR '), referrer: 'api.profiling.suspect-functions.stats', yAxes: ['all_examples()', breakdownFunction], projects, others: false, topEvents: functionsData.length, enabled: totalsQuery.isFetched && hasFunctions, }); 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 && ( {functionsData.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} stats={functionStats} totalDuration={projectTotalDuration as number} query={userQuery ?? ''} /> ); })} )}
); } interface SlowestFunctionEntryProps { breakdownFunction: BreakdownFunction; func: EventsResultsDataRow; isExpanded: boolean; query: string; setExpanded: () => void; totalDuration: number; stats?: UseApiQueryResult>, RequestError>; } const BARS = 10; function SlowestFunctionEntry({ breakdownFunction, func, isExpanded, setExpanded, stats, 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 examples: MenuItemProps[] = useMemo(() => { const rawExamples = stats?.data?.data?.find( s => s.axis === 'all_examples()' && s.label === String(func.fingerprint) ); if (!defined(rawExamples?.values)) { return []; } const timestamps = stats?.data?.timestamps ?? []; return rawExamples.values .map(values => (Array.isArray(values) ? values : [])) .flatMap((example, i) => { const timestamp = ( ); return example.slice(0, 1).map(profileRef => { const targetId = getProfileTargetId(profileRef); return { key: targetId, label: ( {getShortEventId(targetId)} {timestamp} ), textValue: targetId, to: generateProfileRouteFromProfileReference({ orgSlug: organization.slug, projectSlug: project?.slug || '', reference: profileRef, frameName: frame.name, framePackage: frame.package, }), }; }); }) .reverse() .slice(0, 10); }, [func, stats, organization, project, frame]); return (