import {useMemo, useState} from 'react'; import styled from '@emotion/styled'; import emptyStateImg from 'sentry-images/spot/profiling-empty-state.svg'; import {LinkButton} from 'sentry/components/button'; import {SectionHeading} from 'sentry/components/charts/styles'; import InlineDocs from 'sentry/components/events/interfaces/spans/inlineDocs'; import ExternalLink from 'sentry/components/links/externalLink'; import LoadingIndicator from 'sentry/components/loadingIndicator'; import {FlamegraphPreview} from 'sentry/components/profiling/flamegraph/flamegraphPreview'; import QuestionTooltip from 'sentry/components/questionTooltip'; import {t, tct} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import type {EventTransaction} from 'sentry/types/event'; import type {Organization} from 'sentry/types/organization'; import {defined} from 'sentry/utils'; import {trackAnalytics} from 'sentry/utils/analytics'; import type {CanvasView} from 'sentry/utils/profiling/canvasView'; import {colorComponentsToRGBA} from 'sentry/utils/profiling/colors/utils'; import {Flamegraph as FlamegraphModel} from 'sentry/utils/profiling/flamegraph'; import {FlamegraphThemeProvider} from 'sentry/utils/profiling/flamegraph/flamegraphThemeProvider'; import {useFlamegraphTheme} from 'sentry/utils/profiling/flamegraph/useFlamegraphTheme'; import {getProfilingDocsForPlatform} from 'sentry/utils/profiling/platforms'; import {generateProfileFlamechartRouteWithQuery} from 'sentry/utils/profiling/routes'; import {Rect} from 'sentry/utils/profiling/speedscope'; import useOrganization from 'sentry/utils/useOrganization'; import useProjects from 'sentry/utils/useProjects'; import {useProfileGroup} from 'sentry/views/profiling/profileGroupProvider'; import {useProfiles} from 'sentry/views/profiling/profilesProvider'; import type {MissingInstrumentationNode} from '../../../traceModels/missingInstrumentationNode'; import {TraceTree} from '../../../traceModels/traceTree'; import type {TraceTreeNode} from '../../../traceModels/traceTreeNode'; interface SpanProfileProps { event: Readonly; node: TraceTreeNode | MissingInstrumentationNode; } export function ProfilePreview({event, node}: SpanProfileProps) { const {projects} = useProjects(); const profiles = useProfiles(); const profileGroup = useProfileGroup(); const project = useMemo( () => projects.find(p => p.id === event.projectID), [projects, event] ); const organization = useOrganization(); const [canvasView, setCanvasView] = useState | null>(null); const profile = useMemo(() => { const threadId = profileGroup.profiles[profileGroup.activeProfileIndex]?.threadId; if (!defined(threadId)) { return null; } return profileGroup.profiles.find(p => p.threadId === threadId) ?? null; }, [profileGroup.profiles, profileGroup.activeProfileIndex]); const transactionHasProfile = useMemo(() => { return (TraceTree.ParentTransaction(node)?.profiles?.length ?? 0) > 0; }, [node]); const flamegraph = useMemo(() => { if (!transactionHasProfile || !profile) { return FlamegraphModel.Example(); } return new FlamegraphModel(profile, {}); }, [transactionHasProfile, profile]); // The most recent profile formats should contain a timestamp indicating // the beginning of the profile. This timestamp can be after the start // timestamp on the transaction, so we need to account for the gap and // make sure the relative start timestamps we compute for the span is // relative to the start of the profile. // // If the profile does not contain a timestamp, we fall back to using the // start timestamp on the transaction. This won't be as accurate but it's // the next best thing. const startTimestamp = profile?.timestamp ?? event.startTimestamp; const relativeStartTimestamp = transactionHasProfile ? node.value.start_timestamp - startTimestamp : 0; const relativeStopTimestamp = transactionHasProfile ? node.value.timestamp - startTimestamp : flamegraph.configSpace.width; if (transactionHasProfile) { return ( {/* If you remove this div, padding between elements will break */}
{profiles.type === 'loading' ? ( ) : ( )}
); } // The event's platform is more accurate than the project // so use that first and fall back to the project's platform const docsLink = getProfilingDocsForPlatform(event.platform) ?? (project && getProfilingDocsForPlatform(project.platform)); // This project has received a profile before so they've already // set up profiling. No point showing the profiling setup again. if (!docsLink || project?.hasProfiles) { return ; } // At this point we must have a project on a supported // platform that has not setup profiling yet return ; } interface ProfilePreviewProps { canvasView: CanvasView | null; event: Readonly; organization: Organization; } function ProfilePreviewHeader({canvasView, event, organization}: ProfilePreviewProps) { const profileId = event.contexts.profile?.profile_id || ''; // we want to try to go straight to the same config view as the preview const query = canvasView?.configView ? { // TODO: this assumes that profile start timestamp == transaction timestamp fov: Rect.encode(canvasView.configView), // the flamechart persists some preferences to local storage, // force these settings so the view is the same as the preview view: 'top down', type: 'flamechart', } : undefined; const target = generateProfileFlamechartRouteWithQuery({ orgSlug: organization.slug, projectSlug: event?.projectSlug ?? '', profileId, query, }); function handleGoToProfile() { trackAnalytics('profiling_views.go_to_flamegraph', { organization, source: 'performance.missing_instrumentation', }); } return ( {t('Related Profile')} {t('View Profile')} ); } function ProfilePreviewLegend() { const theme = useFlamegraphTheme(); const applicationFrameColor = colorComponentsToRGBA( theme.COLORS.FRAME_APPLICATION_COLOR ); const systemFrameColor = colorComponentsToRGBA(theme.COLORS.FRAME_SYSTEM_COLOR); return ( {t('Application Function')} {t('System Function')} ); } function SetupProfiling({link}: {link: string}) { return (
{t('With Profiling, we could paint a better picture')}

