Skip to content

Commit

Permalink
Merge pull request #2509 from andrewbaldwin44/feature/swarm-chart-per…
Browse files Browse the repository at this point in the history
…centiles

Chart Average Response Time in Modern UI
  • Loading branch information
cyberw authored Dec 8, 2023
2 parents f2cd9ab + c9a4b85 commit 1d55093
Show file tree
Hide file tree
Showing 16 changed files with 190 additions and 127 deletions.
3 changes: 1 addition & 2 deletions locust/html.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,7 @@ def get_html_report(
"show_download_link": show_download_link,
"locustfile": escape(str(environment.locustfile)),
"tasks": task_data,
"percentile1": stats_module.PERCENTILES_TO_CHART[0],
"percentile2": stats_module.PERCENTILES_TO_CHART[1],
"percentiles_to_chart": stats_module.MODERN_UI_PERCENTILES_TO_CHART,
},
theme=theme,
static_js="\n".join(static_js),
Expand Down
9 changes: 9 additions & 0 deletions locust/stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ def resize_handler(signum: int, frame: Optional[FrameType]):
PERCENTILES_TO_REPORT = [0.50, 0.66, 0.75, 0.80, 0.90, 0.95, 0.98, 0.99, 0.999, 0.9999, 1.0]

PERCENTILES_TO_CHART = [0.50, 0.95]
MODERN_UI_PERCENTILES_TO_CHART = [0.95]


class RequestStatsAdditionError(Exception):
Expand Down Expand Up @@ -917,6 +918,12 @@ def stats_history(runner: "Runner") -> None:
if not stats.total.use_response_times_cache:
break
if runner.state != "stopped":
current_response_time_percentiles = {
f"response_time_percentile_{percentile}": stats.total.get_current_response_time_percentile(percentile)
or 0
for percentile in MODERN_UI_PERCENTILES_TO_CHART
}

r = {
"time": datetime.datetime.now(tz=datetime.timezone.utc).strftime("%H:%M:%S"),
"current_rps": stats.total.current_rps or 0,
Expand All @@ -925,6 +932,8 @@ def stats_history(runner: "Runner") -> None:
or 0,
"response_time_percentile_2": stats.total.get_current_response_time_percentile(PERCENTILES_TO_CHART[1])
or 0,
"total_avg_response_time": stats.total.avg_response_time,
"current_response_time_percentiles": current_response_time_percentiles,
"user_count": runner.user_count or 0,
}
stats.history.append(r)
Expand Down
43 changes: 31 additions & 12 deletions locust/web.py
Original file line number Diff line number Diff line change
Expand Up @@ -407,17 +407,27 @@ def request_stats() -> Response:
if stats:
report["total_rps"] = total_stats["current_rps"]
report["total_fail_per_sec"] = total_stats["current_fail_per_sec"]
report["total_avg_response_time"] = total_stats["avg_response_time"]
report["fail_ratio"] = environment.runner.stats.total.fail_ratio
report[
"current_response_time_percentile_1"
] = environment.runner.stats.total.get_current_response_time_percentile(
stats_module.PERCENTILES_TO_CHART[0]
)
report[
"current_response_time_percentile_2"
] = environment.runner.stats.total.get_current_response_time_percentile(
stats_module.PERCENTILES_TO_CHART[1]
)

if self.modern_ui:
report["current_response_time_percentiles"] = {
f"response_time_percentile_{percentile}": environment.runner.stats.total.get_current_response_time_percentile(
percentile
)
for percentile in stats_module.MODERN_UI_PERCENTILES_TO_CHART
}
else:
report[
"current_response_time_percentile_1"
] = environment.runner.stats.total.get_current_response_time_percentile(
stats_module.PERCENTILES_TO_CHART[0]
)
report[
"current_response_time_percentile_2"
] = environment.runner.stats.total.get_current_response_time_percentile(
stats_module.PERCENTILES_TO_CHART[1]
)

