profileCharts.tsx 5.3 KB

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