{t( 'Profiles can give you additional context on which functions are sampled at the same time of these spans.' )}

{t('Set Up Profiling')}
); } function ManualInstrumentationInstruction() { return ( {tct( `You can also [docLink:manually instrument] certain regions of your code to see span details for future transactions.`, { docLink: ( ), } )} ); } const StyledSectionHeading = styled(SectionHeading)` color: ${p => p.theme.textColor}; `; const Container = styled('div')` display: flex; gap: ${space(2)}; justify-content: space-between; flex-direction: column; @media (min-width: ${p => p.theme.breakpoints.medium}) { flex-direction: row-reverse; } `; const InstructionsContainer = styled('div')` > p { margin: 0; } display: flex; gap: ${space(3)}; flex-direction: column; align-items: start; `; const Image = styled('img')` user-select: none; width: 420px; align-self: center; @media (min-width: ${p => p.theme.breakpoints.medium}) { width: 300px; } @media (min-width: ${p => p.theme.breakpoints.large}) { width: 380px; } @media (min-width: ${p => p.theme.breakpoints.xlarge}) { width: 420px; } `; const FlamegraphContainer = styled('div')` height: 310px; margin-top: ${space(1)}; margin-bottom: ${space(1)}; position: relative; `; const LegendContainer = styled('div')` display: flex; flex-direction: row; gap: ${space(1.5)}; `; const LegendItem = styled('span')` display: flex; flex-direction: row; align-items: center; gap: ${space(0.5)}; color: ${p => p.theme.subText}; font-size: ${p => p.theme.fontSizeSmall}; `; const LegendMarker = styled('span')<{color: string}>` display: inline-block; width: 12px; height: 12px; border-radius: 1px; background-color: ${p => p.color}; `; const HeaderContainer = styled('div')` display: flex; flex-direction: row; align-items: center; justify-content: space-between; gap: ${space(1)}; `; const SubText = styled('p')` color: ${p => p.theme.subText}; `;