Skip to content

Commit

Permalink
Merge pull request #2468 from andrewbaldwin44/bugfix/2467
Browse files Browse the repository at this point in the history
Modern UI: Add Time to Chart Tooltips
  • Loading branch information
cyberw authored Nov 16, 2023
2 parents abf3551 + 8d83f51 commit 3949df4
Show file tree
Hide file tree
Showing 9 changed files with 188 additions and 144 deletions.

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion locust/webui/dist/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<meta name="theme-color" content="#000000" />

<title>Locust</title>
<script type="module" crossorigin src="/assets/index-9996beb6.js"></script>
<script type="module" crossorigin src="/assets/index-64e96d17.js"></script>
</head>
<body>
<div id="root"></div>
Expand Down
65 changes: 41 additions & 24 deletions locust/webui/src/components/LineChart/LineChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import {
dispose,
ECharts,
EChartsOption,
TooltipComponentFormatterCallbackParams,
DefaultLabelFormatterCallbackParams,
connect,
TooltipComponentOption,
} from 'echarts';

import { IUiState } from 'redux/slice/ui.slice';
Expand All @@ -21,25 +22,42 @@ interface ICreateOptions {
title: string;
seriesData: EChartsOption['Series'][];
charts: ICharts;
colors?: string[];
}

export interface ILineChartProps {
title: string;
lines: ILine[];
colors?: string[];
}

interface ILineChart extends ILineChartProps {
title: string;
lines: ILine[];
charts: ICharts;
}