if isinstance(environment.runner, MasterRunner):
workers = []
Expand Down Expand Up @@ -580,6 +590,16 @@ def update_template_args(self):
if self.environment.available_shape_classes:
available_shape_classes += sorted(self.environment.available_shape_classes.keys())

if self.modern_ui:
percentiles = {
"percentiles_to_chart": stats_module.MODERN_UI_PERCENTILES_TO_CHART,
}
else:
percentiles = {
"percentile1": stats_module.PERCENTILES_TO_CHART[0],
"percentile2": stats_module.PERCENTILES_TO_CHART[1],
}

self.template_args = {
"locustfile": self.environment.locustfile,
"state": self.environment.runner.state,
Expand All @@ -603,8 +623,7 @@ def update_template_args(self):
"show_userclass_picker": self.userclass_picker_is_active,
"available_user_classes": available_user_classes,
"available_shape_classes": available_shape_classes,
"percentile1": stats_module.PERCENTILES_TO_CHART[0],
"percentile2": stats_module.PERCENTILES_TO_CHART[1],
**percentiles,
}

def _update_shape_class(self, shape_class_name):
Expand Down

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-86565e29.js"></script>
<script type="module" crossorigin src="/assets/index-b3f03a8b.js"></script>
</head>
<body>
<div id="root"></div>
Expand Down
16 changes: 8 additions & 8 deletions locust/webui/src/components/SwarmCharts/SwarmCharts.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import { connect } from 'react-redux';

import LineChart, { ILineChartProps } from 'components/LineChart/LineChart';
import { swarmTemplateArgs } from 'constants/swarm';
import { IRootState } from 'redux/store';
import { ICharts } from 'types/ui.types';

const percentilesToChartLines = swarmTemplateArgs.percentilesToChart.map(percentile => ({
name: `${percentile * 100}th percentile`,
key: `responseTimePercentile${percentile}` as keyof ICharts,
}));

