import {forwardRef, memo, useEffect, useRef} from 'react'; import {useTheme} from '@emotion/react'; import styled from '@emotion/styled'; import moment from 'moment'; import {AreaChart, AreaChartProps} from 'sentry/components/charts/areaChart'; import Grid from 'sentry/components/charts/components/grid'; import Tooltip from 'sentry/components/charts/components/tooltip'; import XAxis from 'sentry/components/charts/components/xAxis'; import YAxis from 'sentry/components/charts/components/yAxis'; import EmptyStateWarning from 'sentry/components/emptyStateWarning'; import {showPlayerTime} from 'sentry/components/replays/utils'; import {t} from 'sentry/locale'; import space from 'sentry/styles/space'; import {ReactEchartsRef, Series} from 'sentry/types/echarts'; import {formatBytesBase2} from 'sentry/utils'; import {getFormattedDate} from 'sentry/utils/dates'; import type {MemorySpanType} from 'sentry/views/replays/types'; interface Props { memorySpans: MemorySpanType[]; setCurrentHoverTime: (time: undefined | number) => void; setCurrentTime: (time: number) => void; startTimestampMs: number | undefined; } interface MemoryChartProps extends Props { forwardedRef: React.Ref; } const formatTimestamp = timestamp => getFormattedDate(timestamp * 1000, 'MMM D, YYYY hh:mm:ss A z', {local: false}); function MemoryChart({ forwardedRef, memorySpans, startTimestampMs = 0, setCurrentTime, setCurrentHoverTime, }: MemoryChartProps) { const theme = useTheme(); if (memorySpans.length <= 0) { return ( {t('No memory metrics found')} ); } const chartOptions: Omit = { grid: Grid({ // makes space for the title top: '40px', left: space(1), right: space(1), }), tooltip: Tooltip({ appendToBody: true, trigger: 'axis', renderMode: 'html', chartId: 'replay-memory-chart', formatter: values => { const seriesTooltips = values.map( value => `
${value.marker}${ value.seriesName } ${formatBytesBase2(value.data[1])}
` ); // showPlayerTime expects a timestamp so we take the captured time in seconds and convert it to a UTC timestamp const template = [ '
', ...seriesTooltips, '
', `
${t( 'Span Time' )}: ${formatTimestamp(values[0].axisValue)}
`, `
${'Relative Time'}: ${showPlayerTime( moment(values[0].axisValue * 1000) .toDate() .toUTCString(), startTimestampMs )}
`, '
', ].join(''); return template; }, }), xAxis: XAxis({ type: 'time', axisLabel: { formatter: formatTimestamp, }, theme, }), yAxis: YAxis({ type: 'value', name: t('Heap Size'), theme, nameTextStyle: { padding: 8, fontSize: theme.fontSizeLarge, fontWeight: 600, lineHeight: 1.2, color: theme.gray300, }, // input is in bytes, minInterval is a megabyte minInterval: 1024 * 1024, // maxInterval is a terabyte maxInterval: Math.pow(1024, 4), // format the axis labels to be whole number values axisLabel: { formatter: value => formatBytesBase2(value, 0), }, }), // XXX: For area charts, mouse events *only* occurs when interacting with // the "line" of the area chart. Mouse events do not fire when interacting // with the "area" under the line. onMouseOver: ({data}) => { if (data[0]) { setCurrentHoverTime(data[0] * 1000 - startTimestampMs); } }, onMouseOut: () => { setCurrentHoverTime(undefined); }, onClick: ({data}) => { if (data.value) { setCurrentTime(data.value * 1000 - startTimestampMs); } }, }; const series: Series[] = [ { seriesName: t('Used Heap Memory'), data: memorySpans.map(span => ({ value: span.data.memory.usedJSHeapSize, name: span.endTimestamp, })), stack: 'heap-memory', lineStyle: { opacity: 0.75, width: 1, }, }, { seriesName: t('Free Heap Memory'), data: memorySpans.map(span => ({ value: span.data.memory.totalJSHeapSize - span.data.memory.usedJSHeapSize, name: span.endTimestamp, })), stack: 'heap-memory', lineStyle: { opacity: 0.75, width: 1, }, }, // Inserting this here so we can update in Container { id: 'currentTime', seriesName: t('Current player time'), data: [], markLine: { symbol: ['', ''], data: [], label: { show: false, }, lineStyle: { type: 'solid' as const, color: theme.purple300, width: 2, }, }, }, { id: 'hoverTime', seriesName: t('Hover player time'), data: [], markLine: { symbol: ['', ''], data: [], label: { show: false, }, lineStyle: { type: 'solid' as const, color: theme.purple200, width: 2, }, }, }, ]; return ( ); } const MemoryChartWrapper = styled('div')` margin-top: ${space(2)}; margin-bottom: ${space(3)}; border-radius: ${space(0.5)}; border: 1px solid ${p => p.theme.border}; `; const MemoizedMemoryChart = memo( forwardRef((props, ref) => ( )) ); interface MemoryChartContainerProps extends Props { currentHoverTime: number | undefined; currentTime: number; } /** * This container is used to update echarts outside of React. `currentTime` is * the current time of the player -- if replay is currently playing, this will be * updated quite frequently causing the chart to constantly re-render. The * re-renders will conflict with mouse interactions (e.g. hovers and * tooltips). * * We need `MemoryChart` (which wraps an ``) to re-render as * infrequently as possible, so we use React.memo and only pass in props that * are not frequently updated. * */ function MemoryChartContainer({ currentTime, currentHoverTime, startTimestampMs = 0, ...props }: MemoryChartContainerProps) { const chart = useRef(null); const theme = useTheme(); useEffect(() => { if (!chart.current) { return; } const echarts = chart.current.getEchartsInstance(); echarts.setOption({ series: [ { id: 'currentTime', markLine: { data: [ { xAxis: currentTime + startTimestampMs, }, ], }, }, ], }); }, [currentTime, startTimestampMs, theme]); useEffect(() => { if (!chart.current) { return; } const echarts = chart.current.getEchartsInstance(); echarts.setOption({ series: [ { id: 'hoverTime', markLine: { data: [ ...(currentHoverTime ? [ { xAxis: currentHoverTime + startTimestampMs, }, ] : []), ], }, }, ], }); }, [currentHoverTime, startTimestampMs, theme]); return ( ); } export default MemoryChartContainer;