profileCharts.tsx 5.9 KB

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