const availableSwarmCharts: ILineChartProps[] = [
{
title: 'Total Requests per Second',
Expand All @@ -16,14 +22,8 @@ const availableSwarmCharts: ILineChartProps[] = [
{
title: 'Response Times (ms)',
lines: [
{
name: `${window.templateArgs.percentile1 * 100}th percentile`,
key: 'responseTimePercentile1',
},
{
name: `${window.templateArgs.percentile2 * 100}th percentile`,
key: 'responseTimePercentile2',
},
...percentilesToChartLines,
{ name: 'Average Response Time', key: 'totalAvgResponseTime' },
],
},
{
Expand Down
4 changes: 4 additions & 0 deletions locust/webui/src/constants/swarm.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { camelCaseKeys } from 'utils/string';

export const SWARM_STATE = {
READY: 'ready',
RUNNING: 'running',
Expand All @@ -7,3 +9,5 @@ export const SWARM_STATE = {
STOPPING: 'stopping',
MISSING: 'missing',
};

export const swarmTemplateArgs = camelCaseKeys(window.templateArgs);
8 changes: 4 additions & 4 deletions locust/webui/src/hooks/useSwarmUi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,16 @@ export default function useSwarmUi() {
}

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

const time = new Date().toLocaleTimeString();
Expand All @@ -57,10 +57,10 @@ export default function useSwarmUi() {
const totalFailureRatioRounded = roundToDecimalPlaces(failRatio * 100);

const newChartEntry = {
...currentResponseTimePercentiles,
currentRps: totalRpsRounded,
currentFailPerSec: totalFailPerSecRounded,
responseTimePercentile1: currentResponseTimePercentile1,
responseTimePercentile2: currentResponseTimePercentile2,
totalAvgResponseTime: roundToDecimalPlaces(totalAvgResponseTime, 2),
userCount: userCount,
time,
};
Expand Down
13 changes: 8 additions & 5 deletions locust/webui/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,23 @@ import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';

import App from 'App';
import { swarmTemplateArgs } from 'constants/swarm';
import { store } from 'redux/store';
import Report from 'Report';
import { IReportTemplateArgs } from 'types/swarm.types';
import { ICharts } from 'types/ui.types';
import { updateArraysAtProps } from 'utils/object';
import { camelCaseKeys } from 'utils/string';

const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);

if ((window.templateArgs as IReportTemplateArgs).is_report) {
const templateArgs = camelCaseKeys(window.templateArgs) as IReportTemplateArgs;
if ((swarmTemplateArgs as IReportTemplateArgs).isReport) {
const reportProps = {
...templateArgs,
charts: templateArgs.history.reduce(updateArraysAtProps, {}) as ICharts,
...(swarmTemplateArgs as IReportTemplateArgs),
charts: swarmTemplateArgs.history.reduce(
(charts, { currentResponseTimePercentiles, ...history }) =>
updateArraysAtProps(charts, { ...currentResponseTimePercentiles, ...history }),
{} as ICharts,
) as ICharts,
};
root.render(<Report {...reportProps} />);
} else {
Expand Down
11 changes: 5 additions & 6 deletions locust/webui/src/redux/slice/swarm.slice.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';

import { swarmTemplateArgs } from 'constants/swarm';
import { updateStateWithPayload } from 'redux/utils';
import { IExtraOptions, History } from 'types/swarm.types';
import { IExtraOptions, IHistory } from 'types/swarm.types';
import { ITab } from 'types/tab.types';
import { ITableStructure } from 'types/table.types';
import { camelCaseKeys } from 'utils/string';

export interface ISwarmState {
availableShapeClasses: string[];
Expand All @@ -13,15 +13,14 @@ export interface ISwarmState {
extendedTabs?: ITab[];
extendedTables?: { key: string; structure: ITableStructure[] }[];
extendedCsvFiles?: { href: string; title: string }[];
history: History[];
history: IHistory[];
host: string;
isDistributed: boolean;
isShape: boolean | null;
locustfile: string;
numUsers: number | null;
overrideHostWarning: boolean;
percentile1: number;
percentile2: number;
percentilesToChart: number[];
runTime?: number;
showUserclassPicker: boolean;
spawnRate: number | null;
Expand All @@ -35,7 +34,7 @@ export interface ISwarmState {

export type SwarmAction = PayloadAction<Partial<ISwarmState>>;

const initialState = camelCaseKeys(window.templateArgs) as ISwarmState;
const initialState = swarmTemplateArgs as ISwarmState;

const swarmSlice = createSlice({
name: 'swarm',
Expand Down
31 changes: 19 additions & 12 deletions locust/webui/src/redux/slice/tests/ui.slice.test.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
import { describe, expect, test } from 'vitest';

import uiSlice, { IUiState, UiAction, uiActions } from 'redux/slice/ui.slice';
import { percentilesToChart } from 'test/mocks/swarmState.mock';
import { ICharts, ISwarmRatios } from 'types/ui.types';

const responseTimePercentileKey1 =
`responseTimePercentile${percentilesToChart[0]}` as `responseTimePercentile${number}`;
const responseTimePercentileKey2 =
`responseTimePercentile${percentilesToChart[1]}` as `responseTimePercentile${number}`;

const initialState = {
totalRps: 0,
failRatio: 0,
stats: [],
errors: [],
exceptions: [],
charts: {
[responseTimePercentileKey1]: [],
[responseTimePercentileKey1]: [],
currentRps: [],
currentFailPerSec: [],
responseTimePercentile1: [],
responseTimePercentile2: [],
totalAvgResponseTime: [],
userCount: [],
time: [],
},
Expand All @@ -39,8 +46,8 @@ describe('uiSlice', () => {
const action = uiActions.updateCharts({
currentRps: 5,
currentFailPerSec: 1,
responseTimePercentile1: 0.4,
responseTimePercentile2: 0.2,
[responseTimePercentileKey1]: 0.4,
[responseTimePercentileKey2]: 0.2,
userCount: 2,
time: '10:10:10',
});
Expand All @@ -50,8 +57,8 @@ describe('uiSlice', () => {

expect(charts.currentRps[0]).toBe(5);
expect(charts.currentFailPerSec[0]).toBe(1);
expect(charts.responseTimePercentile1[0]).toBe(0.4);
expect(charts.responseTimePercentile2[0]).toBe(0.2);
expect(charts[responseTimePercentileKey1][0]).toBe(0.4);
expect(charts[responseTimePercentileKey2][0]).toBe(0.2);
expect(charts.userCount[0]).toBe(2);
expect(charts.time[0]).toBe('10:10:10');
});
Expand All @@ -60,8 +67,8 @@ describe('uiSlice', () => {
const action = uiActions.updateCharts({
currentRps: 5,
currentFailPerSec: 1,
responseTimePercentile1: 0.4,
responseTimePercentile2: 0.2,
[responseTimePercentileKey1]: 0.4,
[responseTimePercentileKey2]: 0.2,
userCount: 2,
time: '10:10:10',
});
Expand All @@ -73,8 +80,8 @@ describe('uiSlice', () => {

expect(charts.currentRps).toEqual([5, 5]);
expect(charts.currentFailPerSec).toEqual([1, 1]);
expect(charts.responseTimePercentile1).toEqual([0.4, 0.4]);
expect(charts.responseTimePercentile2).toEqual([0.2, 0.2]);
expect(charts[responseTimePercentileKey1]).toEqual([0.4, 0.4]);
expect(charts[responseTimePercentileKey2]).toEqual([0.2, 0.2]);
expect(charts.userCount).toEqual([2, 2]);
expect(charts.time).toEqual(['10:10:10', '10:10:10']);
});
Expand All @@ -99,8 +106,8 @@ describe('uiSlice', () => {
// Add space between runs
expect(charts.currentRps[0]).toEqual({ value: null });
expect(charts.currentFailPerSec[0]).toEqual({ value: null });
expect(charts.responseTimePercentile1[0]).toEqual({ value: null });
expect(charts.responseTimePercentile2[0]).toEqual({ value: null });
expect(charts[responseTimePercentileKey1][0]).toEqual({ value: null });
expect(charts[responseTimePercentileKey2][0]).toEqual({ value: null });
expect(charts.userCount[0]).toEqual({ value: null });
});
});
16 changes: 12 additions & 4 deletions locust/webui/src/redux/slice/ui.slice.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';

import { swarmTemplateArgs } from 'constants/swarm';
import { updateStateWithPayload } from 'redux/utils';
import {
ICharts,
Expand All @@ -11,7 +12,6 @@ import {
IExtendedStat,
} from 'types/ui.types';
import { updateArraysAtProps } from 'utils/object';
import { camelCaseKeys } from 'utils/string';

export interface IUiState {
extendedStats?: IExtendedStat[];
Expand All @@ -34,17 +34,25 @@ const initialState = {
stats: [] as ISwarmStat[],
errors: [] as ISwarmError[],
exceptions: [] as ISwarmException[],
charts: camelCaseKeys(window.templateArgs).history.reduce(updateArraysAtProps, {}) as ICharts,
charts: swarmTemplateArgs.history.reduce(updateArraysAtProps, {}) as ICharts,
ratios: {} as ISwarmRatios,
userCount: 0,
};

const percentileNullValues = swarmTemplateArgs.percentilesToChart.reduce(
(percentilesNullValue, percentile) => ({
...percentilesNullValue,
[`responseTimePercentile${percentile}`]: { value: null },
}),
{},
);

const addSpaceToChartsBetweenTests = (charts: ICharts) => {
return updateArraysAtProps(charts, {
...percentileNullValues,
currentRps: { value: null },
currentFailPerSec: { value: null },
responseTimePercentile1: { value: null },
responseTimePercentile2: { value: null },
totalAvgResponseTime: { value: null },
userCount: { value: null },
time: '',
});
Expand Down
Loading

0 comments on commit 1d55093

Please sign in to comment.