memoryChart.tsx 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  1. import {forwardRef, memo, useEffect, useRef} from 'react';
  2. import {useTheme} from '@emotion/react';
  3. import styled from '@emotion/styled';
  4. import moment from 'moment';
  5. import {AreaChart, AreaChartProps} from 'sentry/components/charts/areaChart';
  6. import Grid from 'sentry/components/charts/components/grid';
  7. import Tooltip from 'sentry/components/charts/components/tooltip';
  8. import XAxis from 'sentry/components/charts/components/xAxis';
  9. import YAxis from 'sentry/components/charts/components/yAxis';
  10. import EmptyStateWarning from 'sentry/components/emptyStateWarning';
  11. import {showPlayerTime} from 'sentry/components/replays/utils';
  12. import {t} from 'sentry/locale';
  13. import space from 'sentry/styles/space';
  14. import {ReactEchartsRef, Series} from 'sentry/types/echarts';
  15. import {formatBytesBase2} from 'sentry/utils';
  16. import {getFormattedDate} from 'sentry/utils/dates';
  17. import type {MemorySpanType} from 'sentry/views/replays/types';
  18. interface Props {
  19. memorySpans: MemorySpanType[];
  20. setCurrentHoverTime: (time: undefined | number) => void;
  21. setCurrentTime: (time: number) => void;
  22. startTimestampMs: number | undefined;
  23. }
  24. interface MemoryChartProps extends Props {
  25. forwardedRef: React.Ref<ReactEchartsRef>;
  26. }
  27. const formatTimestamp = timestamp =>
  28. getFormattedDate(timestamp * 1000, 'MMM D, YYYY hh:mm:ss A z', {local: false});
  29. function MemoryChart({
  30. forwardedRef,
  31. memorySpans,
  32. startTimestampMs = 0,
  33. setCurrentTime,
  34. setCurrentHoverTime,
  35. }: MemoryChartProps) {
  36. const theme = useTheme();
  37. if (memorySpans.length <= 0) {
  38. return (
  39. <EmptyStateWarning withIcon={false} small>
  40. {t('No memory metrics found')}
  41. </EmptyStateWarning>
  42. );
  43. }
  44. const chartOptions: Omit<AreaChartProps, 'series'> = {
  45. grid: Grid({
  46. // makes space for the title
  47. top: '40px',
  48. left: space(1),
  49. right: space(1),
  50. }),
  51. tooltip: Tooltip({
  52. trigger: 'axis',
  53. formatter: values => {
  54. const seriesTooltips = values.map(
  55. value => `
  56. <div>
  57. <span className="tooltip-label">${value.marker}<strong>${
  58. value.seriesName
  59. }</strong></span>
  60. ${formatBytesBase2(value.data[1])}
  61. </div>
  62. `
  63. );
  64. // showPlayerTime expects a timestamp so we take the captured time in seconds and convert it to a UTC timestamp
  65. const template = [
  66. '<div class="tooltip-series">',
  67. ...seriesTooltips,
  68. '</div>',
  69. `<div class="tooltip-date" style="display: inline-block; width: max-content;">${t(
  70. 'Span Time'
  71. )}:
  72. ${formatTimestamp(values[0].axisValue)}
  73. </div>`,
  74. `<div class="tooltip-date" style="border: none;">${'Relative Time'}:
  75. ${showPlayerTime(
  76. moment(values[0].axisValue * 1000)
  77. .toDate()
  78. .toUTCString(),
  79. startTimestampMs
  80. )}
  81. </div>`,
  82. '<div class="tooltip-arrow"></div>',
  83. ].join('');
  84. return template;
  85. },
  86. }),
  87. xAxis: XAxis({
  88. type: 'time',
  89. axisLabel: {
  90. formatter: formatTimestamp,
  91. },
  92. theme,
  93. }),
  94. yAxis: YAxis({
  95. type: 'value',
  96. name: t('Heap Size'),
  97. theme,
  98. nameTextStyle: {
  99. padding: 8,
  100. fontSize: theme.fontSizeLarge,
  101. fontWeight: 600,
  102. lineHeight: 1.2,
  103. color: theme.gray300,
  104. },
  105. // input is in bytes, minInterval is a megabyte
  106. minInterval: 1024 * 1024,
  107. // maxInterval is a terabyte
  108. maxInterval: Math.pow(1024, 4),
  109. // format the axis labels to be whole number values
  110. axisLabel: {
  111. formatter: value => formatBytesBase2(value, 0),
  112. },
  113. }),
  114. // XXX: For area charts, mouse events *only* occurs when interacting with
  115. // the "line" of the area chart. Mouse events do not fire when interacting
  116. // with the "area" under the line.
  117. onMouseOver: ({data}) => {
  118. if (data[0]) {
  119. setCurrentHoverTime(data[0] * 1000 - startTimestampMs);
  120. }
  121. },
  122. onMouseOut: () => {
  123. setCurrentHoverTime(undefined);
  124. },
  125. onClick: ({data}) => {
  126. if (data.value) {
  127. setCurrentTime(data.value * 1000 - startTimestampMs);
  128. }
  129. },
  130. };
  131. const series: Series[] = [
  132. {
  133. seriesName: t('Used Heap Memory'),
  134. data: memorySpans.map(span => ({
  135. value: span.data.memory.usedJSHeapSize,
  136. name: span.endTimestamp,
  137. })),
  138. stack: 'heap-memory',
  139. lineStyle: {
  140. opacity: 0.75,
  141. width: 1,
  142. },
  143. },
  144. {
  145. seriesName: t('Free Heap Memory'),
  146. data: memorySpans.map(span => ({
  147. value: span.data.memory.totalJSHeapSize - span.data.memory.usedJSHeapSize,
  148. name: span.endTimestamp,
  149. })),
  150. stack: 'heap-memory',
  151. lineStyle: {
  152. opacity: 0.75,
  153. width: 1,
  154. },
  155. },
  156. // Inserting this here so we can update in Container
  157. {
  158. id: 'currentTime',
  159. seriesName: t('Current player time'),
  160. data: [],
  161. markLine: {
  162. symbol: ['', ''],
  163. data: [],
  164. label: {
  165. show: false,
  166. },
  167. lineStyle: {
  168. type: 'solid' as const,
  169. color: theme.purple300,
  170. width: 2,
  171. },
  172. },
  173. },
  174. {
  175. id: 'hoverTime',
  176. seriesName: t('Hover player time'),
  177. data: [],
  178. markLine: {
  179. symbol: ['', ''],
  180. data: [],
  181. label: {
  182. show: false,
  183. },
  184. lineStyle: {
  185. type: 'solid' as const,
  186. color: theme.purple200,
  187. width: 2,
  188. },
  189. },
  190. },
  191. ];
  192. return (
  193. <MemoryChartWrapper>
  194. <AreaChart forwardedRef={forwardedRef} series={series} {...chartOptions} />
  195. </MemoryChartWrapper>
  196. );
  197. }
  198. const MemoryChartWrapper = styled('div')`
  199. margin-top: ${space(2)};
  200. margin-bottom: ${space(3)};
  201. border-radius: ${space(0.5)};
  202. border: 1px solid ${p => p.theme.border};
  203. `;
  204. const MemoizedMemoryChart = memo(
  205. forwardRef<ReactEchartsRef, Props>((props, ref) => (
  206. <MemoryChart forwardedRef={ref} {...props} />
  207. ))
  208. );
  209. interface MemoryChartContainerProps extends Props {
  210. currentHoverTime: number | undefined;
  211. currentTime: number;
  212. }
  213. /**
  214. * This container is used to update echarts outside of React. `currentTime` is
  215. * the current time of the player -- if replay is currently playing, this will be
  216. * updated quite frequently causing the chart to constantly re-render. The
  217. * re-renders will conflict with mouse interactions (e.g. hovers and
  218. * tooltips).
  219. *
  220. * We need `MemoryChart` (which wraps an `<AreaChart>`) to re-render as
  221. * infrequently as possible, so we use React.memo and only pass in props that
  222. * are not frequently updated.
  223. * */
  224. function MemoryChartContainer({
  225. currentTime,
  226. currentHoverTime,
  227. startTimestampMs = 0,
  228. ...props
  229. }: MemoryChartContainerProps) {
  230. const chart = useRef<ReactEchartsRef>(null);
  231. const theme = useTheme();
  232. useEffect(() => {
  233. if (!chart.current) {
  234. return;
  235. }
  236. const echarts = chart.current.getEchartsInstance();
  237. echarts.setOption({
  238. series: [
  239. {
  240. id: 'currentTime',
  241. markLine: {
  242. data: [
  243. {
  244. xAxis: currentTime + startTimestampMs,
  245. },
  246. ],
  247. },
  248. },
  249. ],
  250. });
  251. }, [currentTime, startTimestampMs, theme]);
  252. useEffect(() => {
  253. if (!chart.current) {
  254. return;
  255. }
  256. const echarts = chart.current.getEchartsInstance();
  257. echarts.setOption({
  258. series: [
  259. {
  260. id: 'hoverTime',
  261. markLine: {
  262. data: [
  263. ...(currentHoverTime
  264. ? [
  265. {
  266. xAxis: currentHoverTime + startTimestampMs,
  267. },
  268. ]
  269. : []),
  270. ],
  271. },
  272. },
  273. ],
  274. });
  275. }, [currentHoverTime, startTimestampMs, theme]);
  276. return (
  277. <MemoizedMemoryChart ref={chart} startTimestampMs={startTimestampMs} {...props} />
  278. );
  279. }
  280. export default MemoryChartContainer;