Files
2024-02-15 17:01:50 +00:00

322 lines
8.9 KiB
TypeScript

import {forwardRef, memo, useEffect, useRef} from 'react';
import {useTheme} from '@emotion/react';
import styled from '@emotion/styled';
import moment from 'moment';
import type {AreaChartProps} from 'sentry/components/charts/areaChart';
import {AreaChart} from 'sentry/components/charts/areaChart';
import Grid from 'sentry/components/charts/components/grid';
import {ChartTooltip} from 'sentry/components/charts/components/tooltip';
import XAxis from 'sentry/components/charts/components/xAxis';
import YAxis from 'sentry/components/charts/components/yAxis';
import EmptyMessage from 'sentry/components/emptyMessage';
import Placeholder from 'sentry/components/placeholder';
import {useReplayContext} from 'sentry/components/replays/replayContext';
import {showPlayerTime} from 'sentry/components/replays/utils';
import {t} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import type {ReactEchartsRef, Series} from 'sentry/types/echarts';
import {formatBytesBase2} from 'sentry/utils';
import {getFormattedDate} from 'sentry/utils/dates';
import type {MemoryFrame} from 'sentry/utils/replays/types';
import FluidHeight from 'sentry/views/replays/detail/layout/fluidHeight';
interface Props {
memoryFrames: undefined | MemoryFrame[];
setCurrentHoverTime: (time: undefined | number) => void;
setCurrentTime: (time: number) => void;
startTimestampMs: undefined | number;
}
interface MemoryChartProps extends Props {
forwardedRef: React.Ref<ReactEchartsRef>;
}
const formatTimestamp = timestamp =>
getFormattedDate(timestamp, 'MMM D, YYYY hh:mm:ss A z', {local: false});
const formatTimestampTrim = timestamp =>
getFormattedDate(timestamp, 'MMM D hh:mm', {local: false});
function MemoryChart({
forwardedRef,
memoryFrames,
startTimestampMs = 0,
setCurrentTime,
setCurrentHoverTime,
}: MemoryChartProps) {
const theme = useTheme();
if (!memoryFrames) {
return (
<MemoryChartWrapper>
<Placeholder height="100%" />
</MemoryChartWrapper>
);
}
if (!memoryFrames.length) {
return (
<MemoryChartWrapper>
<EmptyMessage
data-test-id="replay-details-memory-tab"
title={t('No memory metrics found')}
description={t(
'Memory metrics are only captured within Chromium based browser sessions.'
)}
/>
</MemoryChartWrapper>
);
}
const chartOptions: Omit<AreaChartProps, 'series'> = {
grid: Grid({
// makes space for the title
top: '40px',
left: space(1),
right: space(1),
}),
tooltip: ChartTooltip({
appendToBody: true,
trigger: 'axis',
renderMode: 'html',
chartId: 'replay-memory-chart',
formatter: values => {
const seriesTooltips = values.map(
value => `
<div>
<span className="tooltip-label">${value.marker}<strong>${value.seriesName}</strong></span>
${formatBytesBase2(value.data[1])}
</div>
`
);
// showPlayerTime expects a timestamp so we take the captured time in seconds and convert it to a UTC timestamp
const template = [
'<div class="tooltip-series">',
...seriesTooltips,
'</div>',
`<div class="tooltip-footer" style="display: inline-block; width: max-content;">${t(
'Span Time'
)}:
${formatTimestamp(values[0].axisValue)}
</div>`,
`<div class="tooltip-footer" style="border: none;">${'Relative Time'}:
${showPlayerTime(
moment(values[0].axisValue).toDate().toUTCString(),
startTimestampMs
)}
</div>`,
'<div class="tooltip-arrow"></div>',
].join('');
return template;
},
}),
xAxis: XAxis({
type: 'time',
axisLabel: {
formatter: formatTimestampTrim,
},
theme,
}),
yAxis: YAxis({
type: 'value',
name: t('Heap Size'),
theme,
nameTextStyle: {
padding: [8, 8, 8, -25],
fontSize: theme.fontSizeLarge,
fontWeight: 600,
lineHeight: 1.2,
fontFamily: theme.text.family,
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] - startTimestampMs);
}
},
onMouseOut: () => {
setCurrentHoverTime(undefined);
},
onClick: ({data}) => {
if (data.value) {
setCurrentTime(data.value - startTimestampMs);
}
},
};
const series: Series[] = [
{
seriesName: t('Used Heap Memory'),
data: memoryFrames.map(frame => ({
value: frame.data.memory.usedJSHeapSize,
name: frame.endTimestampMs,
})),
stack: 'heap-memory',
lineStyle: {
opacity: 0.75,
width: 1,
},
},
{
seriesName: t('Free Heap Memory'),
data: memoryFrames.map(frame => ({
value: frame.data.memory.totalJSHeapSize - frame.data.memory.usedJSHeapSize,
name: frame.endTimestampMs,
})),
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 (
<MemoryChartWrapper id="replay-memory-chart">
<AreaChart forwardedRef={forwardedRef} series={series} {...chartOptions} />
</MemoryChartWrapper>
);
}
const MemoryChartWrapper = styled(FluidHeight)`
border: 1px solid ${p => p.theme.border};
border-radius: ${space(0.5)};
justify-content: center;
padding: ${space(1)};
`;
const MemoizedMemoryChart = memo(
forwardRef<ReactEchartsRef, Props>((props, ref) => (
<MemoryChart forwardedRef={ref} {...props} />
))
);
/**
* 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 `<AreaChart>`) to re-render as
* infrequently as possible, so we use React.memo and only pass in props that
* are not frequently updated.
*/
function MemoryChartContainer() {
const {currentTime, currentHoverTime, replay, setCurrentTime, setCurrentHoverTime} =
useReplayContext();
const chart = useRef<ReactEchartsRef>(null);
const theme = useTheme();
const memoryFrames = replay?.getMemoryFrames();
const startTimestampMs = replay?.getReplay()?.started_at?.getTime() ?? 0;
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 (
<MemoizedMemoryChart
ref={chart}
memoryFrames={memoryFrames}
setCurrentHoverTime={setCurrentHoverTime}
setCurrentTime={setCurrentTime}
startTimestampMs={startTimestampMs}
/>
);
}
export default MemoryChartContainer;