eventDifferentialFlamegraph.tsx 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663
  1. import {Fragment, useCallback, useEffect, useMemo, useReducer, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import * as Sentry from '@sentry/react';
  4. import type {LocationDescriptor} from 'history';
  5. import {Button} from 'sentry/components/button';
  6. import ButtonBar from 'sentry/components/buttonBar';
  7. import Link from 'sentry/components/links/link';
  8. import LoadingIndicator from 'sentry/components/loadingIndicator';
  9. import Panel from 'sentry/components/panels/panel';
  10. import Placeholder from 'sentry/components/placeholder';
  11. import {DifferentialFlamegraph} from 'sentry/components/profiling/flamegraph/differentialFlamegraph';
  12. import {DifferentialFlamegraphToolbar} from 'sentry/components/profiling/flamegraph/flamegraphToolbar/differentialFlamegraphToolbar';
  13. import {IconChevron} from 'sentry/icons/iconChevron';
  14. import {t} from 'sentry/locale';
  15. import ProjectsStore from 'sentry/stores/projectsStore';
  16. import {space} from 'sentry/styles/space';
  17. import type {Event} from 'sentry/types/event';
  18. import type {Project} from 'sentry/types/project';
  19. import {defined} from 'sentry/utils';
  20. import {formatAbbreviatedNumber} from 'sentry/utils/formatters';
  21. import {formatPercentage} from 'sentry/utils/number/formatPercentage';
  22. import {
  23. CanvasPoolManager,
  24. useCanvasScheduler,
  25. } from 'sentry/utils/profiling/canvasScheduler';
  26. import {colorComponentsToRGBA} from 'sentry/utils/profiling/colors/utils';
  27. import type {DifferentialFlamegraph as DifferentialFlamegraphModel} from 'sentry/utils/profiling/differentialFlamegraph';
  28. import {FlamegraphStateProvider} from 'sentry/utils/profiling/flamegraph/flamegraphStateProvider/flamegraphContextProvider';
  29. import {FlamegraphThemeProvider} from 'sentry/utils/profiling/flamegraph/flamegraphThemeProvider';
  30. import {useFlamegraphTheme} from 'sentry/utils/profiling/flamegraph/useFlamegraphTheme';
  31. import type {FlamegraphFrame} from 'sentry/utils/profiling/flamegraphFrame';
  32. import type {Frame} from 'sentry/utils/profiling/frame';
  33. import {useDifferentialFlamegraphModel} from 'sentry/utils/profiling/hooks/useDifferentialFlamegraphModel';
  34. import type {DifferentialFlamegraphQueryResult} from 'sentry/utils/profiling/hooks/useDifferentialFlamegraphQuery';
  35. import {useDifferentialFlamegraphQuery} from 'sentry/utils/profiling/hooks/useDifferentialFlamegraphQuery';
  36. import {generateProfileRouteFromProfileReference} from 'sentry/utils/profiling/routes';
  37. import {relativeChange} from 'sentry/utils/profiling/units/units';
  38. import useOrganization from 'sentry/utils/useOrganization';
  39. import usePageFilters from 'sentry/utils/usePageFilters';
  40. import {LOADING_PROFILE_GROUP} from 'sentry/views/profiling/profileGroupProvider';
  41. interface EventDifferentialFlamegraphProps {
  42. event: Event;
  43. }
  44. export function EventDifferentialFlamegraph(props: EventDifferentialFlamegraphProps) {
  45. const selection = usePageFilters();
  46. const evidenceData = props.event.occurrence?.evidenceData;
  47. const fingerprint = evidenceData?.fingerprint;
  48. const breakpoint = evidenceData?.breakpoint;
  49. const isValid = fingerprint !== undefined && breakpoint !== undefined;
  50. const project = useMemo(() => {
  51. return ProjectsStore.getById(props.event.projectID);
  52. }, [props.event.projectID]);
  53. useEffect(() => {
  54. if (isValid) {
  55. return;
  56. }
  57. Sentry.withScope(scope => {
  58. scope.setContext('evidence data fields', {
  59. fingerprint,
  60. breakpoint,
  61. });
  62. Sentry.captureException(
  63. new Error('Missing required evidence data on function regression issue.')
  64. );
  65. });
  66. }, [isValid, fingerprint, breakpoint]);
  67. const {before, after} = useDifferentialFlamegraphQuery({
  68. projectID: parseInt(props.event.projectID, 10),
  69. breakpoint,
  70. environments: selection.selection.environments,
  71. fingerprint: props.event.occurrence?.evidenceData?.fingerprint,
  72. });
  73. return (
  74. <Fragment>
  75. <FlamegraphThemeProvider>
  76. <FlamegraphStateProvider
  77. initialState={{
  78. preferences: {
  79. sorting: 'alphabetical',
  80. view: 'top down',
  81. },
  82. }}
  83. >
  84. <EventDifferentialFlamegraphView
  85. project={project}
  86. before={before}
  87. after={after}
  88. />
  89. </FlamegraphStateProvider>
  90. </FlamegraphThemeProvider>
  91. </Fragment>
  92. );
  93. }
  94. function applicationFrameOnly(frame: Frame): boolean {
  95. return frame.is_application;
  96. }
  97. function systemFrameOnly(frame: Frame): boolean {
  98. return !frame.is_application;
  99. }
  100. interface EventDifferentialFlamegraphViewProps {
  101. after: DifferentialFlamegraphQueryResult['before'];
  102. before: DifferentialFlamegraphQueryResult['after'];
  103. project: Project | undefined;
  104. }
  105. function EventDifferentialFlamegraphView(props: EventDifferentialFlamegraphViewProps) {
  106. const organization = useOrganization();
  107. const [frameFilterSetting, setFrameFilterSetting] = useState<
  108. 'application' | 'system' | 'all'
  109. >('all');
  110. const frameFilter =
  111. frameFilterSetting === 'application'
  112. ? applicationFrameOnly
  113. : frameFilterSetting === 'system'
  114. ? systemFrameOnly
  115. : undefined;
  116. const [negated, setNegated] = useState<boolean>(false);
  117. const canvasPoolManager = useMemo(() => new CanvasPoolManager(), []);
  118. const scheduler = useCanvasScheduler(canvasPoolManager);
  119. const {differentialFlamegraph, afterProfileGroup} = useDifferentialFlamegraphModel({
  120. before: props.before,
  121. after: props.after,
  122. negated,
  123. frameFilter,
  124. });
  125. const makeFunctionFlamechartLink = useCallback(
  126. (frame: FlamegraphFrame): LocationDescriptor => {
  127. if (!props.project) {
  128. return '';
  129. }
  130. if (!frame.profileIds?.length) {
  131. return '';
  132. }
  133. const profile = frame.profileIds?.[0];
  134. if (!defined(profile)) {
  135. return '';
  136. }
  137. return (
  138. generateProfileRouteFromProfileReference({
  139. orgSlug: organization.slug,
  140. projectSlug: props.project.slug,
  141. reference: profile,
  142. framePackage: frame.frame.package,
  143. frameName: frame.frame.name,
  144. }) ?? ''
  145. );
  146. },
  147. [organization.slug, props.project]
  148. );
  149. return (
  150. <FlamegraphContainer>
  151. <StyledPanel>
  152. <DifferentialFlamegraphToolbar
  153. frameFilter={frameFilterSetting}
  154. onFrameFilterChange={setFrameFilterSetting}
  155. negated={negated}
  156. onNegatedChange={setNegated}
  157. flamegraph={differentialFlamegraph}
  158. canvasPoolManager={canvasPoolManager}
  159. />
  160. <DifferentialFlamegraphContainer>
  161. {props.after.isPending || props.before.isPending ? (
  162. <LoadingIndicatorContainer>
  163. <LoadingIndicator />
  164. </LoadingIndicatorContainer>
  165. ) : props.before.isError && props.after.isError ? (
  166. <ErrorMessageContainer>
  167. {t('Failed to load flamegraph for before and after regression time range.')}
  168. </ErrorMessageContainer>
  169. ) : props.before.isError ? (
  170. <ErrorMessageContainer>
  171. {t('Failed to load flamegraph for before regression time range.')}
  172. </ErrorMessageContainer>
  173. ) : props.after.isError ? (
  174. <ErrorMessageContainer>
  175. {t('Failed to load flamegraph for after regression time range.')}
  176. </ErrorMessageContainer>
  177. ) : null}
  178. <DifferentialFlamegraph
  179. profileGroup={afterProfileGroup ?? LOADING_PROFILE_GROUP}
  180. differentialFlamegraph={differentialFlamegraph}
  181. canvasPoolManager={canvasPoolManager}
  182. scheduler={scheduler}
  183. />
  184. </DifferentialFlamegraphContainer>
  185. <DifferentialFlamegraphExplanationBar negated={negated} />
  186. </StyledPanel>
  187. <StyledPanel>
  188. <DifferentialFlamegraphFunctionsContainer>
  189. <DifferentialFlamegraphChangedFunctions
  190. loading={props.after.isPending || props.before.isPending}
  191. title={t('Slower functions')}
  192. subtitle={t('after regression')}
  193. functions={differentialFlamegraph.increasedFrames}
  194. flamegraph={differentialFlamegraph}
  195. makeFunctionLink={makeFunctionFlamechartLink}
  196. />
  197. <DifferentialFlamegraphChangedFunctions
  198. loading={props.after.isPending || props.before.isPending}
  199. title={t('Faster functions')}
  200. subtitle={t('after regression')}
  201. functions={differentialFlamegraph.decreasedFrames}
  202. flamegraph={differentialFlamegraph}
  203. makeFunctionLink={makeFunctionFlamechartLink}
  204. />
  205. </DifferentialFlamegraphFunctionsContainer>
  206. </StyledPanel>
  207. <StyledPanel>
  208. <DifferentialFlamegraphFunctionsContainer>
  209. <DifferentialFlamegraphChangedFunctions
  210. loading={props.after.isPending || props.before.isPending}
  211. title={t('New functions')}
  212. subtitle={t('after regression')}
  213. functions={differentialFlamegraph.newFrames}
  214. flamegraph={differentialFlamegraph}
  215. makeFunctionLink={makeFunctionFlamechartLink}
  216. />
  217. <DifferentialFlamegraphChangedFunctions
  218. loading={props.after.isPending || props.before.isPending}
  219. title={t('Removed functions')}
  220. subtitle={t('after regression')}
  221. functions={differentialFlamegraph.removedFrames}
  222. flamegraph={differentialFlamegraph}
  223. makeFunctionLink={makeFunctionFlamechartLink}
  224. />
  225. </DifferentialFlamegraphFunctionsContainer>
  226. </StyledPanel>
  227. </FlamegraphContainer>
  228. );
  229. }
  230. interface PaginationReducerState {
  231. page: number;
  232. pageCount: number;
  233. pageSize: number;
  234. }
  235. type PaginationReducerAction =
  236. | {type: 'next'}
  237. | {type: 'previous'}
  238. | {list: any[]; pageSize: number; type: 'initialize'};
  239. function paginationReducer(
  240. state: PaginationReducerState,
  241. action: PaginationReducerAction
  242. ): PaginationReducerState {
  243. switch (action.type) {
  244. case 'initialize': {
  245. return {
  246. page: 0,
  247. pageCount: Math.ceil(action.list.length / action.pageSize),
  248. pageSize: action.pageSize,
  249. };
  250. }
  251. case 'next':
  252. return {
  253. ...state,
  254. page: Math.min(state.page + 1, state.pageCount - 1),
  255. };
  256. case 'previous':
  257. return {
  258. ...state,
  259. page: Math.max(state.page - 1, 0),
  260. };
  261. default:
  262. return state;
  263. }
  264. }
  265. interface DifferentialFlamegraphChangedFunctionsProps {
  266. flamegraph: DifferentialFlamegraphModel;
  267. functions:
  268. | DifferentialFlamegraphModel['increasedFrames']
  269. | DifferentialFlamegraphModel['newFrames'];
  270. loading: boolean;
  271. makeFunctionLink: (frame: FlamegraphFrame) => LocationDescriptor;
  272. subtitle: string;
  273. title: string;
  274. }
  275. function DifferentialFlamegraphChangedFunctions(
  276. props: DifferentialFlamegraphChangedFunctionsProps
  277. ) {
  278. const theme = useFlamegraphTheme();
  279. const [state, dispatch] = useReducer(paginationReducer, {
  280. page: 0,
  281. pageSize: 0,
  282. pageCount: 0,
  283. });
  284. const onPreviousPaginationClick = useMemo(() => {
  285. if (state.page === 0) {
  286. return undefined;
  287. }
  288. return () => dispatch({type: 'previous'});
  289. }, [state.page]);
  290. const onNextPaginationClick = useMemo(() => {
  291. if (state.page + 1 === state.pageCount) {
  292. return undefined;
  293. }
  294. return () => dispatch({type: 'next'});
  295. }, [state.page, state.pageCount]);
  296. useEffect(() => {
  297. dispatch({
  298. list: props.functions,
  299. pageSize: 5,
  300. type: 'initialize',
  301. });
  302. }, [props.functions]);
  303. return (
  304. <DifferentialFlamegraphFunctionsWrapper>
  305. <DifferentialFlamegraphChangedFunctionsTitle
  306. title={props.title}
  307. subtitle={props.subtitle}
  308. onNextPageClick={onNextPaginationClick}
  309. onPreviousPageClick={onPreviousPaginationClick}
  310. />
  311. {props.loading
  312. ? new Array(5).fill(0).map((_, idx) => {
  313. return (
  314. <DifferentialFlamegraphChangedFunctionContainer key={idx}>
  315. <div>
  316. <Placeholder
  317. height="16px"
  318. width="66%"
  319. style={MARGIN_BOTTOM_PLACEHOLDER_STYLES}
  320. />
  321. <Placeholder height="16px" width="48%" />
  322. </div>
  323. <DifferentialFlamegraphChangedFunctionStats>
  324. <Placeholder
  325. height="16px"
  326. width="32px"
  327. style={RIGHT_ALIGN_PLACEHOLDER_STYLES}
  328. />
  329. <Placeholder height="16px" width="56px" />
  330. </DifferentialFlamegraphChangedFunctionStats>
  331. </DifferentialFlamegraphChangedFunctionContainer>
  332. );
  333. })
  334. : props.functions
  335. .slice(
  336. state.page * state.pageSize,
  337. state.page * state.pageSize + state.pageSize
  338. )
  339. .map((f, idx) => {
  340. const frame = f;
  341. if (!frame) {
  342. throw new Error('Frame is falsy, this should never happen');
  343. }
  344. const change = props.flamegraph.weights.get(frame.node);
  345. const linkToFlamechart = props.makeFunctionLink(frame);
  346. return (
  347. <DifferentialFlamegraphChangedFunctionContainer key={idx}>
  348. <div>
  349. <DifferentialFlamegraphChangedFunctionNameLink
  350. disabled={!linkToFlamechart}
  351. to={linkToFlamechart}
  352. >
  353. <DifferentialFlamegraphFunctionColorIndicator
  354. style={{
  355. backgroundColor: colorComponentsToRGBA(
  356. props.flamegraph.colors.get(frame.node) ??
  357. theme.COLORS.FRAME_FALLBACK_COLOR
  358. ),
  359. }}
  360. />
  361. <span>{frame.frame.name}</span>
  362. </DifferentialFlamegraphChangedFunctionNameLink>
  363. <DifferentialFlamegraphChangedFunctionModule>
  364. {frame.frame.module || frame.frame.package || frame.frame.file}
  365. </DifferentialFlamegraphChangedFunctionModule>
  366. </div>
  367. {change ? (
  368. <DifferentialFlamegraphChangedFunctionStats>
  369. <div>
  370. {change.after > change.before ? '+' : ''}
  371. {formatPercentage(relativeChange(change.after, change.before))}
  372. {/* diff % */}
  373. {/* n samples, x weight */}
  374. </div>
  375. <DifferentialFlamegraphFunctionSecondaryStats>
  376. {formatAbbreviatedNumber(f.node.totalWeight)} {t('samples')}
  377. </DifferentialFlamegraphFunctionSecondaryStats>
  378. </DifferentialFlamegraphChangedFunctionStats>
  379. ) : null}
  380. </DifferentialFlamegraphChangedFunctionContainer>
  381. );
  382. })}
  383. </DifferentialFlamegraphFunctionsWrapper>
  384. );
  385. }
  386. const DifferentialFlamegraphFunctionsWrapper = styled('div')`
  387. flex: 1;
  388. width: 50%;
  389. &:first-child {
  390. padding-right: ${space(0.5)};
  391. }
  392. &:nth-child(2) {
  393. padding-left: ${space(0.5)};
  394. }
  395. `;
  396. const DifferentialFlamegraphFunctionColorIndicator = styled('div')`
  397. width: 10px;
  398. height: 10px;
  399. border-radius: 2px;
  400. display: inline-block;
  401. border: 1px solid ${p => p.theme.border};
  402. margin-right: ${space(0.25)};
  403. background-color: ${p => p.theme.green300};
  404. `;
  405. const RIGHT_ALIGN_PLACEHOLDER_STYLES: React.CSSProperties = {
  406. marginBottom: '4px',
  407. marginLeft: 'auto',
  408. justifySelf: 'flex-end',
  409. };
  410. const MARGIN_BOTTOM_PLACEHOLDER_STYLES: React.CSSProperties = {
  411. marginBottom: '4px',
  412. };
  413. const DifferentialFlamegraphChangedFunctionStats = styled('div')`
  414. text-align: right;
  415. flex-shrink: 0;
  416. `;
  417. const DifferentialFlamegraphFunctionSecondaryStats = styled('div')`
  418. color: ${p => p.theme.subText};
  419. font-size: ${p => p.theme.fontSizeSmall};
  420. `;
  421. const DifferentialFlamegraphChangedFunctionNameLink = styled(Link)`
  422. display: flex;
  423. flex-direction: row;
  424. align-items: center;
  425. white-space: nowrap;
  426. > span {
  427. overflow: hidden;
  428. text-overflow: ellipsis;
  429. min-width: 0;
  430. }
  431. `;
  432. const DifferentialFlamegraphChangedFunctionModule = styled('div')`
  433. color: ${p => p.theme.subText};
  434. min-width: 0;
  435. text-overflow: ellipsis;
  436. overflow: hidden;
  437. `;
  438. const DifferentialFlamegraphChangedFunctionContainer = styled('div')`
  439. height: 48px;
  440. display: flex;
  441. flex-direction: row;
  442. justify-content: space-between;
  443. gap: ${space(1)};
  444. padding: ${space(0.5)} 0;
  445. > *:first-child {
  446. min-width: 0;
  447. flex: 1;
  448. }
  449. `;
  450. interface DifferentialFlamegraphExplanationBarProps {
  451. negated: boolean;
  452. }
  453. function DifferentialFlamegraphExplanationBar(
  454. props: DifferentialFlamegraphExplanationBarProps
  455. ) {
  456. return (
  457. <DifferentialFlamegraphExplanationBarContainer>
  458. <div>
  459. {props.negated
  460. ? t(`Flamegraph is showing how stack frequency will change.`)
  461. : t(`Flamegraph is showing how stack frequency has changed.`)}
  462. </div>
  463. <DifferentialFlamegraphLegend />
  464. </DifferentialFlamegraphExplanationBarContainer>
  465. );
  466. }
  467. const DifferentialFlamegraphExplanationBarContainer = styled('div')`
  468. display: flex;
  469. justify-content: space-between;
  470. border-radius: 0 0 ${p => p.theme.borderRadius} ${p => p.theme.borderRadius};
  471. padding: ${space(0.5)} ${space(1)};
  472. font-size: ${p => p.theme.fontSizeExtraSmall};
  473. color: ${p => p.theme.subText};
  474. border-top: 1px solid ${p => p.theme.border};
  475. background: ${p => p.theme.backgroundSecondary};
  476. `;
  477. function DifferentialFlamegraphLegend() {
  478. const theme = useFlamegraphTheme();
  479. const {increaseColor, decreaseColor, neutralColor} = useMemo(() => {
  480. return {
  481. increaseColor: theme.COLORS.DIFFERENTIAL_INCREASE.map(n => n * 255)
  482. .concat(0.8)
  483. .join(','),
  484. neutralColor: theme.COLORS.FRAME_FALLBACK_COLOR.slice(0, 3)
  485. .map(n => n * 255)
  486. .concat(0.2)
  487. .join(','),
  488. decreaseColor: theme.COLORS.DIFFERENTIAL_DECREASE.map(n => n * 255)
  489. .concat(0.8)
  490. .join(','),
  491. };
  492. }, [theme]);
  493. return (
  494. <DifferentialFlamegraphLegendContainer>
  495. <div>+</div>
  496. <DifferentialFlamegraphLegendBar
  497. style={{
  498. background: `linear-gradient(90deg, rgba(${increaseColor}) 0%, rgba(${neutralColor}) 50%, rgba(${decreaseColor}) 100%)`,
  499. }}
  500. />
  501. <div>-</div>
  502. </DifferentialFlamegraphLegendContainer>
  503. );
  504. }
  505. const DifferentialFlamegraphLegendContainer = styled('div')`
  506. display: flex;
  507. flex-direction: row;
  508. justify-content: space-between;
  509. align-items: center;
  510. `;
  511. const DifferentialFlamegraphLegendBar = styled('div')`
  512. width: 60px;
  513. height: 14px;
  514. margin: 0 ${space(0.5)};
  515. `;
  516. function DifferentialFlamegraphChangedFunctionsTitle(props: {
  517. onNextPageClick: (() => void) | undefined;
  518. onPreviousPageClick: (() => void) | undefined;
  519. subtitle: string;
  520. title: string;
  521. }) {
  522. return (
  523. <DifferentialFlamegraphChangedFunctionsTitleContainer>
  524. <DifferentialFlamegraphChangedFunctionsTitleText>
  525. <div>{props.title}</div>
  526. <DifferentialFlamegraphChangedFunctionsSubtitleText>
  527. {props.subtitle}
  528. </DifferentialFlamegraphChangedFunctionsSubtitleText>
  529. </DifferentialFlamegraphChangedFunctionsTitleText>
  530. <ButtonBar merged>
  531. <DifferentialFlamegraphPaginationButton
  532. size="xs"
  533. disabled={!props.onPreviousPageClick}
  534. onClick={props.onPreviousPageClick}
  535. icon={<IconChevron direction="left" />}
  536. aria-label={t('Previous page')}
  537. />
  538. <DifferentialFlamegraphPaginationButton
  539. size="xs"
  540. disabled={!props.onNextPageClick}
  541. onClick={props.onNextPageClick}
  542. icon={<IconChevron direction="right" />}
  543. aria-label={t('Next page')}
  544. />
  545. </ButtonBar>
  546. </DifferentialFlamegraphChangedFunctionsTitleContainer>
  547. );
  548. }
  549. const DifferentialFlamegraphChangedFunctionsTitleContainer = styled('div')`
  550. display: flex;
  551. justify-content: space-between;
  552. align-items: center;
  553. `;
  554. const DifferentialFlamegraphChangedFunctionsTitleText = styled('div')`
  555. font-weight: ${p => p.theme.fontWeightBold};
  556. flex: 1;
  557. `;
  558. const DifferentialFlamegraphChangedFunctionsSubtitleText = styled('div')`
  559. font-weight: ${p => p.theme.fontWeightNormal};
  560. font-size: ${p => p.theme.fontSizeSmall};
  561. color: ${p => p.theme.subText};
  562. `;
  563. const DifferentialFlamegraphFunctionsContainer = styled('div')`
  564. display: flex;
  565. flex-direction: row;
  566. padding: ${space(1)};
  567. `;
  568. const DifferentialFlamegraphPaginationButton = styled(Button)`
  569. padding-left: ${space(0.75)};
  570. padding-right: ${space(0.75)};
  571. `;
  572. const ErrorMessageContainer = styled('div')`
  573. position: absolute;
  574. display: flex;
  575. flex-direction: column;
  576. justify-content: center;
  577. width: 100%;
  578. height: 100%;
  579. background-color: ${p => p.theme.background};
  580. color: ${p => p.theme.subText};
  581. text-align: center;
  582. padding: ${space(2)} ${space(4)};
  583. `;
  584. const LoadingIndicatorContainer = styled('div')`
  585. position: absolute;
  586. display: flex;
  587. flex-direction: column;
  588. justify-content: center;
  589. width: 100%;
  590. height: 100%;
  591. `;
  592. const DifferentialFlamegraphContainer = styled('div')`
  593. position: relative;
  594. width: 100%;
  595. height: 420px;
  596. `;
  597. const FlamegraphContainer = styled('div')`
  598. display: flex;
  599. flex-direction: column;
  600. gap: ${space(1.5)};
  601. `;
  602. const StyledPanel = styled(Panel)`
  603. margin-bottom: 0;
  604. `;