const createOptions = ({ charts, title, seriesData }: ICreateOptions) => ({
const CHART_TEXT_COLOR = '#b3c3bc';
const CHART_AXIS_COLOR = '#5b6f66';
const CHART_COLOR_PALETTE = ['#00ca5a', '#ffca5a'];

registerTheme('locust', {
color: CHART_COLOR_PALETTE,
backgroundColor: '#27272a',
xAxis: { lineColor: '#f00' },
graph: {
color: CHART_COLOR_PALETTE,
},
textStyle: { color: CHART_TEXT_COLOR },
title: {
textStyle: { color: CHART_TEXT_COLOR },
},
});

const createOptions = ({ charts, title, seriesData, colors }: ICreateOptions) => ({
legend: {
icon: 'circle',
inactiveColor: '#b3c3bc',
inactiveColor: CHART_TEXT_COLOR,
textStyle: {
color: '#b3c3bc',
color: CHART_TEXT_COLOR,
},
},
title: {
Expand All @@ -49,15 +67,16 @@ const createOptions = ({ charts, title, seriesData }: ICreateOptions) => ({
},
tooltip: {
trigger: 'axis',
formatter: (params: TooltipComponentFormatterCallbackParams) => {
formatter: (params: TooltipComponentOption) => {
if (
!!params &&
Array.isArray(params) &&
params.length > 0 &&
params.some(param => !!param.value)
) {
return params.reduce(
(tooltipText, { color, seriesName, value }) => `
(tooltipText, { axisValue, color, seriesName, value }, index) => `
${index === 0 ? axisValue : ''}
${tooltipText}
<br>
<span style="color:${color};">
Expand All @@ -74,7 +93,7 @@ const createOptions = ({ charts, title, seriesData }: ICreateOptions) => ({
animation: true,
},
textStyle: {
color: '#b3c3bc',
color: CHART_TEXT_COLOR,
fontSize: 13,
},
backgroundColor: 'rgba(21,35,28, 0.93)',
Expand All @@ -88,7 +107,7 @@ const createOptions = ({ charts, title, seriesData }: ICreateOptions) => ({
},
axisLine: {
lineStyle: {
color: '#5b6f66',
color: CHART_AXIS_COLOR,
},
},
data: charts.time,
Expand All @@ -101,13 +120,13 @@ const createOptions = ({ charts, title, seriesData }: ICreateOptions) => ({
},
axisLine: {
lineStyle: {
color: '#5b6f66',
color: CHART_AXIS_COLOR,
},
},
},
series: seriesData,
grid: { x: 60, y: 70, x2: 40, y2: 40 },
color: ['#00ca5a', '#ff6d6d'],
color: colors,
toolbox: {
feature: {
saveAsImage: {
Expand Down Expand Up @@ -136,20 +155,11 @@ const createMarkLine = (charts: ICharts) => ({
label: {
formatter: (params: DefaultLabelFormatterCallbackParams) => `Run #${params.dataIndex + 1}`,
},
lineStyle: { color: '#5b6f66' },
lineStyle: { color: CHART_AXIS_COLOR },
data: (charts.markers || []).map((timeMarker: string) => ({ xAxis: timeMarker })),
});

registerTheme('locust', {
backgroundColor: '#27272a',
xAxis: { lineColor: '#f00' },
textStyle: { color: '#b3c3bc' },
title: {
textStyle: { color: '#b3c3bc' },
},
});

export default function LineChart({ charts, title, lines }: ILineChart) {
export default function LineChart({ charts, title, lines, colors }: ILineChart) {
const [chart, setChart] = useState<ECharts | null>(null);

const chartContainer = useRef<HTMLDivElement | null>(null);
Expand All @@ -161,13 +171,20 @@ export default function LineChart({ charts, title, lines }: ILineChart) {

const initChart = init(chartContainer.current, 'locust');
initChart.setOption(
createOptions({ charts, title, seriesData: getSeriesData({ charts, lines }) }),
createOptions({ charts, title, seriesData: getSeriesData({ charts, lines }), colors }),
);

const handleChartResize = () => initChart.resize();
window.addEventListener('resize', handleChartResize);

initChart.group = 'swarmCharts';
connect('swarmCharts');

setChart(initChart);

return () => {
dispose(initChart);
window.removeEventListener('resize', handleChartResize);
};
}, [chartContainer]);

Expand Down
1 change: 1 addition & 0 deletions locust/webui/src/components/SwarmCharts/SwarmCharts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const availableSwarmCharts: ILineChartProps[] = [
{ name: 'RPS', key: 'currentRps' },
{ name: 'Failures/s', key: 'currentFailPerSec' },
],
colors: ['#00ca5a', '#ff6d6d'],
},
{
title: 'Response Times (ms)',
Expand Down
11 changes: 8 additions & 3 deletions locust/webui/src/components/Table/Table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,16 @@ export interface ITableRowProps {

export interface ITableRowContent {
content: string | number;
formatter?: (content: string | number) => string;
round?: number;
markdown?: boolean;
}

function TableRowContent({ content, round, markdown }: ITableRowContent) {
function TableRowContent({ content, formatter, round, markdown }: ITableRowContent) {
if (formatter) {
return formatter(content);
}

if (round) {
return roundToDecimalPlaces(content as number, round);
}
Expand Down Expand Up @@ -73,9 +78,9 @@ export default function Table<Row extends Record<string, any> = Record<string, s
<TableBody>
{rows.map((row, index) => (
<TableRow key={`${row.name}-${index}`}>
{structure.map(({ key, round, markdown }, index) => (
{structure.map(({ key, ...tableRowProps }, index) => (
<TableCell key={`table-row=${index}`}>
<TableRowContent content={row[key]} markdown={markdown} round={round} />
<TableRowContent content={row[key]} {...tableRowProps} />
</TableCell>
))}
</TableRow>
Expand Down
3 changes: 2 additions & 1 deletion locust/webui/src/components/WorkersTable/WorkersTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import Table from 'components/Table/Table';
import useSortByField from 'hooks/useSortByField';
import { IRootState } from 'redux/store';
import { ISwarmWorker } from 'types/ui.types';
import { formatBytes } from 'utils/string';

const tableStructure = [
{ key: 'id', title: 'Worker' },
{ key: 'state', title: 'State' },
{ key: 'userCount', title: '# users' },
{ key: 'cpuUsage', title: 'CPU usage' },
{ key: 'memoryUsage', title: 'Memory usage' },
{ key: 'memoryUsage', title: 'Memory usage', formatter: formatBytes },
];

function WorkersTable({ workers = [] }: { workers?: ISwarmWorker[] }) {
Expand Down
110 changes: 60 additions & 50 deletions locust/webui/src/hooks/useSwarmUi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export default function useSwarmUi() {
const updateChartMarkers = useAction(uiActions.updateChartMarkers);
const swarm = useSelector(({ swarm }) => swarm);
const previousSwarmState = useRef(swarm.state);
const hasSetInitStats = useRef(false);
const [shouldAddMarker, setShouldAddMarker] = useState(false);

const { data: statsData, refetch: refetchStats } = useGetStatsQuery();
Expand All @@ -26,6 +27,54 @@ export default function useSwarmUi() {
const shouldRunRefetchInterval =
swarm.state === SWARM_STATE.SPAWNING || swarm.state == SWARM_STATE.RUNNING;

const updateStatsUi = () => {
if (!statsData) {
return;
}

const {
extendedStats,
stats,
errors,
totalRps,
failRatio,
workers,
currentResponseTimePercentile1,
currentResponseTimePercentile2,
userCount,
} = statsData;

const time = new Date().toLocaleTimeString();

if (shouldAddMarker) {
setShouldAddMarker(false);
updateChartMarkers(time);
}

const totalRpsRounded = roundToDecimalPlaces(totalRps, 2);
const toalFailureRounded = roundToDecimalPlaces(failRatio * 100);

const newChartEntry = {
currentRps: totalRpsRounded,
currentFailPerSec: failRatio,
responseTimePercentile1: currentResponseTimePercentile1,
responseTimePercentile2: currentResponseTimePercentile2,
userCount: userCount,
time,
};

setUi({
extendedStats,
stats,
errors,
totalRps: totalRpsRounded,
failRatio: toalFailureRounded,
workers,
userCount,
});
updateCharts(newChartEntry);
};

useEffect(() => {
if (
statsData &&
Expand All @@ -35,59 +84,20 @@ export default function useSwarmUi() {
}
}, [statsData && statsData.state]);

useInterval(
() => {
if (!statsData) {
return;
useEffect(() => {
if (statsData) {
if (!hasSetInitStats.current) {
// handle setting stats on first load
updateStatsUi();
}

const {
extendedStats,
stats,
errors,
totalRps,
failRatio,
workers,
currentResponseTimePercentile1,
currentResponseTimePercentile2,
userCount,
} = statsData;

const time = new Date().toLocaleTimeString();

if (shouldAddMarker) {
setShouldAddMarker(false);
updateChartMarkers(time);
}
hasSetInitStats.current = true;
}
}, [statsData]);

const totalRpsRounded = roundToDecimalPlaces(totalRps, 2);
const toalFailureRounded = roundToDecimalPlaces(failRatio * 100);

const newChartEntry = {
currentRps: totalRpsRounded,
currentFailPerSec: failRatio,
responseTimePercentile1: currentResponseTimePercentile1,
responseTimePercentile2: currentResponseTimePercentile2,
userCount: userCount,
time,
};

setUi({
extendedStats,
stats,
errors,
totalRps: totalRpsRounded,
failRatio: toalFailureRounded,
workers,
userCount,
});
updateCharts(newChartEntry);
},
STATS_REFETCH_INTERVAL,
{
shouldRunInterval: !!statsData && shouldRunRefetchInterval,
},
);
useInterval(updateStatsUi, STATS_REFETCH_INTERVAL, {
shouldRunInterval: !!statsData && shouldRunRefetchInterval,
});

useEffect(() => {
if (tasksData) {
Expand Down
6 changes: 1 addition & 5 deletions locust/webui/src/redux/slice/swarm.slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,6 @@ import { ITableStructure } from 'types/table.types';
import { camelCaseKeys } from 'utils/string';

export interface ISwarmState {
auth: {
email: string;
name: string;
};
availableShapeClasses: string[];
availableUserClasses: string[];
extraOptions: IExtraOptions;
Expand All @@ -26,7 +22,7 @@ export interface ISwarmState {
overrideHostWarning: boolean;
percentile1: number;
percentile2: number;
runTime: number;
runTime?: number;
showUserclassPicker: boolean;
spawnRate: number | null;
state: string;
Expand Down
13 changes: 13 additions & 0 deletions locust/webui/src/utils/string.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,16 @@ export const toTitleCase = (string: string) =>

export const queryStringToObject = (queryString: string) =>
Object.fromEntries(new URLSearchParams(queryString).entries());

export const formatBytes = (bytes: number, decimals = 2) => {
if (bytes === 0) return '0 Bytes';
if (bytes === 0) return 'N/A';

const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];

const i = Math.floor(Math.log(bytes) / Math.log(k));

return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
};

0 comments on commit 3949df4

Please sign in to comment.