traceProfiles.tsx 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. import {useCallback, useMemo} from 'react';
  2. import styled from '@emotion/styled';
  3. import {PlatformIcon} from 'platformicons';
  4. import Link from 'sentry/components/links/link';
  5. import {t} from 'sentry/locale';
  6. import {space} from 'sentry/styles/space';
  7. import type {PlatformKey, Project} from 'sentry/types/project';
  8. import {trackAnalytics} from 'sentry/utils/analytics';
  9. import {
  10. generateContinuousProfileFlamechartRouteWithQuery,
  11. generateProfileFlamechartRouteWithQuery,
  12. } from 'sentry/utils/profiling/routes';
  13. import useOrganization from 'sentry/utils/useOrganization';
  14. import useProjects from 'sentry/utils/useProjects';
  15. import {traceAnalytics} from 'sentry/views/performance/newTraceDetails/traceAnalytics';
  16. import {isSpanNode, isTransactionNode} from '../../traceGuards';
  17. import {TraceTree} from '../../traceModels/traceTree';
  18. import type {TraceTreeNode} from '../../traceModels/traceTreeNode';
  19. export function TraceProfiles({
  20. tree,
  21. onScrollToNode,
  22. }: {
  23. onScrollToNode: (node: TraceTreeNode<any>) => void;
  24. tree: TraceTree;
  25. }) {
  26. const {projects} = useProjects();
  27. const organization = useOrganization();
  28. const projectLookup: Record<string, PlatformKey | undefined> = useMemo(() => {
  29. return projects.reduce<Record<Project['slug'], Project['platform']>>(
  30. (acc, project) => {
  31. acc[project.slug] = project.platform;
  32. return acc;
  33. },
  34. {}
  35. );
  36. }, [projects]);
  37. const profiles = useMemo(
  38. () => Array.from(tree.profiled_events.values()),
  39. [tree.profiled_events]
  40. );
  41. const onProfileLinkClick = useCallback(
  42. (profile: TraceTree.Profile) => {
  43. if ('profiler_id' in profile) {
  44. traceAnalytics.trackViewContinuousProfile(organization);
  45. } else {
  46. trackAnalytics('profiling_views.go_to_flamegraph', {
  47. organization,
  48. source: 'performance.trace_view',
  49. });
  50. }
  51. },
  52. [organization]
  53. );
  54. return (
  55. <ProfilesTable>
  56. <ProfilesTableRow>
  57. <ProfilesTableTitle>{t('Profiled Events')}</ProfilesTableTitle>
  58. <ProfilesTableTitle>{t('Profile')}</ProfilesTableTitle>
  59. </ProfilesTableRow>
  60. {profiles.map((node, index) => {
  61. const profile = node.profiles?.[0];
  62. if (!profile) {
  63. return null;
  64. }
  65. const query = isTransactionNode(node)
  66. ? {
  67. eventId: node.value.event_id,
  68. }
  69. : isSpanNode(node)
  70. ? {
  71. eventId: TraceTree.ParentTransaction(node)?.value?.event_id,
  72. }
  73. : {};
  74. const link =
  75. 'profiler_id' in profile
  76. ? generateContinuousProfileFlamechartRouteWithQuery({
  77. orgSlug: organization.slug,
  78. profilerId: profile.profiler_id,
  79. start: new Date(node.space[0]).toISOString(),
  80. end: new Date(node.space[0] + node.space[1]).toISOString(),
  81. projectSlug: node.metadata.project_slug as string,
  82. query: query,
  83. })
  84. : generateProfileFlamechartRouteWithQuery({
  85. orgSlug: organization.slug,
  86. projectSlug: node.metadata.project_slug as string,
  87. profileId: profile.profile_id,
  88. query,
  89. });
  90. const profileOrProfilerId =
  91. 'profiler_id' in profile ? profile.profiler_id : profile.profile_id;
  92. if (isTransactionNode(node)) {
  93. return (
  94. <ProfilesTableRow key={index}>
  95. <div>
  96. <a onClick={() => onScrollToNode(node)}>
  97. <PlatformIcon
  98. platform={projectLookup[node.value.project_slug] ?? 'default'}
  99. />
  100. <span>{node.value['transaction.op']}</span> —{' '}
  101. <span>{node.value.transaction}</span>
  102. </a>
  103. </div>
  104. <div>
  105. <Link to={link} onClick={() => onProfileLinkClick(profile)}>
  106. {profileOrProfilerId.substring(0, 8)}
  107. </Link>
  108. </div>
  109. </ProfilesTableRow>
  110. );
  111. }
  112. if (isSpanNode(node)) {
  113. return (
  114. <ProfilesTableRow key={index}>
  115. <div>
  116. <a onClick={() => onScrollToNode(node)}>
  117. <span>{node.value.op ?? '<unknown>'}</span> —{' '}
  118. <span className="TraceDescription" title={node.value.description}>
  119. {!node.value.description
  120. ? node.value.span_id ?? 'unknown'
  121. : node.value.description.length > 100
  122. ? node.value.description.slice(0, 100).trim() + '\u2026'
  123. : node.value.description}
  124. </span>
  125. </a>
  126. </div>
  127. <div>
  128. <Link to={link} onClick={() => onProfileLinkClick(profile)}>
  129. {profileOrProfilerId.substring(0, 8)}
  130. </Link>
  131. </div>
  132. </ProfilesTableRow>
  133. );
  134. }
  135. return null;
  136. })}
  137. </ProfilesTable>
  138. );
  139. }
  140. const ProfilesTable = styled('div')`
  141. margin-top: ${space(1)};
  142. display: grid !important;
  143. grid-template-columns: 1fr min-content;
  144. grid-template-rows: auto;
  145. width: 100%;
  146. border: 1px solid ${p => p.theme.border};
  147. border-radius: ${p => p.theme.borderRadius};
  148. overflow: hidden;
  149. > div {
  150. white-space: nowrap;
  151. overflow: hidden;
  152. text-overflow: ellipsis;
  153. padding: ${space(0.5)} ${space(1)};
  154. }
  155. img {
  156. width: 16px;
  157. height: 16px;
  158. margin-right: ${space(0.5)};
  159. }
  160. `;
  161. const ProfilesTableRow = styled('div')`
  162. display: grid;
  163. grid-column: 1 / -1;
  164. grid-template-columns: subgrid;
  165. width: 100%;
  166. padding: ${space(0.5)};
  167. padding: ${space(0.5)} ${space(2)};
  168. & > div {
  169. padding: ${space(0.5)} ${space(1)};
  170. white-space: nowrap;
  171. text-overflow: ellipsis;
  172. overflow: hidden;
  173. }
  174. &:first-child {
  175. background-color: ${p => p.theme.backgroundSecondary};
  176. }
  177. &:not(:last-child) {
  178. border-bottom: 1px solid ${p => p.theme.border};
  179. }
  180. `;
  181. const ProfilesTableTitle = styled('div')`
  182. color: ${p => p.theme.subText};
  183. font-size: ${p => p.theme.fontSizeMedium};
  184. font-weight: ${p => p.theme.fontWeightBold};
  185. padding: 0 ${space(0.5)};
  186. background-color: ${p => p.theme.backgroundSecondary};
  187. `;