profileCharts.tsx 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. import {useMemo} from 'react';
  2. import {InjectedRouter} from 'react-router';
  3. import {useTheme} from '@emotion/react';
  4. import styled from '@emotion/styled';
  5. import {AreaChart} from 'sentry/components/charts/areaChart';
  6. import ChartZoom from 'sentry/components/charts/chartZoom';
  7. import {HeaderTitle} from 'sentry/components/charts/styles';
  8. import {Panel} from 'sentry/components/panels';
  9. import {t} from 'sentry/locale';
  10. import space from 'sentry/styles/space';
  11. import {PageFilters} from 'sentry/types';
  12. import {Series} from 'sentry/types/echarts';
  13. import {axisLabelFormatter, tooltipFormatter} from 'sentry/utils/discover/charts';
  14. import {aggregateOutputType} from 'sentry/utils/discover/fields';
  15. import {useProfileEventsStats} from 'sentry/utils/profiling/hooks/useProfileEventsStats';
  16. interface ProfileChartsProps {
  17. query: string;
  18. router: InjectedRouter;
  19. selection?: PageFilters;
  20. }
  21. // We want p99 to be before p75 because echarts renders the series in order.
  22. // So if p75 is before p99, p99 will be rendered on top of p75 which will
  23. // cover it up.
  24. const SERIES_ORDER = ['count()', 'p99()', 'p95()', 'p75()'] as const;
  25. export function ProfileCharts({query, router, selection}: ProfileChartsProps) {
  26. const theme = useTheme();
  27. const profileStats = useProfileEventsStats({
  28. query,
  29. referrer: 'api.profiling.landing-chart',
  30. yAxes: SERIES_ORDER,
  31. });
  32. const series: Series[] = useMemo(() => {
  33. if (profileStats.status !== 'success') {
  34. return [];
  35. }
  36. // the timestamps in the response is in seconds but echarts expects
  37. // a timestamp in milliseconds, so multiply by 1e3 to do the conversion
  38. const timestamps = profileStats.data[0].timestamps.map(ts => ts * 1e3);
  39. const allSeries = profileStats.data[0].data
  40. .filter(rawData => SERIES_ORDER.indexOf(rawData.axis) > -1)
  41. .map(rawData => {
  42. if (timestamps.length !== rawData.values.length) {
  43. throw new Error('Invalid stats response');
  44. }
  45. if (rawData.axis === 'count()') {
  46. return {
  47. data: rawData.values.map((value, i) => ({
  48. name: timestamps[i]!,
  49. // the response value contains nulls when no data is
  50. // available, use 0 to represent it
  51. value: value ?? 0,
  52. })),
  53. seriesName: rawData.axis,
  54. xAxisIndex: 0,
  55. yAxisIndex: 0,
  56. };
  57. }
  58. return {
  59. data: rawData.values.map((value, i) => ({
  60. name: timestamps[i]!,
  61. // the response value contains nulls when no data
  62. // is available, use 0 to represent it
  63. value: (value ?? 0) / 1e6, // convert ns to ms
  64. })),
  65. seriesName: rawData.axis,
  66. xAxisIndex: 1,
  67. yAxisIndex: 1,
  68. };
  69. });
  70. allSeries.sort((a, b) => {
  71. const idxA = SERIES_ORDER.indexOf(a.seriesName as any);
  72. const idxB = SERIES_ORDER.indexOf(b.seriesName as any);
  73. return idxA - idxB;
  74. });
  75. return allSeries;
  76. }, [profileStats]);
  77. return (
  78. <ChartZoom router={router} {...selection?.datetime}>
  79. {zoomRenderProps => (
  80. <StyledPanel>
  81. <TitleContainer>
  82. <StyledHeaderTitle>{t('Profiles by Count')}</StyledHeaderTitle>
  83. <StyledHeaderTitle>{t('Profiles by Percentiles')}</StyledHeaderTitle>
  84. </TitleContainer>
  85. <AreaChart
  86. height={300}
  87. series={series}
  88. grid={[
  89. {
  90. top: '32px',
  91. left: '24px',
  92. right: '52%',
  93. bottom: '16px',
  94. },
  95. {
  96. top: '32px',
  97. left: '52%',
  98. right: '24px',
  99. bottom: '16px',
  100. },
  101. ]}
  102. legend={{
  103. right: 16,
  104. top: 12,
  105. data: SERIES_ORDER.slice(),
  106. }}
  107. axisPointer={{
  108. link: [{xAxisIndex: [0, 1]}],
  109. }}
  110. xAxes={[
  111. {
  112. gridIndex: 0,
  113. type: 'time' as const,
  114. },
  115. {
  116. gridIndex: 1,
  117. type: 'time' as const,
  118. },
  119. ]}
  120. yAxes={[
  121. {
  122. gridIndex: 0,
  123. scale: true,
  124. axisLabel: {
  125. color: theme.chartLabel,
  126. formatter(value: number) {
  127. return axisLabelFormatter(value, 'integer');
  128. },
  129. },
  130. },
  131. {
  132. gridIndex: 1,
  133. scale: true,
  134. axisLabel: {
  135. color: theme.chartLabel,
  136. formatter(value: number) {
  137. return axisLabelFormatter(value, 'duration');
  138. },
  139. },
  140. },
  141. ]}
  142. tooltip={{
  143. valueFormatter: (value, label) =>
  144. tooltipFormatter(value, aggregateOutputType(label)),
  145. }}
  146. isGroupedByDate
  147. showTimeInTooltip
  148. {...zoomRenderProps}
  149. />
  150. </StyledPanel>
  151. )}
  152. </ChartZoom>
  153. );
  154. }
  155. const StyledPanel = styled(Panel)`
  156. padding-top: ${space(2)};
  157. `;
  158. const TitleContainer = styled('div')`
  159. width: 100%;
  160. display: flex;
  161. flex-direction: row;
  162. `;
  163. const StyledHeaderTitle = styled(HeaderTitle)`
  164. flex-grow: 1;
  165. margin-left: ${space(2)};
  166. `;