profileCharts.tsx 5.9 KB

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