profileCharts.tsx 5.7 KB

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