landingAggregateFlamegraph.tsx 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865
  1. import {useCallback, useEffect, useMemo, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
  4. import ProjectAvatar from 'sentry/components/avatar/projectAvatar';
  5. import {Button} from 'sentry/components/button';
  6. import {CompactSelect} from 'sentry/components/compactSelect';
  7. import type {SelectOption} from 'sentry/components/compactSelect/types';
  8. import {DropdownMenu} from 'sentry/components/dropdownMenu';
  9. import Link from 'sentry/components/links/link';
  10. import LoadingIndicator from 'sentry/components/loadingIndicator';
  11. import {AggregateFlamegraph} from 'sentry/components/profiling/flamegraph/aggregateFlamegraph';
  12. import {AggregateFlamegraphTreeTable} from 'sentry/components/profiling/flamegraph/aggregateFlamegraphTreeTable';
  13. import {FlamegraphSearch} from 'sentry/components/profiling/flamegraph/flamegraphToolbar/flamegraphSearch';
  14. import {
  15. formatWeightToProfileDuration,
  16. PROFILING_SAMPLES_FORMATTER,
  17. } from 'sentry/components/profiling/flamegraph/flamegraphTooltip';
  18. import {SegmentedControl} from 'sentry/components/segmentedControl';
  19. import TextOverflow from 'sentry/components/textOverflow';
  20. import {Tooltip} from 'sentry/components/tooltip';
  21. import {IconCopy, IconGithub, IconOpen} from 'sentry/icons';
  22. import {t, tct} from 'sentry/locale';
  23. import {space} from 'sentry/styles/space';
  24. import type {Organization} from 'sentry/types/organization';
  25. import type {Project} from 'sentry/types/project';
  26. import type {DeepPartial} from 'sentry/types/utils';
  27. import type {CanvasScheduler} from 'sentry/utils/profiling/canvasScheduler';
  28. import {
  29. CanvasPoolManager,
  30. useCanvasScheduler,
  31. } from 'sentry/utils/profiling/canvasScheduler';
  32. import type {Flamegraph} from 'sentry/utils/profiling/flamegraph';
  33. import type {FlamegraphState} from 'sentry/utils/profiling/flamegraph/flamegraphStateProvider/flamegraphContext';
  34. import {FlamegraphStateProvider} from 'sentry/utils/profiling/flamegraph/flamegraphStateProvider/flamegraphContextProvider';
  35. import {FlamegraphThemeProvider} from 'sentry/utils/profiling/flamegraph/flamegraphThemeProvider';
  36. import type {FlamegraphFrame} from 'sentry/utils/profiling/flamegraphFrame';
  37. import type {Frame} from 'sentry/utils/profiling/frame';
  38. import {useAggregateFlamegraphQuery} from 'sentry/utils/profiling/hooks/useAggregateFlamegraphQuery';
  39. import {useSourceCodeLink} from 'sentry/utils/profiling/hooks/useSourceLink';
  40. import type {ProfileGroup} from 'sentry/utils/profiling/profile/importProfile';
  41. import {generateProfileRouteFromProfileReference} from 'sentry/utils/profiling/routes';
  42. import {useLocalStorageState} from 'sentry/utils/useLocalStorageState';
  43. import {useLocation} from 'sentry/utils/useLocation';
  44. import useOrganization from 'sentry/utils/useOrganization';
  45. import useProjects from 'sentry/utils/useProjects';
  46. import {
  47. FlamegraphProvider,
  48. useFlamegraph,
  49. } from 'sentry/views/profiling/flamegraphProvider';
  50. import {
  51. ProfileGroupProvider,
  52. useProfileGroup,
  53. } from 'sentry/views/profiling/profileGroupProvider';
  54. const DEFAULT_FLAMEGRAPH_PREFERENCES: DeepPartial<FlamegraphState> = {
  55. preferences: {
  56. sorting: 'left heavy' satisfies FlamegraphState['preferences']['sorting'],
  57. },
  58. };
  59. const noop = () => void 0;
  60. function decodeViewOrDefault(
  61. value: string | string[] | null | undefined,
  62. defaultValue: 'flamegraph' | 'profiles'
  63. ): 'flamegraph' | 'profiles' {
  64. if (!value || Array.isArray(value)) {
  65. return defaultValue;
  66. }
  67. if (value === 'flamegraph' || value === 'profiles') {
  68. return value;
  69. }
  70. return defaultValue;
  71. }
  72. interface AggregateFlamegraphToolbarProps {
  73. canvasPoolManager: CanvasPoolManager;
  74. frameFilter: 'system' | 'application' | 'all';
  75. hideSystemFrames: boolean;
  76. onFrameFilterChange: (value: 'system' | 'application' | 'all') => void;
  77. onHideRegressionsClick: () => void;
  78. onVisualizationChange: (value: 'flamegraph' | 'call tree') => void;
  79. scheduler: CanvasScheduler;
  80. setHideSystemFrames: (value: boolean) => void;
  81. visualization: 'flamegraph' | 'call tree';
  82. }
  83. function AggregateFlamegraphToolbar(props: AggregateFlamegraphToolbarProps) {
  84. const flamegraph = useFlamegraph();
  85. const flamegraphs = useMemo(() => [flamegraph], [flamegraph]);
  86. const spans = useMemo(() => [], []);
  87. const frameSelectOptions: SelectOption<'system' | 'application' | 'all'>[] =
  88. useMemo(() => {
  89. return [
  90. {value: 'system', label: t('System Frames')},
  91. {value: 'application', label: t('Application Frames')},
  92. {value: 'all', label: t('All Frames')},
  93. ];
  94. }, []);
  95. const onResetZoom = useCallback(() => {
  96. props.scheduler.dispatch('reset zoom');
  97. }, [props.scheduler]);
  98. const onFrameFilterChange = useCallback(
  99. (value: {value: 'application' | 'system' | 'all'}) => {
  100. props.onFrameFilterChange(value.value);
  101. },
  102. [props]
  103. );
  104. return (
  105. <AggregateFlamegraphToolbarContainer>
  106. <ViewSelectContainer>
  107. <SegmentedControl
  108. aria-label={t('View')}
  109. size="xs"
  110. value={props.visualization}
  111. onChange={props.onVisualizationChange}
  112. >
  113. <SegmentedControl.Item key="flamegraph">
  114. {t('Flamegraph')}
  115. </SegmentedControl.Item>
  116. <SegmentedControl.Item key="call tree">{t('Call Tree')}</SegmentedControl.Item>
  117. </SegmentedControl>
  118. </ViewSelectContainer>
  119. <AggregateFlamegraphSearch
  120. spans={spans}
  121. canvasPoolManager={props.canvasPoolManager}
  122. flamegraphs={flamegraphs}
  123. />
  124. <Button size="xs" onClick={onResetZoom}>
  125. {t('Reset Zoom')}
  126. </Button>
  127. <CompactSelect
  128. size="xs"
  129. onChange={onFrameFilterChange}
  130. value={props.frameFilter}
  131. options={frameSelectOptions}
  132. />
  133. </AggregateFlamegraphToolbarContainer>
  134. );
  135. }
  136. export function LandingAggregateFlamegraph(): React.ReactNode {
  137. const location = useLocation();
  138. const {data, status} = useAggregateFlamegraphQuery({
  139. dataSource: 'profiles',
  140. });
  141. const [visualization, setVisualization] = useLocalStorageState<
  142. 'flamegraph' | 'call tree'
  143. >('flamegraph-visualization', 'flamegraph');
  144. const onVisualizationChange = useCallback(
  145. (value: 'flamegraph' | 'call tree') => {
  146. setVisualization(value);
  147. },
  148. [setVisualization]
  149. );
  150. const [hideRegressions, setHideRegressions] = useLocalStorageState<boolean>(
  151. 'flamegraph-hide-regressions',
  152. false
  153. );
  154. const [frameFilter, setFrameFilter] = useLocalStorageState<
  155. 'system' | 'application' | 'all'
  156. >('flamegraph-frame-filter', 'application');
  157. const onFrameFilterChange = useCallback(
  158. (value: 'system' | 'application' | 'all') => {
  159. setFrameFilter(value);
  160. },
  161. [setFrameFilter]
  162. );
  163. const onResetFrameFilter = useCallback(() => {
  164. setFrameFilter('all');
  165. }, [setFrameFilter]);
  166. const flamegraphFrameFilter: ((frame: Frame) => boolean) | undefined = useMemo(() => {
  167. if (frameFilter === 'all') {
  168. return () => true;
  169. }
  170. if (frameFilter === 'application') {
  171. return frame => frame.is_application;
  172. }
  173. return frame => !frame.is_application;
  174. }, [frameFilter]);
  175. const canvasPoolManager = useMemo(() => new CanvasPoolManager(), []);
  176. const scheduler = useCanvasScheduler(canvasPoolManager);
  177. const [view, setView] = useState<'flamegraph' | 'profiles'>(
  178. decodeViewOrDefault(location.query.view, 'flamegraph')
  179. );
  180. useEffect(() => {
  181. const newView = decodeViewOrDefault(location.query.view, 'flamegraph');
  182. if (newView !== view) {
  183. setView(decodeViewOrDefault(location.query.view, 'flamegraph'));
  184. }
  185. }, [location.query.view, view]);
  186. const onHideRegressionsClick = useCallback(() => {
  187. return setHideRegressions(!hideRegressions);
  188. }, [hideRegressions, setHideRegressions]);
  189. return (
  190. <ProfileGroupProvider
  191. traceID=""
  192. type="flamegraph"
  193. input={data ?? null}
  194. frameFilter={flamegraphFrameFilter}
  195. >
  196. <FlamegraphStateProvider initialState={DEFAULT_FLAMEGRAPH_PREFERENCES}>
  197. <FlamegraphThemeProvider>
  198. <FlamegraphProvider>
  199. <AggregateFlamegraphContainer>
  200. <AggregateFlamegraphToolbar
  201. scheduler={scheduler}
  202. canvasPoolManager={canvasPoolManager}
  203. visualization={visualization}
  204. onVisualizationChange={onVisualizationChange}
  205. frameFilter={frameFilter}
  206. onFrameFilterChange={onFrameFilterChange}
  207. hideSystemFrames={false}
  208. setHideSystemFrames={noop}
  209. onHideRegressionsClick={onHideRegressionsClick}
  210. />
  211. {status === 'pending' ? (
  212. <RequestStateMessageContainer>
  213. <LoadingIndicator />
  214. </RequestStateMessageContainer>
  215. ) : status === 'error' ? (
  216. <RequestStateMessageContainer>
  217. {t('There was an error loading the flamegraph.')}
  218. </RequestStateMessageContainer>
  219. ) : null}
  220. {visualization === 'flamegraph' ? (
  221. <AggregateFlamegraphCanvasContainer>
  222. <AggregateFlamegraph
  223. filter={frameFilter}
  224. status={status}
  225. onResetFilter={onResetFrameFilter}
  226. canvasPoolManager={canvasPoolManager}
  227. scheduler={scheduler}
  228. />
  229. <AggregateFlamegraphFunctionBreakdown scheduler={scheduler} />
  230. </AggregateFlamegraphCanvasContainer>
  231. ) : (
  232. <AggregateFlamegraphTreeTable
  233. recursion={null}
  234. expanded={false}
  235. withoutBorders
  236. frameFilter={frameFilter}
  237. canvasPoolManager={canvasPoolManager}
  238. />
  239. )}
  240. </AggregateFlamegraphContainer>
  241. </FlamegraphProvider>
  242. </FlamegraphThemeProvider>
  243. </FlamegraphStateProvider>
  244. </ProfileGroupProvider>
  245. );
  246. }
  247. interface AggregateFlamegraphFunctionBreakdownProps {
  248. scheduler: CanvasScheduler;
  249. }
  250. function AggregateFlamegraphFunctionBreakdown(
  251. props: AggregateFlamegraphFunctionBreakdownProps
  252. ) {
  253. const {projects} = useProjects();
  254. const organization = useOrganization();
  255. const flamegraph = useFlamegraph();
  256. const profileGroup = useProfileGroup();
  257. const [nodes, setNodes] = useState<FlamegraphFrame[] | null>(null);
  258. useEffect(() => {
  259. function onFrameHighlight(
  260. frames: FlamegraphFrame[] | null,
  261. type: 'hover' | 'selected'
  262. ) {
  263. if (type === 'selected') {
  264. setNodes(frames);
  265. }
  266. }
  267. props.scheduler.on('highlight frame', onFrameHighlight);
  268. return () => {
  269. props.scheduler.off('highlight frame', onFrameHighlight);
  270. };
  271. }, [props.scheduler, setNodes]);
  272. const example = nodes?.[0];
  273. const projectsLookupTable = useMemo(() => {
  274. return projects.reduce(
  275. (acc, project) => {
  276. acc[project.id] = project;
  277. return acc;
  278. },
  279. {} as Record<string, Project>
  280. );
  281. }, [projects]);
  282. const callers = useMemo(() => {
  283. if (!nodes) {
  284. return [];
  285. }
  286. const results: FlamegraphFrame[] = [];
  287. for (const node of nodes) {
  288. // Filter out the virtual root node
  289. if (node.parent && node.parent !== flamegraph.root) {
  290. results.push(node.parent);
  291. }
  292. }
  293. return results.sort((a, b) => b.frame.totalWeight - a.frame.totalWeight);
  294. }, [nodes, flamegraph.root]);
  295. const callees = useMemo(() => {
  296. if (!nodes) {
  297. return [];
  298. }
  299. const results: FlamegraphFrame[] = [];
  300. for (const node of nodes) {
  301. for (const child of node.children) {
  302. results.push(child);
  303. }
  304. }
  305. return results.sort((a, b) => b.frame.totalWeight - a.frame.totalWeight);
  306. }, [nodes]);
  307. const onFrameHover = useCallback(
  308. (frame: FlamegraphFrame) => {
  309. props.scheduler.dispatch('highlight frame', [frame], 'hover');
  310. },
  311. [props.scheduler]
  312. );
  313. const onFrameClick = useCallback(
  314. (frame: FlamegraphFrame) => {
  315. props.scheduler.dispatch('highlight frame', [frame], 'selected');
  316. },
  317. [props.scheduler]
  318. );
  319. if (!nodes) {
  320. return null;
  321. }
  322. if (!example) {
  323. return null;
  324. }
  325. return (
  326. <AggregateFlamegraphFunctionBreakdownContainer>
  327. <AggregateFlamegraphSectionHeader>
  328. {t('Function Information')}
  329. </AggregateFlamegraphSectionHeader>
  330. <AggregateFlamegraphSection>
  331. <AggregateFlamegraphFunctionBreakdownHeader>
  332. <AggregateFlamegraphFunction
  333. frame={example}
  334. flamegraph={flamegraph}
  335. onFrameHover={onFrameHover}
  336. onFrameClick={onFrameClick}
  337. organization={organization}
  338. profileGroup={profileGroup}
  339. />
  340. </AggregateFlamegraphFunctionBreakdownHeader>
  341. </AggregateFlamegraphSection>
  342. <AggregateFlamegraphSectionHeader>
  343. {tct('Called By ([count])', {count: callers.length})}
  344. </AggregateFlamegraphSectionHeader>
  345. <AggregateFlamegraphSection>
  346. {!callers.length ? (
  347. <AggregateFlamegraphFunctionBreakdownEmptyState>
  348. <Tooltip
  349. title={t(
  350. 'When a function has no callers, it means that it is a root function.'
  351. )}
  352. >
  353. {t('No callers detected.')}
  354. </Tooltip>
  355. </AggregateFlamegraphFunctionBreakdownEmptyState>
  356. ) : (
  357. callers.map((caller, c) => (
  358. <AggregateFlamegraphFunction
  359. key={c}
  360. frame={caller}
  361. flamegraph={flamegraph}
  362. onFrameHover={onFrameHover}
  363. onFrameClick={onFrameClick}
  364. organization={organization}
  365. profileGroup={profileGroup}
  366. />
  367. ))
  368. )}
  369. </AggregateFlamegraphSection>
  370. <AggregateFlamegraphSectionHeader>
  371. {tct('Calls ([count])', {count: callees.length})}
  372. </AggregateFlamegraphSectionHeader>
  373. <AggregateFlamegraphSection>
  374. {!callees.length ? (
  375. <AggregateFlamegraphFunctionBreakdownEmptyState>
  376. <Tooltip
  377. title={t(
  378. 'When a function has no callees, it likely means that it is a leaf function, or that the profiler did not collect any samples of its callees yet.'
  379. )}
  380. >
  381. {t('No callees detected.')}
  382. </Tooltip>
  383. </AggregateFlamegraphFunctionBreakdownEmptyState>
  384. ) : (
  385. callees.map((callee, c) => (
  386. <AggregateFlamegraphFunction
  387. key={c}
  388. frame={callee}
  389. flamegraph={flamegraph}
  390. onFrameHover={onFrameHover}
  391. onFrameClick={onFrameClick}
  392. organization={organization}
  393. profileGroup={profileGroup}
  394. />
  395. ))
  396. )}
  397. </AggregateFlamegraphSection>
  398. <AggregateFlamegraphSectionHeader>
  399. <Tooltip title={t('Example profiles where this function was called.')}>
  400. {tct('Profiles ([count])', {count: example.profileIds?.length})}
  401. </Tooltip>
  402. </AggregateFlamegraphSectionHeader>
  403. <AggregateFlamegraphSection>
  404. {!example.profileIds?.length ? (
  405. <AggregateFlamegraphFunctionBreakdownEmptyState>
  406. {t('No profiles detected.')}
  407. </AggregateFlamegraphFunctionBreakdownEmptyState>
  408. ) : (
  409. example.profileIds?.map((e, i) => {
  410. return (
  411. <AggregateFlamegraphProfileReference
  412. key={i}
  413. profile={e}
  414. frameName={example.frame.name}
  415. framePackage={example.frame.package}
  416. projectLookupTable={projectsLookupTable}
  417. />
  418. );
  419. })
  420. )}
  421. </AggregateFlamegraphSection>
  422. </AggregateFlamegraphFunctionBreakdownContainer>
  423. );
  424. }
  425. function AggregateFlamegraphFunction(props: {
  426. flamegraph: Flamegraph;
  427. frame: FlamegraphFrame;
  428. onFrameClick: (frame: FlamegraphFrame) => void;
  429. onFrameHover: (frame: FlamegraphFrame) => void;
  430. organization: Organization;
  431. profileGroup: ProfileGroup;
  432. }) {
  433. const source =
  434. (props.frame.frame.file ? `${props.frame.frame.getSourceLocation()}` : null) ??
  435. '<unknown>';
  436. return (
  437. <AggregateFlamegraphFunctionContainer
  438. onPointerOver={() => props.onFrameHover(props.frame)}
  439. >
  440. <AggregateFlamegraphFunctionNameRow>
  441. <AggregateFlamegraphFunctionName onClick={() => props.onFrameClick(props.frame)}>
  442. {props.frame.frame.name}
  443. </AggregateFlamegraphFunctionName>
  444. <AggregateFlamegraphFunctionSource>
  445. <TextOverflow>{source}</TextOverflow>
  446. </AggregateFlamegraphFunctionSource>
  447. </AggregateFlamegraphFunctionNameRow>
  448. <AggregateFlamegraphSourceRow>
  449. <AggregateFlamegraphFunctionSamples>
  450. <div>
  451. <AggregateFlamegraphFunctionActionsDropdown
  452. frame={props.frame}
  453. profileGroup={props.profileGroup}
  454. organization={props.organization}
  455. />
  456. </div>
  457. <div>
  458. {PROFILING_SAMPLES_FORMATTER.format(props.frame.frame.totalWeight)}{' '}
  459. {t('samples') + ' '}
  460. {`(${formatWeightToProfileDuration(props.frame.node, props.flamegraph)})`}{' '}
  461. </div>
  462. </AggregateFlamegraphFunctionSamples>
  463. </AggregateFlamegraphSourceRow>
  464. </AggregateFlamegraphFunctionContainer>
  465. );
  466. }
  467. function AggregateFlamegraphFunctionActionsDropdown(props: {
  468. frame: FlamegraphFrame;
  469. organization: Organization;
  470. profileGroup: ProfileGroup;
  471. }) {
  472. const {projects} = useProjects();
  473. const firstProfileReference = props.frame.profileIds?.[0];
  474. const projectsLookupTable = useMemo(() => {
  475. return projects.reduce(
  476. (acc, project) => {
  477. acc[parseInt(project.id, 10)] = project;
  478. return acc;
  479. },
  480. {} as Record<number, Project>
  481. );
  482. }, [projects]);
  483. const project =
  484. firstProfileReference &&
  485. typeof firstProfileReference !== 'string' &&
  486. 'project_id' in firstProfileReference
  487. ? projectsLookupTable[firstProfileReference.project_id]
  488. : undefined;
  489. const sourceCodeLink = useSourceCodeLink({
  490. project,
  491. organization: props.organization,
  492. commitId: props.profileGroup?.metadata?.release?.lastCommit?.id,
  493. platform: props.profileGroup?.metadata?.platform || project?.platform,
  494. frame: {file: props.frame.frame.file, path: props.frame.frame.path},
  495. });
  496. const onOpenInGithubClick = useCallback(() => {
  497. if (!sourceCodeLink.isSuccess) {
  498. return;
  499. }
  500. if (
  501. !sourceCodeLink.data.sourceUrl ||
  502. sourceCodeLink.data.config?.provider?.key !== 'github'
  503. ) {
  504. return;
  505. }
  506. // make a best effort to link to the exact line if we can
  507. const url = props.frame.frame.line
  508. ? `${sourceCodeLink.data.sourceUrl}#L${props.frame.frame.line}`
  509. : sourceCodeLink.data.sourceUrl;
  510. window.open(url, '_blank', 'noopener,noreferrer');
  511. }, [props.frame, sourceCodeLink]);
  512. const onCopyFunctionName = useCallback(() => {
  513. navigator.clipboard
  514. .writeText(props.frame.frame.name)
  515. .then(() => {
  516. addSuccessMessage(t('Copied function name to clipboard'));
  517. })
  518. .catch(() => {
  519. addErrorMessage(t('Failed to copy function name to clipboard'));
  520. });
  521. }, [props.frame]);
  522. const onCopyFunctionSource = useCallback(() => {
  523. navigator.clipboard
  524. .writeText(props.frame.frame.getSourceLocation())
  525. .then(() => {
  526. addSuccessMessage(t('Copied function source to clipboard'));
  527. })
  528. .catch(() => {
  529. addErrorMessage(t('Failed to copy function source to clipboard'));
  530. });
  531. }, [props.frame]);
  532. return (
  533. <DropdownMenu
  534. trigger={triggerProps => (
  535. <AggregateFlamegraphFunctionActionsDropdownButtonWrapper>
  536. <Button aria-label={t('Actions')} size="xs" borderless {...triggerProps}>
  537. {'\u22EF'}
  538. </Button>
  539. </AggregateFlamegraphFunctionActionsDropdownButtonWrapper>
  540. )}
  541. position="bottom-end"
  542. items={[
  543. {
  544. key: 'copy-function-name',
  545. leadingItems: <IconCopy />,
  546. label: t('Copy Function Name'),
  547. disabled: !props.frame.frame.name,
  548. onAction: onCopyFunctionName,
  549. },
  550. {
  551. key: 'copy-function-source',
  552. leadingItems: <IconCopy />,
  553. label: t('Copy Source Location'),
  554. disabled: !props.frame.frame.file,
  555. onAction: onCopyFunctionSource,
  556. },
  557. {
  558. key: 'open-in-github',
  559. leadingItems: sourceCodeLink.isLoading ? (
  560. <SmallLoadingIndicator size={10} hideMessage />
  561. ) : (
  562. <IconGithub />
  563. ),
  564. label: t('Open in GitHub'),
  565. tooltip: sourceCodeLink.isSuccess
  566. ? undefined
  567. : sourceCodeLink.isError
  568. ? t('Failed to resolve source code location in Github')
  569. : undefined,
  570. disabled: !sourceCodeLink.isSuccess || !sourceCodeLink.data?.sourceUrl,
  571. onAction: onOpenInGithubClick,
  572. },
  573. ]}
  574. />
  575. );
  576. }
  577. // We need this because the styling is overriden by the dropdown menu
  578. const AggregateFlamegraphFunctionActionsDropdownButtonWrapper = styled('span')`
  579. button {
  580. padding: ${space(0.25)} ${space(0.5)} !important;
  581. height: auto !important;
  582. min-height: auto !important;
  583. }
  584. `;
  585. const SmallLoadingIndicator = styled(LoadingIndicator)`
  586. margin: 0;
  587. transform: translateX(-2px);
  588. > div {
  589. border: 2px solid ${p => p.theme.gray100} !important;
  590. border-left-color: ${p => p.theme.gray200} !important;
  591. }
  592. `;
  593. function AggregateFlamegraphProfileReference(props: {
  594. frameName: string;
  595. framePackage: string | undefined;
  596. profile: Profiling.ProfileReference;
  597. projectLookupTable: Record<string, Project>;
  598. }) {
  599. const organization = useOrganization();
  600. const project =
  601. typeof props.profile !== 'string' && 'project_id' in props.profile
  602. ? props.projectLookupTable[props.profile.project_id]
  603. : undefined;
  604. if (!project) {
  605. return null;
  606. }
  607. const to = generateProfileRouteFromProfileReference({
  608. orgSlug: organization.slug,
  609. projectSlug: project.slug,
  610. reference: props.profile,
  611. frameName: props.frameName,
  612. framePackage: props.framePackage,
  613. });
  614. const reference =
  615. typeof props.profile === 'string'
  616. ? props.profile
  617. : 'profiler_id' in props.profile
  618. ? props.profile.profiler_id
  619. : props.profile.profile_id;
  620. return (
  621. <AggregateFlamegraphProfileReferenceContainer>
  622. <AggregateFlamegraphProfileReferenceProject>
  623. <ProjectAvatar project={project} />
  624. {project.name || project.slug}
  625. </AggregateFlamegraphProfileReferenceProject>
  626. <Link to={to}>
  627. <TextOverflow>{reference.substring(0, 8)}</TextOverflow>
  628. <IconOpen />
  629. </Link>
  630. </AggregateFlamegraphProfileReferenceContainer>
  631. );
  632. }
  633. const AggregateFlamegraphFunctionContainer = styled('div')`
  634. display: grid;
  635. grid-template-columns: 1fr min-content;
  636. gap: ${space(1)};
  637. &:not(:last-child) {
  638. margin-bottom: ${space(1)};
  639. }
  640. `;
  641. const AggregateFlamegraphFunctionName = styled('button')`
  642. font-size: ${p => p.theme.fontSizeMedium};
  643. padding: 0;
  644. border: none;
  645. background: none;
  646. cursor: pointer;
  647. `;
  648. const AggregateFlamegraphFunctionBreakdownContainer = styled('div')`
  649. flex-direction: column;
  650. width: 360px;
  651. border-left: 1px solid ${p => p.theme.border};
  652. `;
  653. const AggregateFlamegraphFunctionBreakdownHeaderRow = styled('div')<{
  654. fontSize?: string;
  655. }>`
  656. display: flex;
  657. align-items: center;
  658. font-size: ${p => p.fontSize ?? p.theme.fontSizeSmall};
  659. `;
  660. const AggregateFlamegraphSourceRow = styled('div')``;
  661. const AggregateFlamegraphFunctionNameRow = styled('div')`
  662. display: flex;
  663. flex-direction: column;
  664. align-items: start;
  665. justify-content: space-between;
  666. min-width: 0;
  667. overflow: hidden;
  668. `;
  669. const AggregateFlamegraphFunctionSource = styled(
  670. AggregateFlamegraphFunctionBreakdownHeaderRow
  671. )`
  672. color: ${p => p.theme.subText};
  673. margin-top: ${space(0.25)};
  674. font-size: ${p => p.theme.fontSizeSmall};
  675. min-width: 0;
  676. cursor: pointer;
  677. width: 100%;
  678. `;
  679. const AggregateFlamegraphFunctionSamples = styled(
  680. AggregateFlamegraphFunctionBreakdownHeaderRow
  681. )`
  682. height: 100%;
  683. display: flex;
  684. flex-direction: column;
  685. align-items: flex-end;
  686. justify-content: space-between;
  687. font-size: ${p => p.theme.fontSizeSmall};
  688. color: ${p => p.theme.subText};
  689. white-space: nowrap;
  690. line-height: 1.2;
  691. `;
  692. const AggregateFlamegraphProfileReferenceContainer = styled('div')`
  693. display: flex;
  694. align-items: center;
  695. justify-content: space-between;
  696. font-family: ${p => p.theme.text.family};
  697. gap: ${space(1)};
  698. &:not(:last-child) {
  699. margin-bottom: ${space(0.5)};
  700. padding: ${space(0.25)} 0;
  701. }
  702. a {
  703. display: flex;
  704. align-items: center;
  705. gap: ${space(0.5)};
  706. color: ${p => p.theme.textColor};
  707. }
  708. `;
  709. const AggregateFlamegraphProfileReferenceProject = styled('div')`
  710. display: flex;
  711. align-items: center;
  712. gap: ${space(0.5)};
  713. `;
  714. const AggregateFlamegraphSectionHeader = styled('div')`
  715. font-size: ${p => p.theme.fontSizeSmall};
  716. color: ${p => p.theme.subText};
  717. background-color: ${p => p.theme.backgroundSecondary};
  718. padding: ${space(0.5)} ${space(1)};
  719. border-bottom: 1px solid ${p => p.theme.border};
  720. text-transform: uppercase;
  721. font-weight: ${p => p.theme.fontWeightBold};
  722. &:not(:first-child) {
  723. border-top: 1px solid ${p => p.theme.border};
  724. }
  725. `;
  726. const AggregateFlamegraphSection = styled('div')`
  727. padding: ${space(1)};
  728. `;
  729. const AggregateFlamegraphFunctionBreakdownEmptyState = styled('div')`
  730. display: flex;
  731. justify-content: space-between;
  732. align-items: center;
  733. color: ${p => p.theme.subText};
  734. text-align: center;
  735. `;
  736. const AggregateFlamegraphFunctionBreakdownHeader = styled('div')`
  737. width: 100%;
  738. `;
  739. const AggregateFlamegraphCanvasContainer = styled('div')`
  740. display: grid;
  741. height: 100%;
  742. grid-template-columns: 1fr min-content;
  743. `;
  744. const AggregateFlamegraphSearch = styled(FlamegraphSearch)`
  745. max-width: 300px;
  746. `;
  747. const AggregateFlamegraphToolbarContainer = styled('div')`
  748. display: flex;
  749. justify-content: space-between;
  750. gap: ${space(1)};
  751. padding: ${space(1)} ${space(1)};
  752. /*
  753. force height to be the same as profile digest header,
  754. but subtract 1px for the border that doesnt exist on the header
  755. */
  756. height: 41px;
  757. border-bottom: 1px solid ${p => p.theme.border};
  758. `;
  759. const ViewSelectContainer = styled('div')`
  760. min-width: 160px;
  761. `;
  762. const RequestStateMessageContainer = styled('div')`
  763. position: absolute;
  764. left: 0;
  765. right: 0;
  766. top: 0;
  767. bottom: 0;
  768. display: flex;
  769. justify-content: center;
  770. align-items: center;
  771. color: ${p => p.theme.subText};
  772. `;
  773. const AggregateFlamegraphContainer = styled('div')`
  774. display: flex;
  775. flex-direction: column;
  776. flex: 1 1 100%;
  777. height: 100%;
  778. width: 100%;
  779. overflow: hidden;
  780. position: absolute;
  781. left: 0px;
  782. top: 0px;
  783. `;