memoryChart.tsx 8.1 KB

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