Skip to content

Commit

Permalink
Use uplot for Dashboard graphs to reduce CPU usage. #5794
Browse files Browse the repository at this point in the history
  • Loading branch information
adityatoshniwal committed Feb 6, 2023
1 parent f3bb477 commit 4a3bcfa
Show file tree
Hide file tree
Showing 14 changed files with 230 additions and 126 deletions.
3 changes: 3 additions & 0 deletions web/package.json
Expand Up @@ -150,6 +150,7 @@
"react-draggable": "^4.4.4",
"react-dropzone": "^14.2.1",
"react-leaflet": "^3.2.2",
"react-resize-detector": "^8.0.3",
"react-rnd": "^10.3.5",
"react-router-dom": "^6.2.2",
"react-select": "^4.2.1",
Expand All @@ -164,6 +165,8 @@
"styled-components": "^5.2.1",
"tempusdominus-bootstrap-4": "^5.1.2",
"tempusdominus-core": "^5.19.3",
"uplot": "^1.6.24",
"uplot-react": "^1.1.4",
"valid-filename": "^2.0.1",
"webcabin-docker": "git+https://github.com/pgadmin-org/wcdocker/#3df8aac825ee2892f4d824de273b779cc6dbcad8",
"wkx": "^0.5.0",
Expand Down
4 changes: 2 additions & 2 deletions web/pgadmin/browser/static/js/node.js
Expand Up @@ -175,9 +175,9 @@ define('pgadmin.browser.node', [
// Show query tool only in context menu of supported nodes.
if (_.indexOf(pgAdmin.unsupported_nodes, self.type) == -1) {
let enable = function(itemData) {
if (itemData._type == 'database' && itemData.allowConn)
if (itemData?._type == 'database' && itemData?.allowConn)
return true;
else if (itemData._type != 'database')
else if (itemData?._type != 'database')
return true;
else
return false;
Expand Down
10 changes: 5 additions & 5 deletions web/pgadmin/dashboard/__init__.py
Expand Up @@ -75,39 +75,39 @@ def register_preferences(self):
self.session_stats_refresh = self.dashboard_preference.register(
'dashboards', 'session_stats_refresh',
gettext("Session statistics refresh rate"), 'integer',
1, min_val=1, max_val=999999,
5, min_val=1, max_val=999999,
category_label=PREF_LABEL_REFRESH_RATES,
help_str=help_string
)

self.tps_stats_refresh = self.dashboard_preference.register(
'dashboards', 'tps_stats_refresh',
gettext("Transaction throughput refresh rate"), 'integer',
1, min_val=1, max_val=999999,
5, min_val=1, max_val=999999,
category_label=PREF_LABEL_REFRESH_RATES,
help_str=help_string
)

self.ti_stats_refresh = self.dashboard_preference.register(
'dashboards', 'ti_stats_refresh',
gettext("Tuples in refresh rate"), 'integer',
1, min_val=1, max_val=999999,
5, min_val=1, max_val=999999,
category_label=PREF_LABEL_REFRESH_RATES,
help_str=help_string
)

self.to_stats_refresh = self.dashboard_preference.register(
'dashboards', 'to_stats_refresh',
gettext("Tuples out refresh rate"), 'integer',
1, min_val=1, max_val=999999,
5, min_val=1, max_val=999999,
category_label=PREF_LABEL_REFRESH_RATES,
help_str=help_string
)

self.bio_stats_refresh = self.dashboard_preference.register(
'dashboards', 'bio_stats_refresh',
gettext("Block I/O statistics refresh rate"), 'integer',
1, min_val=1, max_val=999999,
5, min_val=1, max_val=999999,
category_label=PREF_LABEL_REFRESH_RATES,
help_str=help_string
)
Expand Down
21 changes: 13 additions & 8 deletions web/pgadmin/dashboard/static/js/Dashboard.jsx
Expand Up @@ -911,13 +911,20 @@ export function ChartContainer(props) {
<div className="card-header">
<div className="d-flex">
<div id={props.id}>{props.title}</div>
<div className="ml-auto my-auto legend" ref={props.legendRef}></div>
<div className="ml-auto my-auto legend">
<div className="d-flex">
{props.datasets?.map((datum, i)=>(
<div className="legend-value" key={i}>
<span style={{backgroundColor: datum.borderColor}}>&nbsp;&nbsp;&nbsp;&nbsp;</span>
<span className="legend-label">{datum.label}</span>
</div>
))}
</div>
</div>
</div>
</div>
<div className="card-body dashboard-graph-body">
<div className={'chart-wrapper ' + (props.errorMsg ? 'd-none' : '')}>
{props.children}
</div>
{!props.errorMsg && !props.isTest && props.children}
<ChartError message={props.errorMsg} />
</div>
</div>
Expand All @@ -927,12 +934,10 @@ export function ChartContainer(props) {
ChartContainer.propTypes = {
id: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
legendRef: PropTypes.oneOfType([
PropTypes.func,
PropTypes.shape({ current: PropTypes.any }),
]).isRequired,
datasets: PropTypes.array.isRequired,
children: PropTypes.node.isRequired,
errorMsg: PropTypes.string,
isTest: PropTypes.bool
};

export function ChartError(props) {
Expand Down
107 changes: 22 additions & 85 deletions web/pgadmin/dashboard/static/js/Graphs.jsx
Expand Up @@ -6,54 +6,37 @@
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React, { useEffect, useRef, useState, useReducer, useCallback, useMemo } from 'react';
import { LineChart, DATA_POINT_STYLE, DATA_POINT_SIZE } from 'sources/chartjs';
import React, { useEffect, useRef, useState, useReducer, useMemo } from 'react';
import { DATA_POINT_SIZE } from 'sources/chartjs';
import {ChartContainer, DashboardRowCol, DashboardRow} from './Dashboard';
import url_for from 'sources/url_for';
import axios from 'axios';
import gettext from 'sources/gettext';
import {getGCD, getEpoch} from 'sources/utils';
import {useInterval, usePrevious} from 'sources/custom_hooks';
import PropTypes from 'prop-types';
import StreamingChart from '../../../static/js/components/PgChart/StreamingChart';

export const X_AXIS_LENGTH = 75;

/* Transform the labels data to suit ChartJS */
export function transformData(labels, refreshRate, use_diff_point_style) {
export function transformData(labels, refreshRate) {
const colors = ['#00BCD4', '#9CCC65', '#E64A19'];
let datasets = Object.keys(labels).map((label, i)=>{
return {
label: label,
data: labels[label] || [],
borderColor: colors[i],
backgroundColor: colors[i],
pointHitRadius: DATA_POINT_SIZE,
pointStyle: use_diff_point_style ? DATA_POINT_STYLE[i] : 'circle'
};
}) || [];

return {
labels: [...Array(X_AXIS_LENGTH).keys()],
datasets: datasets,
refreshRate: refreshRate,
};
}

/* Custom ChartJS legend callback */
export function generateLegend(chart) {
let text = [];
text.push('<div class="' + chart.id + '-legend d-flex">');
for (let chart_val of chart.data.datasets) {
text.push('<div class="legend-value"><span style="background-color:' + chart_val.backgroundColor + '">&nbsp;&nbsp;&nbsp;&nbsp;</span>');
if (chart_val.label) {
text.push('<span class="legend-label">' + chart_val.label + '</span>');
}
text.push('</div>');
}
text.push('</div>');
return text.join('');
}

/* URL for fetching graphs data */
export function getStatsUrl(sid=-1, did=-1, chart_names=[]) {
let base_url = url_for('dashboard.dashboard_stats');
Expand Down Expand Up @@ -104,7 +87,7 @@ const chartsDefault = {
'bio_stats': {'Reads': [], 'Hits': []},
};

export default function Graphs({preferences, sid, did, pageVisible, enablePoll=true}) {
export default function Graphs({preferences, sid, did, pageVisible, enablePoll=true, isTest}) {
const refreshOn = useRef(null);
const prevPrefernces = usePrevious(preferences);

Expand Down Expand Up @@ -238,6 +221,7 @@ export default function Graphs({preferences, sid, did, pageVisible, enablePoll=t
showDataPoints={preferences['graph_data_points']}
lineBorderWidth={preferences['graph_line_border_width']}
isDatabase={did > 0}
isTest={isTest}
/>
}
</>
Expand All @@ -256,92 +240,45 @@ Graphs.propTypes = {
]),
pageVisible: PropTypes.bool,
enablePoll: PropTypes.bool,
isTest: PropTypes.bool,
};

export function GraphsWrapper(props) {
const sessionStatsLegendRef = useRef();
const tpsStatsLegendRef = useRef();
const tiStatsLegendRef = useRef();
const toStatsLegendRef = useRef();
const bioStatsLegendRef = useRef();
const options = useMemo(()=>({
elements: {
point: {
radius: props.showDataPoints ? DATA_POINT_SIZE : 0,
},
line: {
borderWidth: props.lineBorderWidth,
},
},
plugins: {
legend: {
display: false,
},
tooltip: {
enabled: props.showTooltip,
callbacks: {
title: function(tooltipItem) {
let title = '';
try {
title = parseInt(tooltipItem[0].label) * tooltipItem[0].chart?.data.refreshRate + gettext(' seconds ago');
} catch (error) {
title = '';
}
return title;
},
},
}
},
scales: {
x: {
reverse: true,
},
y: {
min: 0,
}
},
showDataPoints: props.showDataPoints,
showTooltip: props.showTooltip,
lineBorderWidth: props.lineBorderWidth,
}), [props.showTooltip, props.showDataPoints, props.lineBorderWidth]);
const updateOptions = useMemo(()=>({duration: 0}), []);

const onInitCallback = useCallback(
(legendRef)=>(chart)=>{
legendRef.current.innerHTML = generateLegend(chart);
}
);

return (
<>
<DashboardRow>
<DashboardRowCol breakpoint='md' parts={6}>
<ChartContainer id='sessions-graph' title={props.isDatabase ? gettext('Database sessions') : gettext('Server sessions')} legendRef={sessionStatsLegendRef} errorMsg={props.errorMsg}>
<LineChart options={options} data={props.sessionStats} updateOptions={updateOptions}
onInit={onInitCallback(sessionStatsLegendRef)}/>
<ChartContainer id='sessions-graph' title={props.isDatabase ? gettext('Database sessions') : gettext('Server sessions')}
datasets={props.sessionStats.datasets} errorMsg={props.errorMsg} isTest={props.isTest}>
<StreamingChart data={props.sessionStats} dataPointSize={DATA_POINT_SIZE} xRange={X_AXIS_LENGTH} options={options} />
</ChartContainer>
</DashboardRowCol>
<DashboardRowCol breakpoint='md' parts={6}>
<ChartContainer id='tps-graph' title={gettext('Transactions per second')} legendRef={tpsStatsLegendRef} errorMsg={props.errorMsg}>
<LineChart options={options} data={props.tpsStats} updateOptions={updateOptions}
onInit={onInitCallback(tpsStatsLegendRef)}/>
<ChartContainer id='tps-graph' title={gettext('Transactions per second')} datasets={props.tpsStats.datasets} errorMsg={props.errorMsg} isTest={props.isTest}>
<StreamingChart data={props.tpsStats} dataPointSize={DATA_POINT_SIZE} xRange={X_AXIS_LENGTH} options={options} />
</ChartContainer>
</DashboardRowCol>
</DashboardRow>
<DashboardRow>
<DashboardRowCol breakpoint='md' parts={4}>
<ChartContainer id='ti-graph' title={gettext('Tuples in')} legendRef={tiStatsLegendRef} errorMsg={props.errorMsg}>
<LineChart options={options} data={props.tiStats} updateOptions={updateOptions}
onInit={onInitCallback(tiStatsLegendRef)}/>
<ChartContainer id='ti-graph' title={gettext('Tuples in')} datasets={props.tiStats.datasets} errorMsg={props.errorMsg} isTest={props.isTest}>
<StreamingChart data={props.tiStats} dataPointSize={DATA_POINT_SIZE} xRange={X_AXIS_LENGTH} options={options} />
</ChartContainer>
</DashboardRowCol>
<DashboardRowCol breakpoint='md' parts={4}>
<ChartContainer id='to-graph' title={gettext('Tuples out')} legendRef={toStatsLegendRef} errorMsg={props.errorMsg}>
<LineChart options={options} data={props.toStats} updateOptions={updateOptions}
onInit={onInitCallback(toStatsLegendRef)}/>
<ChartContainer id='to-graph' title={gettext('Tuples out')} datasets={props.toStats.datasets} errorMsg={props.errorMsg} isTest={props.isTest}>
<StreamingChart data={props.toStats} dataPointSize={DATA_POINT_SIZE} xRange={X_AXIS_LENGTH} options={options} />
</ChartContainer>
</DashboardRowCol>
<DashboardRowCol breakpoint='md' parts={4}>
<ChartContainer id='bio-graph' title={gettext('Block I/O')} legendRef={bioStatsLegendRef} errorMsg={props.errorMsg}>
<LineChart options={options} data={props.bioStats} updateOptions={updateOptions}
onInit={onInitCallback(bioStatsLegendRef)}/>
<ChartContainer id='bio-graph' title={gettext('Block I/O')} datasets={props.bioStats.datasets} errorMsg={props.errorMsg} isTest={props.isTest}>
<StreamingChart data={props.bioStats} dataPointSize={DATA_POINT_SIZE} xRange={X_AXIS_LENGTH} options={options} />
</ChartContainer>
</DashboardRowCol>
</DashboardRow>
Expand All @@ -350,7 +287,6 @@ export function GraphsWrapper(props) {
}

const propTypeStats = PropTypes.shape({
labels: PropTypes.array.isRequired,
datasets: PropTypes.array,
refreshRate: PropTypes.number.isRequired,
});
Expand All @@ -365,4 +301,5 @@ GraphsWrapper.propTypes = {
showDataPoints: PropTypes.bool.isRequired,
lineBorderWidth: PropTypes.number.isRequired,
isDatabase: PropTypes.bool.isRequired,
isTest: PropTypes.bool,
};
3 changes: 3 additions & 0 deletions web/pgadmin/static/css/style.css
Expand Up @@ -17,3 +17,6 @@
@import 'node_modules/react-checkbox-tree/lib/react-checkbox-tree.css';

@import 'node_modules/@simonwep/pickr/dist/themes/monolith.min.css';

@import 'node_modules/uplot/dist/uPlot.min.css';

2 changes: 2 additions & 0 deletions web/pgadmin/static/js/Theme/index.jsx
Expand Up @@ -21,6 +21,7 @@ import getDarkTheme from './dark';
import getHightContrastTheme from './high_contrast';
import { CssBaseline } from '@material-ui/core';
import pickrOverride from './overrides/pickr.override';
import uplotOverride from './overrides/uplot.override';

/* Common settings across all themes */
let basicSettings = createMuiTheme();
Expand Down Expand Up @@ -313,6 +314,7 @@ function getFinalTheme(baseTheme) {
padding: 0,
},
...pickrOverride(baseTheme),
...uplotOverride(baseTheme),
},
},
MuiOutlinedInput: {
Expand Down
32 changes: 32 additions & 0 deletions web/pgadmin/static/js/Theme/overrides/uplot.override.js
@@ -0,0 +1,32 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2023, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////

export default function uplotOverride(theme) {
return {
'.uplot': {
'& .u-legend': {
display: 'none',
}
},
'.uplot-tooltip': {
position: 'absolute',
fontSize: '0.9em',
padding: '4px 8px',
borderRadius: theme.shape.borderRadius,
color: theme.palette.background.default,
backgroundColor: theme.palette.text.primary,

'& .uplot-tooltip-label': {
display: 'flex',
gap: '4px',
alignItems: 'center',
}
}
};
}

0 comments on commit 4a3bcfa

Please sign in to comment.