import {Fragment, useCallback, useMemo} from 'react'; import styled from '@emotion/styled'; import {Button, LinkButton} from 'sentry/components/button'; import ErrorPanel from 'sentry/components/charts/errorPanel'; import {SectionHeading} from 'sentry/components/charts/styles'; import {CompactSelect} from 'sentry/components/compactSelect'; import type {SelectOption} from 'sentry/components/compactSelect/types'; import Count from 'sentry/components/count'; import {DateTime} from 'sentry/components/dateTime'; 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 {AggregateFlamegraph} from 'sentry/components/profiling/flamegraph/aggregateFlamegraph'; import {AggregateFlamegraphTreeTable} from 'sentry/components/profiling/flamegraph/aggregateFlamegraphTreeTable'; import {FlamegraphSearch} from 'sentry/components/profiling/flamegraph/flamegraphToolbar/flamegraphSearch'; import {SegmentedControl} from 'sentry/components/segmentedControl'; import {IconProfiling, IconWarning} from 'sentry/icons'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import type {DeepPartial} from 'sentry/types/utils'; import {trackAnalytics} from 'sentry/utils/analytics'; import {browserHistory} from 'sentry/utils/browserHistory'; import {generateLinkToEventInTraceView} from 'sentry/utils/discover/urls'; import {getShortEventId} from 'sentry/utils/events'; import {isEmptyObject} from 'sentry/utils/object/isEmptyObject'; import type {CanvasScheduler} from 'sentry/utils/profiling/canvasScheduler'; import { CanvasPoolManager, useCanvasScheduler, } from 'sentry/utils/profiling/canvasScheduler'; import type {FlamegraphState} from 'sentry/utils/profiling/flamegraph/flamegraphStateProvider/flamegraphContext'; import {FlamegraphStateProvider} from 'sentry/utils/profiling/flamegraph/flamegraphStateProvider/flamegraphContextProvider'; import {FlamegraphThemeProvider} from 'sentry/utils/profiling/flamegraph/flamegraphThemeProvider'; import type {Frame} from 'sentry/utils/profiling/frame'; import {isEventedProfile, isSampledProfile} from 'sentry/utils/profiling/guards/profile'; import {useAggregateFlamegraphQuery} from 'sentry/utils/profiling/hooks/useAggregateFlamegraphQuery'; import type {ProfilingFieldType} from 'sentry/utils/profiling/hooks/useProfileEvents'; import {useProfileEvents} from 'sentry/utils/profiling/hooks/useProfileEvents'; import {decodeScalar} from 'sentry/utils/queryString'; import {useLocalStorageState} from 'sentry/utils/useLocalStorageState'; import {useLocation} from 'sentry/utils/useLocation'; import useOrganization from 'sentry/utils/useOrganization'; import {TraceViewSources} from 'sentry/views/performance/newTraceDetails/traceMetadataHeader'; import { FlamegraphProvider, useFlamegraph, } from 'sentry/views/profiling/flamegraphProvider'; import {ProfileGroupProvider} from 'sentry/views/profiling/profileGroupProvider'; import {generateProfileLink} from '../utils'; const DEFAULT_FLAMEGRAPH_PREFERENCES: DeepPartial = { preferences: { sorting: 'left heavy' satisfies FlamegraphState['preferences']['sorting'], }, }; const noop = () => void 0; interface TransactionProfilesContentProps { query: string; transaction: string; } export function TransactionProfilesContent(props: TransactionProfilesContentProps) { return ( ); } function isEmpty(resp: Profiling.Schema) { const profile = resp.profiles[0]; if (!profile) return true; if ( resp.profiles.length === 1 && isSampledProfile(profile) && profile.startValue === 0 && profile.endValue === 0 ) { return true; } if ( resp.profiles.length === 1 && isEventedProfile(profile) && profile.startValue === 0 && profile.endValue === 0 ) { return true; } return false; } function ProfileVisualization({query}: TransactionProfilesContentProps) { const {data, isPending, isError} = useAggregateFlamegraphQuery({ query, }); const [frameFilter, setFrameFilter] = useLocalStorageState< 'system' | 'application' | 'all' >('flamegraph-frame-filter', 'application'); const onFrameFilterChange = useCallback( (value: 'system' | 'application' | 'all') => { setFrameFilter(value); }, [setFrameFilter] ); const flamegraphFrameFilter: ((frame: Frame) => boolean) | undefined = useMemo(() => { if (frameFilter === 'all') { return () => true; } if (frameFilter === 'application') { return frame => frame.is_application; } return frame => !frame.is_application; }, [frameFilter]); const [visualization, setVisualization] = useLocalStorageState< 'flamegraph' | 'call tree' >('flamegraph-visualization', 'flamegraph'); const onVisualizationChange = useCallback( (value: 'flamegraph' | 'call tree') => { setVisualization(value); }, [setVisualization] ); const canvasPoolManager = useMemo(() => new CanvasPoolManager(), []); const scheduler = useCanvasScheduler(canvasPoolManager); return ( {visualization === 'flamegraph' ? ( ) : ( )} {isPending ? ( ) : isError ? ( {t('There was an error loading the flamegraph.')} ) : isEmpty(data) ? ( {t('No profiling data found')} ) : null} ); } interface AggregateFlamegraphToolbarProps { canvasPoolManager: CanvasPoolManager; frameFilter: 'system' | 'application' | 'all'; hideSystemFrames: boolean; onFrameFilterChange: (value: 'system' | 'application' | 'all') => void; onVisualizationChange: (value: 'flamegraph' | 'call tree') => void; scheduler: CanvasScheduler; setHideSystemFrames: (value: boolean) => void; visualization: 'flamegraph' | 'call tree'; } function AggregateFlamegraphToolbar(props: AggregateFlamegraphToolbarProps) { const flamegraph = useFlamegraph(); const flamegraphs = useMemo(() => [flamegraph], [flamegraph]); const spans = useMemo(() => [], []); const frameSelectOptions: SelectOption<'system' | 'application' | 'all'>[] = useMemo(() => { return [ {value: 'system', label: t('System Frames')}, {value: 'application', label: t('Application Frames')}, {value: 'all', label: t('All Frames')}, ]; }, []); const onResetZoom = useCallback(() => { props.scheduler.dispatch('reset zoom'); }, [props.scheduler]); const onFrameFilterChange = useCallback( (value: {value: 'application' | 'system' | 'all'}) => { props.onFrameFilterChange(value.value); }, [props] ); return ( {t('Flamegraph')} {t('Call Tree')} ); } const PERCENTILE_DIGESTS = [ 'p50()', 'p75()', 'p95()', 'p99()', ] satisfies ProfilingFieldType[]; const ALL_DIGESTS = [ ...PERCENTILE_DIGESTS, 'last_seen()', 'count()', ] satisfies ProfilingFieldType[]; function ProfileDigest({query}: TransactionProfilesContentProps) { const organization = useOrganization(); const profilesSummary = useProfileEvents({ fields: ALL_DIGESTS, query, sort: {key: 'last_seen()', order: 'desc'}, referrer: 'api.profiling.profile-summary-table', continuousProfilingCompat: organization.features.includes( 'continuous-profiling-compat' ), }); const digestData = profilesSummary.data?.data?.[0]; return ( {t('Last Seen')} {profilesSummary.isPending ? ( '' ) : profilesSummary.isError ? ( '' ) : ( )} {t('Count')} {profilesSummary.isPending ? ( '' ) : profilesSummary.isError ? ( '' ) : ( )} {PERCENTILE_DIGESTS.map(percentile => ( {percentile.substring(0, percentile.length - 2)} {profilesSummary.isPending ? ( '' ) : profilesSummary.isError ? ( '' ) : ( )} ))} ); } const ALLOWED_SORTS = [ '-timestamp', 'timestamp', '-transaction.duration', 'transaction.duration', ] as const; type SortOption = (typeof ALLOWED_SORTS)[number]; const sortOptions: SelectOption[] = [ {value: '-timestamp', label: t('Newest Events')}, {value: 'timestamp', label: t('Oldest Events')}, {value: '-transaction.duration', label: t('Slowest Events')}, {value: 'transaction.duration', label: t('Fastest Events')}, ]; const PROFILES_SORT = 'profilesSort'; const PROFILES_CURSOR = 'profilesCursor'; function ProfileList({query: userQuery, transaction}: TransactionProfilesContentProps) { const location = useLocation(); const organization = useOrganization(); const sortValue = useMemo(() => { const rawSort = decodeScalar(location.query[PROFILES_SORT]); if (ALLOWED_SORTS.includes(rawSort as any)) { return rawSort as SortOption; } return '-timestamp' as const; }, [location.query]); const sort = useMemo(() => { if (sortValue === '-timestamp') { return {key: 'timestamp', order: 'desc'} as const; } if (sortValue === 'timestamp') { return {key: 'timestamp', order: 'asc'} as const; } if (sortValue === '-transaction.duration') { return {key: 'transaction.duration', order: 'desc'} as const; } if (sortValue === 'transaction.duration') { return {key: 'transaction.duration', order: 'asc'} as const; } throw new Error(`Unsupport sort: ${sortValue}`); }, [sortValue]); const cursor = useMemo( () => decodeScalar(location.query[PROFILES_CURSOR]), [location.query] ); const profilesList = useProfileEvents({ fields: [ 'id', 'project.name', 'trace', 'transaction.duration', 'profile.id', 'profiler.id', 'thread.id', 'precise.start_ts', 'precise.finish_ts', 'timestamp', ], query: userQuery, sort, referrer: 'api.profiling.profile-summary-table', cursor, limit: 10, continuousProfilingCompat: organization.features.includes( 'continuous-profiling-compat' ), }); const handleSort = useCallback( (value: {value: SortOption}) => { browserHistory.push({ ...location, query: { ...location.query, [PROFILES_SORT]: value.value, [PROFILES_CURSOR]: undefined, }, }); }, [location] ); const handleCursor = useCallback((newCursor, pathname, query) => { browserHistory.push({ pathname, query: {...query, [PROFILES_CURSOR]: newCursor}, }); }, []); return ( {profilesList.isPending ? ( ) : profilesList.isError ? ( ) : ( {t('Event ID')} {t('Duration')} {t('Profile')} {profilesList.data?.data?.map(row => { const traceTarget = generateLinkToEventInTraceView({ eventId: row.id as string, timestamp: row.timestamp as string, traceSlug: row.trace as string, projectSlug: row['project.name'] as string, location, organization, transactionName: transaction, source: TraceViewSources.PERFORMANCE_TRANSACTION_SUMMARY_PROFILES, }); const profileTarget = generateProfileLink()( organization, { id: row.id as string, 'project.name': row['project.name'] as string, 'profile.id': (row['profile.id'] as string) || '', 'profiler.id': (row['profiler.id'] as string) || '', 'thread.id': (row['thread.id'] as string) || '', 'precise.start_ts': row['precise.start_ts'] as number, 'precise.finish_ts': row['precise.finish_ts'] as number, trace: row.trace as string, }, location ); return ( {getShortEventId(row.id as string)} { trackAnalytics('profiling_views.go_to_flamegraph', { organization, source: 'profiling_transaction.profiles_table', }); }} size="xs" > ); })} )} ); } const TransactionProfilesContentContainer = styled('div')` display: grid; /* false positive for grid layout */ /* stylelint-disable */ grid-template-areas: 'visualization digest'; grid-template-columns: 1fr 250px; flex: 1; border: 1px solid ${p => p.theme.border}; border-radius: ${p => p.theme.borderRadius}; overflow: hidden; `; const ProfileVisualizationContainer = styled('div')` grid-area: visualization; display: grid; grid-template-rows: min-content 1fr; height: 100%; position: relative; `; const FlamegraphContainer = styled('div')` overflow: hidden; display: flex; `; const RequestStateMessageContainer = styled('div')` position: absolute; left: 0; right: 0; top: 0; bottom: 0; display: flex; justify-content: center; align-items: center; color: ${p => p.theme.subText}; pointer-events: none; `; const AggregateFlamegraphToolbarContainer = styled('div')` display: flex; justify-content: space-between; gap: ${space(1)}; padding: ${space(1)}; background-color: ${p => p.theme.background}; /* force height to be the same as profile digest header, but subtract 1px for the border that doesnt exist on the header */ height: 41px; border-bottom: 1px solid ${p => p.theme.border}; `; const ViewSelectContainer = styled('div')` min-width: 160px; `; const AggregateFlamegraphSearch = styled(FlamegraphSearch)` max-width: 300px; `; const ProfileSidebarContainer = styled('div')` grid-area: digest; border-left: 1px solid ${p => p.theme.border}; background-color: ${p => p.theme.background}; position: relative; overflow: hidden; display: grid; grid-template-rows: min-content 1fr; `; const ProfileDigestContainer = styled('div')` display: grid; grid-template-columns: min-content 1fr; padding: ${space(2)}; gap: ${space(1)}; `; const ProfileListContainer = styled('div')` padding: ${space(2)}; border-top: 1px solid ${p => p.theme.border}; `; const ProfileListControls = styled('div')` display: flex; justify-content: space-between; margin-bottom: ${space(2)}; `; const StyledPagination = styled(Pagination)` margin: 0; `; const ProfileListResultsContainer = styled('div')` display: grid; grid-template-columns: min-content 1fr min-content; gap: ${space(1)}; `; const ProfileDigestLabel = styled(SectionHeading)<{align?: 'left' | 'right'}>` margin: 0; text-transform: uppercase; text-wrap: nowrap; text-align: ${p => p.align ?? 'left'}; `; const ProfileDigestValue = styled('div')<{align: 'left' | 'right'}>` text-align: ${p => p.align}; `;