spanProfileDetails.tsx 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  1. import {Fragment, useMemo, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import {Button} from 'sentry/components/button';
  4. import ButtonBar from 'sentry/components/buttonBar';
  5. import {SectionHeading} from 'sentry/components/charts/styles';
  6. import StackTrace from 'sentry/components/events/interfaces/crashContent/stackTrace';
  7. import {Tooltip} from 'sentry/components/tooltip';
  8. import {IconChevron, IconProfiling} from 'sentry/icons';
  9. import {t, tct} from 'sentry/locale';
  10. import {space} from 'sentry/styles/space';
  11. import {EventTransaction, Frame, PlatformType} from 'sentry/types/event';
  12. import {STACK_VIEW} from 'sentry/types/stacktrace';
  13. import {defined} from 'sentry/utils';
  14. import {formatPercentage} from 'sentry/utils/formatters';
  15. import {CallTreeNode} from 'sentry/utils/profiling/callTreeNode';
  16. import {Frame as ProfilingFrame} from 'sentry/utils/profiling/frame';
  17. import {Profile} from 'sentry/utils/profiling/profile/profile';
  18. import {generateProfileFlamechartRouteWithQuery} from 'sentry/utils/profiling/routes';
  19. import {formatTo} from 'sentry/utils/profiling/units/units';
  20. import useOrganization from 'sentry/utils/useOrganization';
  21. import useProjects from 'sentry/utils/useProjects';
  22. import {useProfileGroup} from 'sentry/views/profiling/profileGroupProvider';
  23. import {SpanType} from './types';
  24. const MAX_STACK_DEPTH = 16;
  25. const MAX_TOP_NODES = 5;
  26. const MIN_TOP_NODES = 3;
  27. const TOP_NODE_MIN_COUNT = 3;
  28. interface SpanProfileDetailsProps {
  29. event: Readonly<EventTransaction>;
  30. span: Readonly<SpanType>;
  31. }
  32. export function SpanProfileDetails({event, span}: SpanProfileDetailsProps) {
  33. const organization = useOrganization();
  34. const {projects} = useProjects();
  35. const project = projects.find(p => p.id === event.projectID);
  36. const profileGroup = useProfileGroup();
  37. // TODO: Pick another thread if it's more relevant.
  38. const threadId = useMemo(
  39. () => profileGroup.profiles[profileGroup.activeProfileIndex]?.threadId,
  40. [profileGroup]
  41. );
  42. const profile = useMemo(() => {
  43. if (!defined(threadId)) {
  44. return null;
  45. }
  46. return profileGroup.profiles.find(p => p.threadId === threadId) ?? null;
  47. }, [profileGroup.profiles, threadId]);
  48. const nodes: CallTreeNode[] = useMemo(() => {
  49. if (profile === null) {
  50. return [];
  51. }
  52. const relativeStartTimestamp = formatTo(
  53. span.start_timestamp - event.startTimestamp,
  54. 'second',
  55. profile.unit
  56. );
  57. const relativeStopTimestamp = formatTo(
  58. span.timestamp - event.startTimestamp,
  59. 'second',
  60. profile.unit
  61. );
  62. return getTopNodes(profile, relativeStartTimestamp, relativeStopTimestamp);
  63. }, [profile, span, event]);
  64. const [index, setIndex] = useState(0);
  65. const totalWeight = useMemo(
  66. () => nodes.reduce((count, node) => count + node.count, 0),
  67. [nodes]
  68. );
  69. const maxNodes = useMemo(() => {
  70. // find the number of nodes with the minimum number of samples
  71. let hasMinCount = 0;
  72. for (let i = 0; i < nodes.length; i++) {
  73. if (nodes[i].count >= TOP_NODE_MIN_COUNT) {
  74. hasMinCount += 1;
  75. } else {
  76. break;
  77. }
  78. }
  79. hasMinCount = Math.max(MIN_TOP_NODES, hasMinCount);
  80. return Math.min(nodes.length, MAX_TOP_NODES, hasMinCount);
  81. }, [nodes]);
  82. const {frames, hasPrevious, hasNext} = useMemo(() => {
  83. if (index >= maxNodes) {
  84. return {frames: [], hasPrevious: false, hasNext: false};
  85. }
  86. return {
  87. frames: extractFrames(nodes[index], event.platform || 'other'),
  88. hasPrevious: index > 0,
  89. hasNext: index + 1 < maxNodes,
  90. };
  91. }, [index, maxNodes, event, nodes]);
  92. const profileTarget =
  93. project &&
  94. profileGroup &&
  95. profile &&
  96. generateProfileFlamechartRouteWithQuery({
  97. orgSlug: organization.slug,
  98. projectSlug: project.slug,
  99. profileId: profileGroup.traceID,
  100. query: {tid: String(profile.threadId)},
  101. });
  102. const spanTarget =
  103. project &&
  104. profileGroup &&
  105. profile &&
  106. generateProfileFlamechartRouteWithQuery({
  107. orgSlug: organization.slug,
  108. projectSlug: project.slug,
  109. profileId: profileGroup.traceID,
  110. query: {
  111. tid: String(profile.threadId),
  112. spanId: span.span_id,
  113. },
  114. });
  115. if (!defined(profile) || !defined(profileTarget) || !defined(spanTarget)) {
  116. return null;
  117. }
  118. if (!frames.length) {
  119. return null;
  120. }
  121. return (
  122. <Fragment>
  123. <SpanDetails>
  124. <SpanDetailsItem grow>
  125. <SectionHeading>{t('Most Frequent Stacks in this Span')}</SectionHeading>
  126. </SpanDetailsItem>
  127. <SpanDetailsItem>
  128. <SectionSubtext>
  129. <Tooltip title={t('%s out of %s samples', nodes[index].count, totalWeight)}>
  130. {tct('Showing stacks [index] of [total] ([percentage])', {
  131. index: index + 1,
  132. total: maxNodes,
  133. percentage: formatPercentage(nodes[index].count / totalWeight),
  134. })}
  135. </Tooltip>
  136. </SectionSubtext>
  137. </SpanDetailsItem>
  138. <SpanDetailsItem>
  139. <ButtonBar merged>
  140. <Button
  141. icon={<IconChevron direction="left" size="xs" />}
  142. aria-label={t('Previous')}
  143. size="xs"
  144. disabled={!hasPrevious}
  145. onClick={() => {
  146. setIndex(prevIndex => prevIndex - 1);
  147. }}
  148. />
  149. <Button
  150. icon={<IconChevron direction="right" size="xs" />}
  151. aria-label={t('Next')}
  152. size="xs"
  153. disabled={!hasNext}
  154. onClick={() => {
  155. setIndex(prevIndex => prevIndex + 1);
  156. }}
  157. />
  158. </ButtonBar>
  159. </SpanDetailsItem>
  160. <SpanDetailsItem>
  161. <Button icon={<IconProfiling />} to={spanTarget} size="xs">
  162. {t('View Profile')}
  163. </Button>
  164. </SpanDetailsItem>
  165. </SpanDetails>
  166. <StackTrace
  167. event={event}
  168. hasHierarchicalGrouping={false}
  169. newestFirst
  170. platform={event.platform || 'other'}
  171. stacktrace={{
  172. framesOmitted: null,
  173. hasSystemFrames: false,
  174. registers: null,
  175. frames,
  176. }}
  177. stackView={STACK_VIEW.APP}
  178. nativeV2
  179. inlined
  180. maxDepth={MAX_STACK_DEPTH}
  181. />
  182. </Fragment>
  183. );
  184. }
  185. function getTopNodes(profile: Profile, startTimestamp, stopTimestamp): CallTreeNode[] {
  186. let duration = profile.startedAt;
  187. const callTree: CallTreeNode = new CallTreeNode(ProfilingFrame.Root, null);
  188. for (let i = 0; i < profile.samples.length; i++) {
  189. const sample = profile.samples[i];
  190. // TODO: should this take self times into consideration?
  191. const inRange = startTimestamp <= duration && duration < stopTimestamp;
  192. duration += profile.weights[i];
  193. if (sample.isRoot() || !inRange) {
  194. continue;
  195. }
  196. const stack: CallTreeNode[] = [sample];
  197. let node: CallTreeNode | null = sample;
  198. while (node && !node.isRoot()) {
  199. stack.push(node);
  200. node = node.parent;
  201. }
  202. let tree = callTree;
  203. // make sure to iterate the stack backwards here, the 0th index is the
  204. // inner most frame, and the last index is the outer most frame
  205. for (let j = stack.length - 1; j >= 0; j--) {
  206. node = stack[j]!;
  207. const frame = node.frame;
  208. // find a child in the current tree with the same frame,
  209. // merge the current node into it if it exists
  210. let last = tree.children.find(n => n.frame === frame);
  211. if (!defined(last)) {
  212. last = new CallTreeNode(frame, tree);
  213. tree.children.push(last);
  214. }
  215. // make sure to increment the count/weight so it can be sorted later
  216. last.count += node.count;
  217. last.addToSelfWeight(node.selfWeight);
  218. tree = last;
  219. }
  220. }
  221. const nodes: CallTreeNode[] = [];
  222. const trees = [callTree];
  223. while (trees.length) {
  224. const tree = trees.pop()!;
  225. // walk to the leaf nodes, these correspond with the inner most frame
  226. // on a stack
  227. if (tree.children.length === 0) {
  228. nodes.push(tree);
  229. continue;
  230. }
  231. trees.push(...tree.children);
  232. }
  233. return nodes.sort(sortByCount);
  234. }
  235. // TODO: does this work for android? The counts on the evented format may not be what we expect
  236. function sortByCount(a: CallTreeNode, b: CallTreeNode) {
  237. if (a.count === b.count) {
  238. return b.selfWeight - a.selfWeight;
  239. }
  240. return b.count - a.count;
  241. }
  242. function extractFrames(node: CallTreeNode | null, platform: PlatformType): Frame[] {
  243. const frames: Frame[] = [];
  244. while (node && !node.isRoot()) {
  245. const frame = {
  246. absPath: node.frame.path ?? null,
  247. colNo: node.frame.column ?? null,
  248. context: [],
  249. errors: null,
  250. filename: node.frame.file ?? null,
  251. function: node.frame.name ?? null,
  252. inApp: node.frame.is_application,
  253. instructionAddr: null,
  254. lineNo: node.frame.line ?? null,
  255. // TODO: distinguish between module/package
  256. module: node.frame.image ?? null,
  257. package: null,
  258. platform,
  259. rawFunction: null,
  260. symbol: null,
  261. symbolAddr: null,
  262. trust: null,
  263. vars: null,
  264. };
  265. frames.push(frame);
  266. node = node.parent;
  267. }
  268. // Profile stacks start from the inner most frame, while error stacks
  269. // start from the outer most frame. Reverse the order here to match
  270. // the convention on errors.
  271. return frames.reverse();
  272. }
  273. const SpanDetails = styled('div')`
  274. padding: ${space(2)};
  275. display: flex;
  276. align-items: baseline;
  277. gap: ${space(1)};
  278. `;
  279. const SpanDetailsItem = styled('span')<{grow?: boolean}>`
  280. ${p => (p.grow ? 'flex: 1 2 auto' : 'flex: 0 1 auto')}
  281. `;
  282. const SectionSubtext = styled('span')`
  283. color: ${p => p.theme.subText};
  284. font-size: ${p => p.theme.fontSizeMedium};
  285. `;