Skip to content

Commit

Permalink
Feat: Adds new run-time metrics visualisation to a test (#833)
Browse files Browse the repository at this point in the history
  • Loading branch information
crazyplayy authored Sep 22, 2023
1 parent 5ac67a6 commit 58c653b
Show file tree
Hide file tree
Showing 8 changed files with 261 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Statistic } from "antd";
import React from "react";

const MetricCard = ({ value, title, unit, borderColor }) => (
<Statistic
title={title}
value={value}
suffix={unit}
style={{
border: `1px solid ${borderColor}` || "#d9d9d9",
borderRadius: "10px",
boxShadow: "0px 2px 4px rgba(0, 0, 0, 0.2)",
flex: "1 1 auto",
margin: "8px",
textAlign: "center",
padding: "5px"
}}
/>
);

export default MetricCard;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import MetricCard from "./MetricCard";

export default MetricCard;
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
/* eslint-disable max-statements */
import { Descriptions } from "antd";
import moment from "moment";
import React, { useEffect, useRef, useState } from "react";

import { fetchMetrics } from "../../../../lib/api/endpoints/runMetric";
import { runStatus as runStatusModel } from "../../../../lib/api/models";
import { avg } from "../../../../lib/utils";
import PageSpinner from "../../../layout/PageSpinner";
import MetricCard from "./MetricCard";

