|
@@ -1,321 +0,0 @@
|
|
|
-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;
|