|
@@ -1,4 +1,4 @@
|
|
|
-import {useMemo, useState} from 'react';
|
|
|
+import {Fragment, useMemo, useState} from 'react';
|
|
|
import styled from '@emotion/styled';
|
|
|
|
|
|
import emptyStateImg from 'sentry-images/spot/profiling-empty-state.svg';
|
|
@@ -8,6 +8,8 @@ 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 Panel from 'sentry/components/panels/panel';
|
|
|
+import PanelBody from 'sentry/components/panels/panelBody';
|
|
|
import {FlamegraphPreview} from 'sentry/components/profiling/flamegraph/flamegraphPreview';
|
|
|
import QuestionTooltip from 'sentry/components/questionTooltip';
|
|
|
import {t, tct} from 'sentry/locale';
|
|
@@ -26,12 +28,15 @@ import {generateProfileFlamechartRouteWithQuery} from 'sentry/utils/profiling/ro
|
|
|
import {Rect} from 'sentry/utils/profiling/speedscope';
|
|
|
import useOrganization from 'sentry/utils/useOrganization';
|
|
|
import useProjects from 'sentry/utils/useProjects';
|
|
|
+import {SectionDivider} from 'sentry/views/issueDetails/streamline/foldSection';
|
|
|
+import {InterimSection} from 'sentry/views/issueDetails/streamline/interimSection';
|
|
|
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';
|
|
|
+import {useHasTraceNewUi} from '../../../useHasTraceNewUi';
|
|
|
|
|
|
interface SpanProfileProps {
|
|
|
event: Readonly<EventTransaction>;
|
|
@@ -39,6 +44,157 @@ interface SpanProfileProps {
|
|
|
}
|
|
|
|
|
|
export function ProfilePreview({event, node}: SpanProfileProps) {
|
|
|
+ const {projects} = useProjects();
|
|
|
+ const hasNewTraceUi = useHasTraceNewUi();
|
|
|
+ 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<CanvasView<FlamegraphModel> | 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]);
|
|
|
+
|
|
|
+ if (!hasNewTraceUi) {
|
|
|
+ return (
|
|
|
+ <LegacyProfilePreview event={event} node={node as MissingInstrumentationNode} />
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ // 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;
|
|
|
+
|
|
|
+ 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',
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ const message = (
|
|
|
+ <TextBlock>{t('Or, see if profiling can provide more context on this:')}</TextBlock>
|
|
|
+ );
|
|
|
+
|
|
|
+ if (transactionHasProfile) {
|
|
|
+ return (
|
|
|
+ <FlamegraphThemeProvider>
|
|
|
+ {message}
|
|
|
+ <SectionDivider />
|
|
|
+ <InterimSection
|
|
|
+ title={t('Related Profile')}
|
|
|
+ type="no_instrumentation_profile"
|
|
|
+ initialCollapse={false}
|
|
|
+ actions={
|
|
|
+ <LinkButton size="xs" onClick={handleGoToProfile} to={target}>
|
|
|
+ {t('Open in Profiling')}
|
|
|
+ </LinkButton>
|
|
|
+ }
|
|
|
+ >
|
|
|
+ {/* If you remove this div, padding between elements will break */}
|
|
|
+ <div>
|
|
|
+ <ProfilePreviewLegend />
|
|
|
+ <FlamegraphContainer>
|
|
|
+ {profiles.type === 'loading' ? (
|
|
|
+ <LoadingIndicator />
|
|
|
+ ) : (
|
|
|
+ <FlamegraphPreview
|
|
|
+ flamegraph={flamegraph}
|
|
|
+ updateFlamegraphView={setCanvasView}
|
|
|
+ relativeStartTimestamp={relativeStartTimestamp}
|
|
|
+ relativeStopTimestamp={relativeStopTimestamp}
|
|
|
+ />
|
|
|
+ )}
|
|
|
+ </FlamegraphContainer>
|
|
|
+ </div>
|
|
|
+ </InterimSection>
|
|
|
+ </FlamegraphThemeProvider>
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ // 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 null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // At this point we must have a project on a supported
|
|
|
+ // platform that has not setup profiling yet
|
|
|
+ return (
|
|
|
+ <Fragment>
|
|
|
+ {message}
|
|
|
+ <SetupProfiling link={docsLink} />
|
|
|
+ </Fragment>
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+const TextBlock = styled('div')`
|
|
|
+ font-size: ${p => p.theme.fontSizeLarge};
|
|
|
+ line-height: 1.5;
|
|
|
+ margin-bottom: ${space(2)};
|
|
|
+`;
|
|
|
+
|
|
|
+function LegacyProfilePreview({event, node}: SpanProfileProps) {
|
|
|
const {projects} = useProjects();
|
|
|
const profiles = useProfiles();
|
|
|
const profileGroup = useProfileGroup();
|
|
@@ -130,7 +286,7 @@ export function ProfilePreview({event, node}: SpanProfileProps) {
|
|
|
|
|
|
// At this point we must have a project on a supported
|
|
|
// platform that has not setup profiling yet
|
|
|
- return <SetupProfiling link={docsLink} />;
|
|
|
+ return <LegacySetupProfiling link={docsLink} />;
|
|
|
}
|
|
|
|
|
|
interface ProfilePreviewProps {
|
|
@@ -210,9 +366,56 @@ function ProfilePreviewLegend() {
|
|
|
}
|
|
|
|
|
|
function SetupProfiling({link}: {link: string}) {
|
|
|
+ return (
|
|
|
+ <Panel>
|
|
|
+ <StyledPanelBody>
|
|
|
+ <span>
|
|
|
+ <h5>{t('Profiling for a Better Picture')}</h5>
|
|
|
+ <TextBlock>
|
|
|
+ {t(
|
|
|
+ 'Profiles can also give you additional context on which functions are getting sampled at the time of these spans.'
|
|
|
+ )}
|
|
|
+ </TextBlock>
|
|
|
+ <LinkButton size="sm" priority="primary" href={link} external>
|
|
|
+ {t('Get Started')}
|
|
|
+ </LinkButton>
|
|
|
+ </span>
|
|
|
+ <ImageContainer>
|
|
|
+ <Image src={emptyStateImg} />
|
|
|
+ </ImageContainer>
|
|
|
+ </StyledPanelBody>
|
|
|
+ </Panel>
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+const Image = styled('img')`
|
|
|
+ user-select: none;
|
|
|
+ width: 250px;
|
|
|
+ align-self: center;
|
|
|
+`;
|
|
|
+
|
|
|
+const StyledPanelBody = styled(PanelBody)`
|
|
|
+ display: flex;
|
|
|
+ gap: ${space(2)};
|
|
|
+ justify-content: space-between;
|
|
|
+ padding: ${space(2)};
|
|
|
+ container-type: inline-size;
|
|
|
+`;
|
|
|
+
|
|
|
+const ImageContainer = styled('div')`
|
|
|
+ display: flex;
|
|
|
+ min-width: 200px;
|
|
|
+ justify-content: center;
|
|
|
+
|
|
|
+ @container (max-width: 600px) {
|
|
|
+ display: none;
|
|
|
+ }
|
|
|
+`;
|
|
|
+
|
|
|
+function LegacySetupProfiling({link}: {link: string}) {
|
|
|
return (
|
|
|
<Container>
|
|
|
- <Image src={emptyStateImg} />
|
|
|
+ <LegacyImage src={emptyStateImg} />
|
|
|
<InstructionsContainer>
|
|
|
<h5>{t('With Profiling, we could paint a better picture')}</h5>
|
|
|
<p>
|
|
@@ -270,9 +473,9 @@ const InstructionsContainer = styled('div')`
|
|
|
align-items: start;
|
|
|
`;
|
|
|
|
|
|
-const Image = styled('img')`
|
|
|
+const LegacyImage = styled('img')`
|
|
|
user-select: none;
|
|
|
- width: 420px;
|
|
|
+ width: 200px;
|
|
|
align-self: center;
|
|
|
|
|
|
@media (min-width: ${p => p.theme.breakpoints.medium}) {
|
|
@@ -289,7 +492,7 @@ const Image = styled('img')`
|
|
|
`;
|
|
|
|
|
|
const FlamegraphContainer = styled('div')`
|
|
|
- height: 310px;
|
|
|
+ height: 200px;
|
|
|
margin-top: ${space(1)};
|
|
|
margin-bottom: ${space(1)};
|
|
|
position: relative;
|