import {useCallback, useEffect, useMemo, useState} from 'react'; import styled from '@emotion/styled'; import type {Location} from 'history'; import {Button, LinkButton} from 'sentry/components/button'; 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 ErrorBoundary from 'sentry/components/errorBoundary'; import SearchBar from 'sentry/components/events/searchBar'; import FeedbackWidgetButton from 'sentry/components/feedback/widget/feedbackWidgetButton'; import IdBadge from 'sentry/components/idBadge'; import * as Layout from 'sentry/components/layouts/thirds'; import Link from 'sentry/components/links/link'; import LoadingIndicator from 'sentry/components/loadingIndicator'; import {DatePageFilter} from 'sentry/components/organizations/datePageFilter'; import {EnvironmentPageFilter} from 'sentry/components/organizations/environmentPageFilter'; import PageFilterBar from 'sentry/components/organizations/pageFilterBar'; import PageFiltersContainer from 'sentry/components/organizations/pageFilters/container'; 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 type {ProfilingBreadcrumbsProps} from 'sentry/components/profiling/profilingBreadcrumbs'; import {ProfilingBreadcrumbs} from 'sentry/components/profiling/profilingBreadcrumbs'; import {SegmentedControl} from 'sentry/components/segmentedControl'; import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle'; import type {SmartSearchBarProps} from 'sentry/components/smartSearchBar'; import SmartSearchBar from 'sentry/components/smartSearchBar'; import {TabList, Tabs} from 'sentry/components/tabs'; import {MAX_QUERY_LENGTH} from 'sentry/constants'; import {IconPanel} from 'sentry/icons'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import type {PageFilters} from 'sentry/types/core'; import type {Organization} from 'sentry/types/organization'; import type {Project} from 'sentry/types/project'; import type {DeepPartial} from 'sentry/types/utils'; import {defined} from 'sentry/utils'; import {browserHistory} from 'sentry/utils/browserHistory'; import type EventView from 'sentry/utils/discover/eventView'; import {isAggregateField} from 'sentry/utils/discover/fields'; 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 {useAggregateFlamegraphQuery} from 'sentry/utils/profiling/hooks/useAggregateFlamegraphQuery'; import {useCurrentProjectFromRouteParam} from 'sentry/utils/profiling/hooks/useCurrentProjectFromRouteParam'; import {useProfileEvents} from 'sentry/utils/profiling/hooks/useProfileEvents'; import {useProfileFilters} from 'sentry/utils/profiling/hooks/useProfileFilters'; import {generateProfileFlamechartRoute} from 'sentry/utils/profiling/routes'; import {decodeScalar} from 'sentry/utils/queryString'; import {MutableSearch} from 'sentry/utils/tokenizeSearch'; import {useLocalStorageState} from 'sentry/utils/useLocalStorageState'; import {useLocation} from 'sentry/utils/useLocation'; import useOrganization from 'sentry/utils/useOrganization'; import usePageFilters from 'sentry/utils/usePageFilters'; import {transactionSummaryRouteWithQuery} from 'sentry/views/performance/transactionSummary/utils'; import { FlamegraphProvider, useFlamegraph, } from 'sentry/views/profiling/flamegraphProvider'; import {ProfilesSummaryChart} from 'sentry/views/profiling/landing/profilesSummaryChart'; import {ProfileGroupProvider} from 'sentry/views/profiling/profileGroupProvider'; import type {ProfilingFieldType} from 'sentry/views/profiling/profileSummary/content'; import {ProfilesTable} from 'sentry/views/profiling/profileSummary/profilesTable'; import {DEFAULT_PROFILING_DATETIME_SELECTION} from 'sentry/views/profiling/utils'; import {MostRegressedProfileFunctions} from './regressedProfileFunctions'; import {SlowestProfileFunctions} from './slowestProfileFunctions'; const noop = () => void 0; function decodeViewOrDefault( value: string | string[] | null | undefined, defaultValue: 'flamegraph' | 'profiles' ): 'flamegraph' | 'profiles' { if (!value || Array.isArray(value)) { return defaultValue; } if (value === 'flamegraph' || value === 'profiles') { return value; } return defaultValue; } const DEFAULT_FLAMEGRAPH_PREFERENCES: DeepPartial = { preferences: { sorting: 'alphabetical' satisfies FlamegraphState['preferences']['sorting'], }, }; interface ProfileSummaryHeaderProps { location: Location; onViewChange: (newView: 'flamegraph' | 'profiles') => void; organization: Organization; project: Project | null; query: string; transaction: string; view: 'flamegraph' | 'profiles'; } function ProfileSummaryHeader(props: ProfileSummaryHeaderProps) { const breadcrumbTrails: ProfilingBreadcrumbsProps['trails'] = useMemo(() => { return [ { type: 'landing', payload: { query: props.location.query, }, }, { type: 'profile summary', payload: { projectSlug: props.project?.slug ?? '', query: props.location.query, transaction: props.transaction, }, }, ]; }, [props.location.query, props.project?.slug, props.transaction]); const transactionSummaryTarget = props.project && props.transaction && transactionSummaryRouteWithQuery({ orgSlug: props.organization.slug, transaction: props.transaction, projectID: props.project.id, query: {query: props.query}, }); return ( {props.project ? ( ) : null} {props.transaction} {transactionSummaryTarget && ( {t('View Transaction Summary')} )} {t('Flamegraph')} {t('Sampled Profiles')} ); } const ProfilingHeader = styled(Layout.Header)` padding: ${space(1)} ${space(2)} ${space(0)} ${space(2)} !important; `; const ProfilingHeaderContent = styled(Layout.HeaderContent)` margin-bottom: ${space(1)}; h1 { line-height: normal; } `; const StyledHeaderActions = styled(Layout.HeaderActions)` display: flex; flex-direction: row; gap: ${space(1)}; `; const ProfilingTitleContainer = styled('div')` display: flex; align-items: center; gap: ${space(1)}; font-size: ${p => p.theme.fontSizeLarge}; `; interface ProfileFiltersProps { location: Location; organization: Organization; projectIds: EventView['project']; query: string; selection: PageFilters; transaction: string | undefined; usingTransactions: boolean; } function ProfileFilters(props: ProfileFiltersProps) { const filtersQuery = useMemo(() => { // To avoid querying for the filters each time the query changes, // do not pass the user query to get the filters. const search = new MutableSearch(''); if (defined(props.transaction)) { search.setFilterValues('transaction_name', [props.transaction]); } return search.formatString(); }, [props.transaction]); const profileFilters = useProfileFilters({ query: filtersQuery, selection: props.selection, disabled: props.usingTransactions, }); const handleSearch: SmartSearchBarProps['onSearch'] = useCallback( (searchQuery: string) => { browserHistory.push({ ...props.location, query: { ...props.location.query, query: searchQuery || undefined, cursor: undefined, }, }); }, [props.location] ); return ( {props.usingTransactions ? ( ) : ( )} ); } const ActionBar = styled('div')` display: grid; gap: ${space(1)}; grid-template-columns: min-content auto; padding: ${space(1)} ${space(1)}; background-color: ${p => p.theme.background}; `; interface ProfileSummaryPageProps { location: Location; params: { projectId?: Project['slug']; }; selection: PageFilters; view: 'flamegraph' | 'profile list'; } function ProfileSummaryPage(props: ProfileSummaryPageProps) { const organization = useOrganization(); const project = useCurrentProjectFromRouteParam(); const {selection} = usePageFilters(); const profilingUsingTransactions = organization.features.includes( 'profiling-using-transactions' ); const transaction = decodeScalar(props.location.query.transaction); if (!transaction) { throw new TypeError( `Profile summary requires a transaction query params, got ${ transaction?.toString() ?? transaction }` ); } const rawQuery = decodeScalar(props.location?.query?.query, ''); const projectIds: number[] = useMemo(() => { if (!defined(project)) { return []; } const projects = parseInt(project.id, 10); if (isNaN(projects)) { return []; } return [projects]; }, [project]); const projectSlugs: string[] = useMemo(() => { return defined(project) ? [project.slug] : []; }, [project]); const query = useMemo(() => { const search = new MutableSearch(rawQuery); if (defined(transaction)) { search.setFilterValues('transaction', [transaction]); } // there are no aggregations happening on this page, // so remove any aggregate filters Object.keys(search.filters).forEach(field => { if (isAggregateField(field)) { search.removeFilter(field); } }); return search.formatString(); }, [rawQuery, transaction]); const {data, isLoading, isError} = useAggregateFlamegraphQuery({ transaction, environments: selection.environments, projects: selection.projects, datetime: selection.datetime, }); const [visualization, setVisualization] = useLocalStorageState< 'flamegraph' | 'call tree' >('flamegraph-visualization', 'flamegraph'); const onVisualizationChange = useCallback( (value: 'flamegraph' | 'call tree') => { setVisualization(value); }, [setVisualization] ); const [hideRegressions, setHideRegressions] = useLocalStorageState( 'flamegraph-hide-regressions', false ); 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 canvasPoolManager = useMemo(() => new CanvasPoolManager(), []); const scheduler = useCanvasScheduler(canvasPoolManager); const location = useLocation(); const [view, setView] = useState<'flamegraph' | 'profiles'>( decodeViewOrDefault(location.query.view, 'flamegraph') ); useEffect(() => { const newView = decodeViewOrDefault(location.query.view, 'flamegraph'); if (newView !== view) { setView(decodeViewOrDefault(location.query.view, 'flamegraph')); } }, [location.query.view, view]); const onSetView = useCallback( (newView: 'flamegraph' | 'profiles') => { setView(newView); browserHistory.push({ ...location, query: { ...location.query, view: newView, }, }); }, [location] ); const onHideRegressionsClick = useCallback(() => { return setHideRegressions(!hideRegressions); }, [hideRegressions, setHideRegressions]); return ( {view === 'profiles' ? ( ) : ( {isLoading ? ( ) : isError ? ( {t('There was an error loading the flamegraph.')} ) : null} {visualization === 'flamegraph' ? ( ) : ( )} {hideRegressions ? null : ( )} )} ); } 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}; `; const AggregateFlamegraphContainer = styled('div')` display: flex; flex-direction: column; flex: 1 1 100%; height: 100%; width: 100%; overflow: hidden; position: absolute; left: 0px; top: 0px; `; interface AggregateFlamegraphToolbarProps { canvasPoolManager: CanvasPoolManager; frameFilter: 'system' | 'application' | 'all'; hideSystemFrames: boolean; onFrameFilterChange: (value: 'system' | 'application' | 'all') => void; onHideRegressionsClick: () => 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 ViewSelectContainer = styled('div')` min-width: 160px; `; const AggregateFlamegraphToolbarContainer = styled('div')` display: flex; justify-content: space-between; gap: ${space(1)}; padding: ${space(1)} ${space(0.5)}; 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; `; const AggregateFlamegraphSearch = styled(FlamegraphSearch)` max-width: 300px; `; const ProfileVisualization = styled('div')` grid-area: visualization; position: relative; height: 100%; `; const ProfileDigestContainer = styled('div')` grid-area: digest; border-left: 1px solid ${p => p.theme.border}; background-color: ${p => p.theme.background}; display: flex; flex: 1 1 100%; flex-direction: column; position: relative; overflow: hidden; `; const ProfileDigestScrollContainer = styled('div')` position: absolute; left: 0; right: 0; top: 0; bottom: 0; display: flex; flex-direction: column; `; const ProfileVisualizationContainer = styled('div')<{hideRegressions}>` display: grid; /* false positive for grid layout */ /* stylelint-disable */ grid-template-areas: ${p => p.hideRegressions ? "'visualization'" : "'visualization digest'"}; grid-template-columns: ${p => (p.hideRegressions ? `100%` : `60% 40%`)}; flex: 1 1 100%; `; const ProfileSummaryContainer = styled('div')` display: flex; flex-direction: column; flex: 1 1 100%; /* * The footer component is a sibling of this div. * Remove it so the flamegraph can take up the * entire screen. */ ~ footer { display: none; } `; const PROFILE_DIGEST_FIELDS = [ 'last_seen()', 'p75()', 'p95()', 'p99()', 'count()', ] satisfies ProfilingFieldType[]; const percentiles = ['p75()', 'p95()', 'p99()'] as const; interface ProfileDigestProps { onViewChange: (newView: 'flamegraph' | 'profiles') => void; transaction: string; } function ProfileDigest(props: ProfileDigestProps) { const location = useLocation(); const organization = useOrganization(); const project = useCurrentProjectFromRouteParam(); const query = useMemo(() => { const conditions = new MutableSearch(''); conditions.setFilterValues('transaction', [props.transaction]); return conditions.formatString(); }, [props.transaction]); const profilesCursor = useMemo( () => decodeScalar(location.query.cursor), [location.query.cursor] ); const profiles = useProfileEvents({ cursor: profilesCursor, fields: PROFILE_DIGEST_FIELDS, query, sort: {key: 'last_seen()', order: 'desc'}, referrer: 'api.profiling.profile-summary-table', }); const data = profiles.data?.data?.[0]; const latestProfile = useProfileEvents({ cursor: profilesCursor, fields: ['profile.id', 'timestamp'], query: '', sort: {key: 'timestamp', order: 'desc'}, limit: 1, referrer: 'api.profiling.profile-summary-table', }); const profile = latestProfile.data?.data?.[0]; const flamegraphTarget = project && profile ? generateProfileFlamechartRoute({ orgSlug: organization.slug, projectSlug: project.slug, profileId: profile?.['profile.id'] as string, }) : undefined; return (
{t('Last Seen')}
{profiles.isLoading ? ( '' ) : profiles.isError ? ( '' ) : flamegraphTarget ? ( ) : ( )}
{percentiles.map(p => { return ( {p}
{profiles.isLoading ? ( '' ) : profiles.isError ? ( '' ) : ( )}
); })} {t('profiles')}
{profiles.isLoading ? ( '' ) : profiles.isError ? ( '' ) : ( props.onViewChange('profiles')} to=""> )}
); } const ProfileDigestColumn = styled('div')` text-align: right; `; const ProfileDigestHeader = styled('div')` display: flex; justify-content: space-between; align-items: center; padding: 0 ${space(1)}; border-bottom: 1px solid ${p => p.theme.border}; /* force height to be same as toolbar */ height: 42px; flex-shrink: 0; `; const ProfileDigestLabel = styled('span')` color: ${p => p.theme.textColor}; font-size: ${p => p.theme.fontSizeSmall}; font-weight: ${p => p.theme.fontWeightBold}; text-transform: uppercase; `; export default function ProfileSummaryPageToggle(props: ProfileSummaryPageProps) { return ( ); }