profilesSummaryChart.tsx 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. import {useMemo} from 'react';
  2. import {useTheme} from '@emotion/react';
  3. import styled from '@emotion/styled';
  4. import {AreaChart, AreaChartProps} from 'sentry/components/charts/areaChart';
  5. import ChartZoom from 'sentry/components/charts/chartZoom';
  6. import {t} from 'sentry/locale';
  7. import {space} from 'sentry/styles/space';
  8. import {PageFilters} from 'sentry/types';
  9. import {Series} from 'sentry/types/echarts';
  10. import {axisLabelFormatter, tooltipFormatter} from 'sentry/utils/discover/charts';
  11. import {aggregateOutputType} from 'sentry/utils/discover/fields';
  12. import {useProfileEventsStats} from 'sentry/utils/profiling/hooks/useProfileEventsStats';
  13. import useRouter from 'sentry/utils/useRouter';
  14. // We want p99 to be before p75 because echarts renders the series in order.
  15. // So if p75 is before p99, p99 will be rendered on top of p75 which will
  16. // cover it up.
  17. const SERIES_ORDER = ['count()', 'p99()', 'p95()', 'p75()'] as const;
  18. interface ProfileSummaryChartProps {
  19. query: string;
  20. referrer: string;
  21. hideCount?: boolean;
  22. selection?: PageFilters;
  23. }
  24. export function ProfilesSummaryChart({
  25. query,
  26. referrer,
  27. selection,
  28. hideCount,
  29. }: ProfileSummaryChartProps) {
  30. const router = useRouter();
  31. const theme = useTheme();
  32. const seriesOrder = useMemo(() => {
  33. if (hideCount) {
  34. return SERIES_ORDER.filter(s => s !== 'count()');
  35. }
  36. return SERIES_ORDER;
  37. }, [hideCount]);
  38. const profileStats = useProfileEventsStats({
  39. dataset: 'profiles',
  40. query,
  41. referrer,
  42. yAxes: seriesOrder,
  43. });
  44. const series: Series[] = useMemo(() => {
  45. if (profileStats.status !== 'success') {
  46. return [];
  47. }
  48. // the timestamps in the response is in seconds but echarts expects
  49. // a timestamp in milliseconds, so multiply by 1e3 to do the conversion
  50. const timestamps = profileStats.data.timestamps.map(ts => ts * 1e3);
  51. const allSeries = profileStats.data.data
  52. .filter(rawData => seriesOrder.includes(rawData.axis))
  53. .map(rawData => {
  54. if (timestamps.length !== rawData.values.length) {
  55. throw new Error('Invalid stats response');
  56. }
  57. if (rawData.axis === 'count()') {
  58. return {
  59. data: rawData.values.map((value, i) => ({
  60. name: timestamps[i]!,
  61. // the response value contains nulls when no data is
  62. // available, use 0 to represent it
  63. value: value ?? 0,
  64. })),
  65. seriesName: rawData.axis,
  66. xAxisIndex: 0,
  67. yAxisIndex: 0,
  68. };
  69. }
  70. return {
  71. data: rawData.values.map((value, i) => ({
  72. name: timestamps[i]!,
  73. // the response value contains nulls when no data
  74. // is available, use 0 to represent it
  75. value: value ?? 0,
  76. })),
  77. seriesName: rawData.axis,
  78. xAxisIndex: 1,
  79. yAxisIndex: 1,
  80. };
  81. });
  82. allSeries.sort((a, b) => {
  83. const idxA = seriesOrder.indexOf(a.seriesName as any);
  84. const idxB = seriesOrder.indexOf(b.seriesName as any);
  85. return idxA - idxB;
  86. });
  87. return allSeries;
  88. }, [profileStats, seriesOrder]);
  89. const chartProps: AreaChartProps = useMemo(() => {
  90. const baseProps: AreaChartProps = {
  91. height: 150,
  92. series,
  93. grid: [
  94. {
  95. top: '8px',
  96. left: '16px',
  97. right: '8px',
  98. bottom: '16px',
  99. },
  100. {
  101. top: '8px',
  102. left: '8px',
  103. right: '16px',
  104. bottom: '8px',
  105. },
  106. ],
  107. legend: {
  108. right: 16,
  109. top: 12,
  110. data: seriesOrder.slice(),
  111. },
  112. tooltip: {
  113. valueFormatter: (value, label) =>
  114. tooltipFormatter(value, aggregateOutputType(label)),
  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. };
  157. return baseProps;
  158. }, [hideCount, series, seriesOrder, theme.chartLabel]);
  159. return (
  160. <ProfilesChartContainer>
  161. <ProfilesChartTitle>{t('Profile durations')}</ProfilesChartTitle>
  162. <ChartZoom router={router} {...selection?.datetime}>
  163. {zoomRenderProps => (
  164. <AreaChart
  165. {...chartProps}
  166. isGroupedByDate
  167. showTimeInTooltip
  168. {...zoomRenderProps}
  169. />
  170. )}
  171. </ChartZoom>
  172. </ProfilesChartContainer>
  173. );
  174. }
  175. const ProfilesChartTitle = styled('div')`
  176. font-size: ${p => p.theme.fontSizeSmall};
  177. color: ${p => p.theme.textColor};
  178. font-weight: 600;
  179. padding: ${space(0.25)} ${space(1)};
  180. `;
  181. const ProfilesChartContainer = styled('div')`
  182. background-color: ${p => p.theme.background};
  183. border-bottom: 1px solid ${p => p.theme.border};
  184. `;