memoryChart.tsx 8.0 KB

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