const OverviewMetrics = ({ run }) => {
const [runMetrics, setrunMetrics] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [totals, setTotals] = useState({
totalLatencyAvg: 0,
totallatencyP90: 0,
totalSuccessCount: 0,
totalCount: 0,
totalRpm: 0
});
const refreshInterval = 3000;
const timerRef = useRef(null);

const calculateErrorRate = (successCount, totalCount) => {
const errorRate = parseFloat((1 - successCount / totalCount) * 100).toFixed(
2
);

return errorRate;
};

const updaterunMetrics = async (runIdToFetch) => {
setIsLoading(true);

const metricsRes = await fetchMetrics(runIdToFetch, 0, true);

const getRpm = (minDatetime, maxDatetime, totalCount) => {
const dateDuration = moment.duration(maxDatetime.diff(minDatetime));
const diffInSeconds =
dateDuration.asSeconds() > 0 ? dateDuration.asSeconds() : 1;

return parseFloat((totalCount / diffInSeconds) * 60).toFixed(0);
};

const formattedrunMetrics = metricsRes.map(
({
minDatetime,
maxDatetime,
totalCount,
successCount,
label,
latencyAvg,
latencyP90,
responses
}) => {
const errorsCount = totalCount - successCount;
const errorRate = calculateErrorRate(successCount, totalCount);
const rpm = getRpm(minDatetime, maxDatetime, totalCount);

return {
key: label,
errorsCount,
successCount,
errorRate,
label,
rpm,
totalCount,
latencyAvg,
latencyP90,
responses
};
}
);

setrunMetrics(formattedrunMetrics);

setIsLoading(false);
};

const calculateTotals = () => {
const totalLatencyAvg =
runMetrics.length > 0
? runMetrics.reduce((total, metric) => total + metric.latencyAvg, 0)
: 0;
const totallatencyP90 =
runMetrics.length > 0
? runMetrics.reduce((total, metric) => total + metric.latencyP90, 0)
: 0;
const totalSuccessCount =
runMetrics.length > 0
? runMetrics.reduce((total, metric) => total + metric.successCount, 0)
: 0;
const totalCount =
runMetrics.length > 0
? runMetrics.reduce((total, metric) => total + metric.totalCount, 0)
: 0;
const totalRpm =
runMetrics.length > 0
? runMetrics.reduce(
(total, metric) => total + parseFloat(metric.rpm),
0
)
: 0;

setTotals({
totalLatencyAvg,
totallatencyP90,
totalSuccessCount,
totalCount,
totalRpm
});
};

const startMetricsRefreshTimer = () => {
if (run.runStatus === runStatusModel.RUNNING) {
timerRef.current = setInterval(() => {
updaterunMetrics(run.id);
}, refreshInterval);
}
};

const stopMetricsRefreshTimer = () => {
clearInterval(timerRef.current);
};

useEffect(() => {
updaterunMetrics(run.id);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [run]);

useEffect(() => {
calculateTotals();
startMetricsRefreshTimer();
return () => {
stopMetricsRefreshTimer();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [runMetrics, run.runStatus]);

return isLoading ? (
<PageSpinner />
) : (
<div
style={{
border: "1px solid #fff "
}}
>
<Descriptions>
<div
style={{
flexWrap: "wrap"
}}
>
<Descriptions.Item>
<MetricCard
title="Average Latency"
value={avg(totals.totalLatencyAvg, runMetrics.length).toFixed(2)}
unit="ms"
borderColor="#ffd166"
/>
</Descriptions.Item>
<Descriptions.Item>
<MetricCard
title="90% Latency"
value={avg(totals.totallatencyP90, runMetrics.length).toFixed(2)}
unit="ms"
borderColor="#ffab40"
/>
</Descriptions.Item>
<Descriptions.Item>
<MetricCard
title="Errors"
value={calculateErrorRate(
totals.totalSuccessCount,
totals.totalCount
)}
unit="%"
borderColor="#ff6b6b"
/>
</Descriptions.Item>
<Descriptions.Item>
<MetricCard
title="Average RPM"
value={avg(
parseFloat(totals.totalRpm),
runMetrics.length
).toFixed(0)}
unit="req/min"
borderColor="#64b5f6"
/>
</Descriptions.Item>
</div>
</Descriptions>
</div>
);
};

export default OverviewMetrics;
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { render } from "@testing-library/react";
import React from "react";

import OverviewMetrics from "./OverviewMetrics";

describe("OverviewMetrics", () => {
const mockRun = {
id: "123",
runStatus: "RUNNING"
};

test("should render OverviewMetrics component", () => {
const rendered = render(<OverviewMetrics run={mockRun} />);
const component = rendered.container;
expect(component.outerHTML).toMatchSnapshot();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`OverviewMetrics should render OverviewMetrics component 1`] = `"<div><div class=\\"ant-row ant-row-center ant-row-middle\\" style=\\"align-self: center;\\"><div class=\\"ant-spin ant-spin-spinning\\"><span role=\\"img\\" aria-label=\\"loading\\" style=\\"font-size: 64px;\\" class=\\"anticon anticon-loading anticon-spin ant-spin-dot\\"><svg viewBox=\\"0 0 1024 1024\\" focusable=\\"false\\" data-icon=\\"loading\\" width=\\"1em\\" height=\\"1em\\" fill=\\"currentColor\\" aria-hidden=\\"true\\"><path d=\\"M988 548c-19.9 0-36-16.1-36-36 0-59.4-11.6-117-34.6-171.3a440.45 440.45 0 00-94.3-139.9 437.71 437.71 0 00-139.9-94.3C629 83.6 571.4 72 512 72c-19.9 0-36-16.1-36-36s16.1-36 36-36c69.1 0 136.2 13.5 199.3 40.3C772.3 66 827 103 874 150c47 47 83.9 101.8 109.7 162.7 26.7 63.1 40.2 130.2 40.2 199.3.1 19.9-16 36-35.9 36z\\"></path></svg></span></div></div></div>"`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import OverviewMetrics from "./OverviewMetrics";

export default OverviewMetrics;
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import RunEndpointCharts from "./EndpointCharts";
import InitialConfiguration from "./InitialConfiguration";
import MoreButtonsMenu from "./MoreButtonsMenu";
import RunNotesInput from "./NotesInput";
import OverviewMetrics from "./OverviewMetrics";
import ResponseCodes from "./ResponseCodes";
import RunRunningTime from "./RunningTime";
import StopExecutionButton from "./StopExecutionButton";
Expand Down Expand Up @@ -150,7 +151,9 @@ const RunRunningStatus = ({ run }) => {
</>
) : null}
</Col>

<Col style={{ flex: 1, marginTop: "10px" }}>
<OverviewMetrics run={run} />
</Col>
<Col span={24}>
<Tabs
defaultActiveKey={activeTabKey}
Expand Down
11 changes: 11 additions & 0 deletions web/frontend/src/pages/Layout/layout.css
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,14 @@ main {
text-overflow: ellipsis;
}

.ant-descriptions-row > td {
padding-bottom: 5px;
}

.ant-statistic-content {
font-size: 16px;
}

.ant-statistic-title {
margin-bottom: 0px;
}

0 comments on commit 58c653b

Please sign in to comment.