diff --git a/web/pgadmin/dashboard/static/js/Graphs.jsx b/web/pgadmin/dashboard/static/js/Graphs.jsx
index 7f2eae22825..53d23099acc 100644
--- a/web/pgadmin/dashboard/static/js/Graphs.jsx
+++ b/web/pgadmin/dashboard/static/js/Graphs.jsx
@@ -64,16 +64,27 @@ export function statsReducer(state, action) {
action.counterData = action.incoming;
}
+ /* When the counter represents a rate (e.g. transactions per second),
+ * the raw delta between two polls must be normalised by the number of
+ * seconds elapsed between them, otherwise it only reads correctly when
+ * the refresh interval happens to be 1 second.
+ */
+ let elapsed = action.elapsed > 0 ? action.elapsed : 1;
+
let newState = {};
Object.keys(action.incoming).forEach(label => {
+ let value = action.incoming[label];
+ if(action.counter) {
+ value = (action.incoming[label] - action.counterData[label]) / elapsed;
+ }
if(state[label]) {
newState[label] = [
- action.counter ? action.incoming[label] - action.counterData[label] : action.incoming[label],
+ value,
...state[label].slice(0, X_AXIS_LENGTH-1),
];
} else {
newState[label] = [
- action.counter ? action.incoming[label] - action.counterData[label] : action.incoming[label],
+ value,
];
}
});
@@ -164,12 +175,21 @@ export default function Graphs({preferences, sid, did, pageVisible, enablePoll=t
});
let path = getStatsUrl(sid, did, getFor);
+ /* Normalise TPS by the time actually measured between two samples
+ * rather than the configured refresh interval, as timers can be
+ * delayed or throttled by the browser.
+ */
+ const sampledAt = Date.now();
axios.get(path)
.then((resp)=>{
let data = resp.data;
setErrorMsg(null);
sessionStatsReduce({incoming: data['session_stats']});
- tpsStatsReduce({incoming: data['tps_stats'], counter: true, counterData: counterData['tps_stats']});
+ tpsStatsReduce({
+ incoming: data['tps_stats'], counter: true,
+ counterData: counterData['tps_stats'],
+ elapsed: (sampledAt - counterData['tps_stats_sampled_at']) / 1000,
+ });
tiStatsReduce({incoming: data['ti_stats'], counter: true, counterData: counterData['ti_stats']});
toStatsReduce({incoming: data['to_stats'], counter: true, counterData: counterData['to_stats']});
bioStatsReduce({incoming: data['bio_stats'], counter: true, counterData: counterData['bio_stats']});
@@ -178,6 +198,7 @@ export default function Graphs({preferences, sid, did, pageVisible, enablePoll=t
return {
...prevCounterData,
...data,
+ ...(data['tps_stats'] ? {'tps_stats_sampled_at': sampledAt} : {}),
};
});
})
diff --git a/web/regression/javascript/dashboard/graphs_spec.js b/web/regression/javascript/dashboard/graphs_spec.js
index d02cd3488fa..6ab4541c1d2 100644
--- a/web/regression/javascript/dashboard/graphs_spec.js
+++ b/web/regression/javascript/dashboard/graphs_spec.js
@@ -5,7 +5,20 @@ import { DATA_POINT_SIZE } from 'sources/chartjs';
import Graphs, { transformData,
getStatsUrl, statsReducer} from '../../../pgadmin/dashboard/static/js/Graphs';
import { withTheme } from '../fake_theme';
-import { render } from '@testing-library/react';
+import { act, render } from '@testing-library/react';
+import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
+
+/* Render each chart's data points as text so the plotted values can be
+ * checked without drawing the charts.
+ */
+jest.mock('../../../pgadmin/dashboard/static/js/components/ChartContainer', ()=>{
+ const React = require('react');
+ const MockChartContainer = ({id, datasets})=>React.createElement(
+ 'div', {'data-testid': id}, JSON.stringify(datasets.map((d)=>d.data))
+ );
+ return MockChartContainer;
+});
describe('Graphs.js', ()=>{
it('transformData', ()=>{
@@ -72,6 +85,25 @@ describe('Graphs.js', ()=>{
expect(state).toEqual(newState);
});
+ it('with incoming with counter and elapsed time', ()=>{
+ let state = {
+ 'Label1': [1], 'Label2': [2],
+ };
+ let action = {
+ incoming: {
+ 'Label1': 11, 'Label2': 23,
+ },
+ counter: true,
+ counterData: {'Label1': 1, 'Label2': 3},
+ elapsed: 5,
+ };
+ let newState = {
+ 'Label1': [2, 1], 'Label2': [4, 2],
+ };
+ state = statsReducer(state, action);
+ expect(state).toEqual(newState);
+ });
+
it('with reset', ()=>{
let state = {
'Label1': [0, 1], 'Label2': [1, 2],
@@ -132,4 +164,65 @@ describe('Graphs.js', ()=>{
expect(found).toHaveTextContent('5000');
});
});
+
+ describe(' TPS polling', ()=>{
+ let networkMock;
+ let ThemedGraphs = withTheme(Graphs);
+ let dashboardPref = {
+ session_stats_refresh: 5,
+ tps_stats_refresh: 5,
+ ti_stats_refresh: 5,
+ to_stats_refresh: 5,
+ bio_stats_refresh: 5,
+ show_graphs: true,
+ graph_data_points: true,
+ graph_mouse_track: true,
+ graph_line_border_width: 2
+ };
+
+ let commitCounts;
+
+ beforeEach(()=>{
+ jest.useFakeTimers();
+ commitCounts = [];
+ networkMock = new MockAdapter(axios);
+ /* Only answer with TPS data when the poll asks for it. */
+ networkMock.onGet(/\/dashboard\/dashboard_stats\//).reply((config)=>{
+ if(config.url.includes('tps_stats')) {
+ return [200, {'tps_stats': {'Commits': commitCounts.shift()}}];
+ }
+ return [200, {}];
+ });
+ });
+
+ afterEach(()=>{
+ networkMock.restore();
+ jest.useRealTimers();
+ });
+
+ it('normalises TPS by the measured time between samples', async ()=>{
+ commitCounts.push(100);
+ let graphComp;
+ await act(async ()=>{
+ graphComp = render();
+ });
+
+ /* 50 transactions over the configured 5 seconds is 10 per second. */
+ commitCounts.push(150);
+ await act(async ()=>{
+ await jest.advanceTimersByTimeAsync(5000);
+ });
+ expect(graphComp.getByTestId('tps-graph')).toHaveTextContent('[[10,0]]');
+
+ /* A delayed poll: 100 transactions over 10 seconds is still 10 per
+ * second, not 20.
+ */
+ jest.setSystemTime(Date.now() + 5000);
+ commitCounts.push(250);
+ await act(async ()=>{
+ await jest.advanceTimersByTimeAsync(5000);
+ });
+ expect(graphComp.getByTestId('tps-graph')).toHaveTextContent('[[10,10,0]]');
+ });
+ });
});