Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 24 additions & 3 deletions web/pgadmin/dashboard/static/js/Graphs.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
];
}
});
Expand Down Expand Up @@ -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']});
Expand All @@ -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} : {}),
};
});
})
Expand Down
95 changes: 94 additions & 1 deletion web/regression/javascript/dashboard/graphs_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', ()=>{
Expand Down Expand Up @@ -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],
Expand Down Expand Up @@ -132,4 +164,65 @@ describe('Graphs.js', ()=>{
expect(found).toHaveTextContent('5000');
});
});

describe('<Graphs /> 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(<ThemedGraphs preferences={dashboardPref} sid={1} did={1} enablePoll={true} pageVisible={true} />);
});

/* 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]]');
});
});
});
Loading