profilesSummaryChart.tsx 4.8 KB

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