profilePreview.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. import {useMemo, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import emptyStateImg from 'sentry-images/spot/profiling-empty-state.svg';
  4. import {LinkButton} from 'sentry/components/button';
  5. import {SectionHeading} from 'sentry/components/charts/styles';
  6. import InlineDocs from 'sentry/components/events/interfaces/spans/inlineDocs';
  7. import ExternalLink from 'sentry/components/links/externalLink';
  8. import LoadingIndicator from 'sentry/components/loadingIndicator';
  9. import {FlamegraphPreview} from 'sentry/components/profiling/flamegraph/flamegraphPreview';
  10. import QuestionTooltip from 'sentry/components/questionTooltip';
  11. import {t, tct} from 'sentry/locale';
  12. import {space} from 'sentry/styles/space';
  13. import type {EventTransaction} from 'sentry/types/event';
  14. import type {Organization} from 'sentry/types/organization';
  15. import {defined} from 'sentry/utils';
  16. import {trackAnalytics} from 'sentry/utils/analytics';
  17. import type {CanvasView} from 'sentry/utils/profiling/canvasView';
  18. import {colorComponentsToRGBA} from 'sentry/utils/profiling/colors/utils';
  19. import {Flamegraph as FlamegraphModel} from 'sentry/utils/profiling/flamegraph';
  20. import {FlamegraphThemeProvider} from 'sentry/utils/profiling/flamegraph/flamegraphThemeProvider';
  21. import {useFlamegraphTheme} from 'sentry/utils/profiling/flamegraph/useFlamegraphTheme';
  22. import {getProfilingDocsForPlatform} from 'sentry/utils/profiling/platforms';
  23. import {generateProfileFlamechartRouteWithQuery} from 'sentry/utils/profiling/routes';
  24. import {Rect} from 'sentry/utils/profiling/speedscope';
  25. import useOrganization from 'sentry/utils/useOrganization';
  26. import useProjects from 'sentry/utils/useProjects';
  27. import {useProfileGroup} from 'sentry/views/profiling/profileGroupProvider';
  28. import {useProfiles} from 'sentry/views/profiling/profilesProvider';
  29. import type {MissingInstrumentationNode} from '../../../traceModels/missingInstrumentationNode';
  30. import {TraceTree} from '../../../traceModels/traceTree';
  31. import type {TraceTreeNode} from '../../../traceModels/traceTreeNode';
  32. interface SpanProfileProps {
  33. event: Readonly<EventTransaction>;
  34. node: TraceTreeNode<TraceTree.Span> | MissingInstrumentationNode;
  35. }
  36. export function ProfilePreview({event, node}: SpanProfileProps) {
  37. const {projects} = useProjects();
  38. const profiles = useProfiles();
  39. const profileGroup = useProfileGroup();
  40. const project = useMemo(
  41. () => projects.find(p => p.id === event.projectID),
  42. [projects, event]
  43. );
  44. const organization = useOrganization();
  45. const [canvasView, setCanvasView] = useState<CanvasView<FlamegraphModel> | null>(null);
  46. const profile = useMemo(() => {
  47. const threadId = profileGroup.profiles[profileGroup.activeProfileIndex]?.threadId;
  48. if (!defined(threadId)) {
  49. return null;
  50. }
  51. return profileGroup.profiles.find(p => p.threadId === threadId) ?? null;
  52. }, [profileGroup.profiles, profileGroup.activeProfileIndex]);
  53. const transactionHasProfile = useMemo(() => {
  54. return (TraceTree.ParentTransaction(node)?.profiles?.length ?? 0) > 0;
  55. }, [node]);
  56. const flamegraph = useMemo(() => {
  57. if (!transactionHasProfile || !profile) {
  58. return FlamegraphModel.Example();
  59. }
  60. return new FlamegraphModel(profile, {});
  61. }, [transactionHasProfile, profile]);
  62. // The most recent profile formats should contain a timestamp indicating
  63. // the beginning of the profile. This timestamp can be after the start
  64. // timestamp on the transaction, so we need to account for the gap and
  65. // make sure the relative start timestamps we compute for the span is
  66. // relative to the start of the profile.
  67. //
  68. // If the profile does not contain a timestamp, we fall back to using the
  69. // start timestamp on the transaction. This won't be as accurate but it's
  70. // the next best thing.
  71. const startTimestamp = profile?.timestamp ?? event.startTimestamp;
  72. const relativeStartTimestamp = transactionHasProfile
  73. ? node.value.start_timestamp - startTimestamp
  74. : 0;
  75. const relativeStopTimestamp = transactionHasProfile
  76. ? node.value.timestamp - startTimestamp
  77. : flamegraph.configSpace.width;
  78. if (transactionHasProfile) {
  79. return (
  80. <FlamegraphThemeProvider>
  81. {/* If you remove this div, padding between elements will break */}
  82. <div>
  83. <ProfilePreviewHeader
  84. event={event}
  85. canvasView={canvasView}
  86. organization={organization}
  87. />
  88. <ProfilePreviewLegend />
  89. <FlamegraphContainer>
  90. {profiles.type === 'loading' ? (
  91. <LoadingIndicator />
  92. ) : (
  93. <FlamegraphPreview
  94. flamegraph={flamegraph}
  95. updateFlamegraphView={setCanvasView}
  96. relativeStartTimestamp={relativeStartTimestamp}
  97. relativeStopTimestamp={relativeStopTimestamp}
  98. />
  99. )}
  100. </FlamegraphContainer>
  101. <ManualInstrumentationInstruction />
  102. </div>
  103. </FlamegraphThemeProvider>
  104. );
  105. }
  106. // The event's platform is more accurate than the project
  107. // so use that first and fall back to the project's platform
  108. const docsLink =
  109. getProfilingDocsForPlatform(event.platform) ??
  110. (project && getProfilingDocsForPlatform(project.platform));
  111. // This project has received a profile before so they've already
  112. // set up profiling. No point showing the profiling setup again.
  113. if (!docsLink || project?.hasProfiles) {
  114. return <InlineDocs platform={event.sdk?.name || ''} />;
  115. }
  116. // At this point we must have a project on a supported
  117. // platform that has not setup profiling yet
  118. return <SetupProfiling link={docsLink} />;
  119. }
  120. interface ProfilePreviewProps {
  121. canvasView: CanvasView<FlamegraphModel> | null;
  122. event: Readonly<EventTransaction>;
  123. organization: Organization;
  124. }
  125. function ProfilePreviewHeader({canvasView, event, organization}: ProfilePreviewProps) {
  126. const profileId = event.contexts.profile?.profile_id || '';
  127. // we want to try to go straight to the same config view as the preview
  128. const query = canvasView?.configView
  129. ? {
  130. // TODO: this assumes that profile start timestamp == transaction timestamp
  131. fov: Rect.encode(canvasView.configView),
  132. // the flamechart persists some preferences to local storage,
  133. // force these settings so the view is the same as the preview
  134. view: 'top down',
  135. type: 'flamechart',
  136. }
  137. : undefined;
  138. const target = generateProfileFlamechartRouteWithQuery({
  139. orgSlug: organization.slug,
  140. projectSlug: event?.projectSlug ?? '',
  141. profileId,
  142. query,
  143. });
  144. function handleGoToProfile() {
  145. trackAnalytics('profiling_views.go_to_flamegraph', {
  146. organization,
  147. source: 'performance.missing_instrumentation',
  148. });
  149. }
  150. return (
  151. <HeaderContainer>
  152. <HeaderContainer>
  153. <StyledSectionHeading>{t('Related Profile')}</StyledSectionHeading>
  154. <QuestionTooltip
  155. position="top"
  156. size="sm"
  157. containerDisplayMode="block"
  158. title={t(
  159. 'This profile was collected concurrently with the transaction. It displays the relevant stacks and functions for the duration of this span.'
  160. )}
  161. />
  162. </HeaderContainer>
  163. <LinkButton size="xs" onClick={handleGoToProfile} to={target}>
  164. {t('View Profile')}
  165. </LinkButton>
  166. </HeaderContainer>
  167. );
  168. }
  169. function ProfilePreviewLegend() {
  170. const theme = useFlamegraphTheme();
  171. const applicationFrameColor = colorComponentsToRGBA(
  172. theme.COLORS.FRAME_APPLICATION_COLOR
  173. );
  174. const systemFrameColor = colorComponentsToRGBA(theme.COLORS.FRAME_SYSTEM_COLOR);
  175. return (
  176. <LegendContainer>
  177. <LegendItem>
  178. <LegendMarker color={applicationFrameColor} />
  179. {t('Application Function')}
  180. </LegendItem>
  181. <LegendItem>
  182. <LegendMarker color={systemFrameColor} />
  183. {t('System Function')}
  184. </LegendItem>
  185. </LegendContainer>
  186. );
  187. }
  188. function SetupProfiling({link}: {link: string}) {
  189. return (
  190. <Container>
  191. <Image src={emptyStateImg} />
  192. <InstructionsContainer>
  193. <h5>{t('With Profiling, we could paint a better picture')}</h5>
  194. <p>
  195. {t(
  196. 'Profiles can give you additional context on which functions are sampled at the same time of these spans.'
  197. )}
  198. </p>
  199. <LinkButton size="sm" priority="primary" href={link} external>
  200. {t('Set Up Profiling')}
  201. </LinkButton>
  202. <ManualInstrumentationInstruction />
  203. </InstructionsContainer>
  204. </Container>
  205. );
  206. }
  207. function ManualInstrumentationInstruction() {
  208. return (
  209. <SubText>
  210. {tct(
  211. `You can also [docLink:manually instrument] certain regions of your code to see span details for future transactions.`,
  212. {
  213. docLink: (
  214. <ExternalLink href="https://docs.sentry.io/product/performance/getting-started/" />
  215. ),
  216. }
  217. )}
  218. </SubText>
  219. );
  220. }
  221. const StyledSectionHeading = styled(SectionHeading)`
  222. color: ${p => p.theme.textColor};
  223. `;
  224. const Container = styled('div')`
  225. display: flex;
  226. gap: ${space(2)};
  227. justify-content: space-between;
  228. flex-direction: column;
  229. @media (min-width: ${p => p.theme.breakpoints.medium}) {
  230. flex-direction: row-reverse;
  231. }
  232. `;
  233. const InstructionsContainer = styled('div')`
  234. > p {
  235. margin: 0;
  236. }
  237. display: flex;
  238. gap: ${space(3)};
  239. flex-direction: column;
  240. align-items: start;
  241. `;
  242. const Image = styled('img')`
  243. user-select: none;
  244. width: 420px;
  245. align-self: center;
  246. @media (min-width: ${p => p.theme.breakpoints.medium}) {
  247. width: 300px;
  248. }
  249. @media (min-width: ${p => p.theme.breakpoints.large}) {
  250. width: 380px;
  251. }
  252. @media (min-width: ${p => p.theme.breakpoints.xlarge}) {
  253. width: 420px;
  254. }
  255. `;
  256. const FlamegraphContainer = styled('div')`
  257. height: 310px;
  258. margin-top: ${space(1)};
  259. margin-bottom: ${space(1)};
  260. position: relative;
  261. `;
  262. const LegendContainer = styled('div')`
  263. display: flex;
  264. flex-direction: row;
  265. gap: ${space(1.5)};
  266. `;
  267. const LegendItem = styled('span')`
  268. display: flex;
  269. flex-direction: row;
  270. align-items: center;
  271. gap: ${space(0.5)};
  272. color: ${p => p.theme.subText};
  273. font-size: ${p => p.theme.fontSizeSmall};
  274. `;
  275. const LegendMarker = styled('span')<{color: string}>`
  276. display: inline-block;
  277. width: 12px;
  278. height: 12px;
  279. border-radius: 1px;
  280. background-color: ${p => p.color};
  281. `;
  282. const HeaderContainer = styled('div')`
  283. display: flex;
  284. flex-direction: row;
  285. align-items: center;
  286. justify-content: space-between;
  287. gap: ${space(1)};
  288. `;
  289. const SubText = styled('p')`
  290. color: ${p => p.theme.subText};
  291. `;