openreplay/frontend/app/components/Session_/Performance/Performance.tsx
2022-12-30 17:59:56 +01:00

496 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React from 'react';
import { connect } from 'react-redux';
import { PlayerContext } from 'App/components/Session/playerContext';
import { observer } from 'mobx-react-lite';
import {
AreaChart,
Area,
ComposedChart,
Line,
XAxis,
YAxis,
Tooltip,
ResponsiveContainer,
ReferenceLine,
Label,
} from 'recharts';
import { durationFromMsFormatted } from 'App/date';
import { formatBytes } from 'App/utils';
import stl from './performance.module.css';
import BottomBlock from '../BottomBlock';
import InfoLine from '../BottomBlock/InfoLine';
const CPU_VISUAL_OFFSET = 10;
const FPS_COLOR = '#C5E5E7';
const FPS_STROKE_COLOR = '#92C7CA';
const FPS_LOW_COLOR = 'pink';
const FPS_VERY_LOW_COLOR = 'red';
const CPU_COLOR = '#A8D1DE';
const CPU_STROKE_COLOR = '#69A5B8';
const USED_HEAP_COLOR = '#A9ABDC';
const USED_HEAP_STROKE_COLOR = '#8588CF';
const TOTAL_HEAP_STROKE_COLOR = '#4A4EB7';
const NODES_COUNT_COLOR = '#C6A9DC';
const NODES_COUNT_STROKE_COLOR = '#7360AC';
const HIDDEN_SCREEN_COLOR = '#CCC';
const CURSOR_COLOR = '#394EFF';
const Gradient = ({ color, id }) => (
<linearGradient id={id} x1="-1" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={color} stopOpacity={0.7} />
<stop offset="95%" stopColor={color} stopOpacity={0.2} />
</linearGradient>
);
const TOTAL_HEAP = 'Allocated Heap';
const USED_HEAP = 'JS Heap';
const FPS = 'Framerate';
const CPU = 'CPU Load';
const NODES_COUNT = 'Nodes Сount';
const FPSTooltip = ({ active, payload }) => {
if (!active || !payload || payload.length < 3) {
return null;
}
if (payload[0].value === null) {
return (
<div className={stl.tooltipWrapper} style={{ color: HIDDEN_SCREEN_COLOR }}>
{'Page is not active. User switched the tab or hid the window.'}
</div>
);
}
let style;
if (payload[1].value != null && payload[1].value > 0) {
style = { color: FPS_LOW_COLOR };
}
if (payload[2].value != null && payload[2].value > 0) {
style = { color: FPS_VERY_LOW_COLOR };
}
return (
<div className={stl.tooltipWrapper} style={style}>
<span className="font-medium">{`${FPS}: `}</span>
{Math.trunc(payload[0].value)}
</div>
);
};
const CPUTooltip = ({ active, payload }) => {
if (!active || payload.length < 1 || payload[0].value === null) {
return null;
}
return (
<div className={stl.tooltipWrapper}>
<span className="font-medium">{`${CPU}: `}</span>
{payload[0].value - CPU_VISUAL_OFFSET}
{'%'}
</div>
);
};
const HeapTooltip = ({ active, payload }) => {
if (!active || payload.length < 2) return null;
return (
<div className={stl.tooltipWrapper}>
<p>
<span className="font-medium">{`${TOTAL_HEAP}: `}</span>
{formatBytes(payload[0].value)}
</p>
<p>
<span className="font-medium">{`${USED_HEAP}: `}</span>
{formatBytes(payload[1].value)}
</p>
</div>
);
};
const NodesCountTooltip = ({ active, payload }) => {
if (!active || !payload || payload.length === 0) return null;
return (
<div className={stl.tooltipWrapper}>
<p>
<span className="font-medium">{`${NODES_COUNT}: `}</span>
{payload[0].value}
</p>
</div>
);
};
const TICKS_COUNT = 10;
function generateTicks(data: Array<Timed>): Array<number> {
if (data.length === 0) return [];
const minTime = data[0].time;
const maxTime = data[data.length - 1].time;
const ticks = [];
const tickGap = (maxTime - minTime) / (TICKS_COUNT + 1);
for (let i = 0; i < TICKS_COUNT; i++) {
const tick = tickGap * (i + 1) + minTime;
ticks.push(tick);
}
return ticks;
}
const LOW_FPS = 30;
const VERY_LOW_FPS = 20;
const LOW_FPS_MARKER_VALUE = 5;
const HIDDEN_SCREEN_MARKER_VALUE = 20;
function addFpsMetadata(data) {
return [...data].map((point, i) => {
let fpsVeryLowMarker = null;
let fpsLowMarker = null;
let hiddenScreenMarker = 0;
if (point.fps != null) {
fpsVeryLowMarker = 0;
fpsLowMarker = 0;
if (point.fps < VERY_LOW_FPS) {
fpsVeryLowMarker = LOW_FPS_MARKER_VALUE;
} else if (point.fps < LOW_FPS) {
fpsLowMarker = LOW_FPS_MARKER_VALUE;
}
}
if (
point.fps == null ||
(i > 0 && data[i - 1].fps == null) //||
//(i < data.length-1 && data[i + 1].fps == null)
) {
hiddenScreenMarker = HIDDEN_SCREEN_MARKER_VALUE;
}
if (point.cpu != null) {
point.cpu += CPU_VISUAL_OFFSET;
}
return {
...point,
fpsLowMarker,
fpsVeryLowMarker,
hiddenScreenMarker,
};
});
}
function Performance({
userDeviceHeapSize,
}: {
userDeviceHeapSize: number;
}) {
const { player, store } = React.useContext(PlayerContext);
const [_timeTicks, setTicks] = React.useState<number[]>([])
const [_data, setData] = React.useState<any[]>([])
const {
performanceChartTime,
performanceChartData,
connType,
connBandwidth,
performanceAvaliability: avaliability,
} = store.get();
React.useState(() => {
setTicks(generateTicks(performanceChartData));
setData(addFpsMetadata(performanceChartData));
})
const onDotClick = ({ index: pointer }: { index: number }) => {
const point = _data[pointer];
if (!!point) {
player.jump(point.time);
}
};
const onChartClick = (e: any) => {
if (e === null) return;
const { activeTooltipIndex } = e;
const point = _data[activeTooltipIndex];
if (!!point) {
player.jump(point.time);
}
};
const { fps, cpu, heap, nodes } = avaliability;
const avaliableCount = [fps, cpu, heap, nodes].reduce((c, av) => (av ? c + 1 : c), 0);
const height = avaliableCount === 0 ? '0' : `${100 / avaliableCount}%`;
return (
<BottomBlock>
<BottomBlock.Header>
<div className="flex items-center w-full">
<div className="font-semibold color-gray-medium mr-auto">Performance</div>
<InfoLine>
<InfoLine.Point
label="Device Heap Size"
value={formatBytes(userDeviceHeapSize)}
display={true}
/>
<InfoLine.Point
label="Connection Type"
value={connType}
display={connType != null && connType !== 'unknown'}
/>
<InfoLine.Point
label="Connection Speed"
value={
connBandwidth >= 1000 ? `${connBandwidth / 1000} Mbps` : `${connBandwidth} Kbps`
}
display={connBandwidth != null}
/>
</InfoLine>
</div>
</BottomBlock.Header>
<BottomBlock.Content>
{fps && (
<ResponsiveContainer height={height}>
<AreaChart
onClick={onChartClick}
data={_data}
syncId="s"
margin={{
top: 0,
right: 0,
left: 0,
bottom: 0,
}}
>
<defs>
<Gradient id="fpsGradient" color={FPS_COLOR} />
</defs>
<XAxis
dataKey="time"
type="number"
mirror
orientation="top"
tickLine={false}
tickFormatter={durationFromMsFormatted}
tick={{ fontSize: '12px', fill: '#333' }}
domain={[0, 'dataMax']}
ticks={_timeTicks}
>
<Label value="FPS" position="insideTopRight" className="fill-gray-darkest" />
</XAxis>
<YAxis axisLine={false} tick={false} mirror domain={[0, 85]} />
<Area
dataKey="fps"
type="stepBefore"
stroke={FPS_STROKE_COLOR}
fill="url(#fpsGradient)"
dot={false}
activeDot={{
onClick: onDotClick,
style: { cursor: 'pointer' },
}}
isAnimationActive={false}
/>
<Area
dataKey="fpsLowMarker"
type="stepBefore"
stroke="none"
fill={FPS_LOW_COLOR}
activeDot={false}
isAnimationActive={false}
/>
<Area
dataKey="fpsVeryLowMarker"
type="stepBefore"
stroke="none"
fill={FPS_VERY_LOW_COLOR}
activeDot={false}
isAnimationActive={false}
/>
<Area
dataKey="hiddenScreenMarker"
type="stepBefore"
stroke="none"
fill={HIDDEN_SCREEN_COLOR}
activeDot={false}
isAnimationActive={false}
/>
<ReferenceLine x={performanceChartTime} stroke={CURSOR_COLOR} />
<Tooltip content={FPSTooltip} filterNull={false} />
</AreaChart>
</ResponsiveContainer>
)}
{cpu && (
<ResponsiveContainer height={height}>
<AreaChart
onClick={onChartClick}
data={_data}
syncId="s"
margin={{
top: 0,
right: 0,
left: 0,
bottom: 0,
}}
>
<defs>
<Gradient id="cpuGradient" color={CPU_COLOR} />
</defs>
{/* <CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" /> */}
<XAxis
dataKey="time"
type="number"
mirror
orientation="top"
tickLine={false}
tickFormatter={() => ''}
domain={[0, 'dataMax']}
ticks={_timeTicks}
>
<Label value="CPU" position="insideTopRight" className="fill-gray-darkest" />
</XAxis>
<YAxis axisLine={false} tick={false} mirror domain={[0, 120]} orientation="right" />
<Area
dataKey="cpu"
type="monotone"
stroke={CPU_STROKE_COLOR}
fill="url(#cpuGradient)"
dot={false}
activeDot={{
onClick: onDotClick,
style: { cursor: 'pointer' },
}}
isAnimationActive={false}
/>
<Area
dataKey="hiddenScreenMarker"
type="stepBefore"
stroke="none"
fill={HIDDEN_SCREEN_COLOR}
activeDot={false}
isAnimationActive={false}
/>
<ReferenceLine x={performanceChartTime} stroke={CURSOR_COLOR} />
<Tooltip content={CPUTooltip} filterNull={false} />
</AreaChart>
</ResponsiveContainer>
)}
{heap && (
<ResponsiveContainer height={height}>
<ComposedChart
onClick={onChartClick}
data={_data}
margin={{
top: 0,
right: 0,
left: 0,
bottom: 0,
}}
syncId="s"
>
<defs>
<Gradient id="usedHeapGradient" color={USED_HEAP_COLOR} />
</defs>
<XAxis
dataKey="time"
type="number"
mirror
orientation="top"
tickLine={false}
tickFormatter={() => ''} // tick={false} + _timeTicks to cartesian array
domain={[0, 'dataMax']}
ticks={_timeTicks}
>
<Label value="HEAP" position="insideTopRight" className="fill-gray-darkest" />
</XAxis>
<YAxis
axisLine={false}
tickFormatter={formatBytes}
mirror
// Hack to keep only end tick
minTickGap={Number.MAX_SAFE_INTEGER}
domain={[0, (max: number) => max * 1.2]}
/>
<Line
type="monotone"
dataKey="totalHeap"
stroke={TOTAL_HEAP_STROKE_COLOR}
dot={false}
activeDot={{
onClick: onDotClick,
style: { cursor: 'pointer' },
}}
isAnimationActive={false}
/>
<Area
dataKey="usedHeap"
type="monotone"
fill="url(#usedHeapGradient)"
stroke={USED_HEAP_STROKE_COLOR}
dot={false}
activeDot={{
onClick: onDotClick,
style: { cursor: 'pointer' },
}}
isAnimationActive={false}
/>
<ReferenceLine x={performanceChartTime} stroke={CURSOR_COLOR} />
<Tooltip content={HeapTooltip} filterNull={false} />
</ComposedChart>
</ResponsiveContainer>
)}
{nodes && (
<ResponsiveContainer height={height}>
<AreaChart
onClick={onChartClick}
data={_data}
syncId="s"
margin={{
top: 0,
right: 0,
left: 0,
bottom: 0,
}}
>
<defs>
<Gradient id="nodesGradient" color={NODES_COUNT_COLOR} />
</defs>
<XAxis
dataKey="time"
type="number"
mirror
orientation="top"
tickLine={false}
tickFormatter={() => ''}
domain={[0, 'dataMax']}
ticks={_timeTicks}
>
<Label value="NODES" position="insideTopRight" className="fill-gray-darkest" />
</XAxis>
<YAxis
axisLine={false}
tick={false}
mirror
orientation="right"
domain={[0, (max: number) => max * 1.2]}
/>
<Area
dataKey="nodesCount"
type="monotone"
stroke={NODES_COUNT_STROKE_COLOR}
fill="url(#nodesGradient)"
dot={false}
activeDot={{
onClick: onDotClick,
style: { cursor: 'pointer' },
}}
isAnimationActive={false}
/>
<ReferenceLine x={performanceChartTime} stroke={CURSOR_COLOR} />
<Tooltip content={NodesCountTooltip} filterNull={false} />
</AreaChart>
</ResponsiveContainer>
)}
</BottomBlock.Content>
</BottomBlock>
);
}
export const ConnectedPerformance = connect((state: any) => ({
userDeviceHeapSize: state.getIn(['sessions', 'current']).userDeviceHeapSize,
}))(observer(Performance));