pgadmin4/web/pgadmin/dashboard/static/js/SystemStats/Memory.jsx

317 lines
11 KiB
React
Raw Normal View History

/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
2024-01-01 02:43:48 -06:00
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React, { useState, useEffect, useRef, useReducer, useMemo } from 'react';
import PgTable from 'sources/components/PgTable';
import gettext from 'sources/gettext';
import PropTypes from 'prop-types';
import { makeStyles } from '@material-ui/core/styles';
import {getGCD, getEpoch} from 'sources/utils';
import ChartContainer from '../components/ChartContainer';
import { Box, Grid } from '@material-ui/core';
import { DATA_POINT_SIZE } from 'sources/chartjs';
import StreamingChart from '../../../../static/js/components/PgChart/StreamingChart';
import {useInterval, usePrevious} from 'sources/custom_hooks';
import axios from 'axios';
import { getStatsUrl, transformData, statsReducer, X_AXIS_LENGTH } from './utility.js';
import { toPrettySize } from '../../../../static/js/utils';
const useStyles = makeStyles((theme) => ({
autoResizer: {
height: '100% !important',
width: '100% !important',
background: theme.palette.grey[400],
padding: '8px',
overflowX: 'auto !important',
overflowY: 'hidden !important',
minHeight: '100%',
minWidth: '100%',
},
container: {
height: 'auto',
padding: '0px !important',
marginBottom: '4px',
},
fixedContainer: {
flexGrow: 1,
padding: '0px !important',
marginBottom: '4px',
},
tableContainer: {
padding: '6px',
width: '100%',
},
containerHeader: {
2023-10-11 00:57:21 -05:00
fontSize: '15px',
fontWeight: 'bold',
2023-10-11 00:57:21 -05:00
display: 'flex',
alignItems: 'center',
height: '100%',
}
}));
const chartsDefault = {
'm_stats': {'Total': [], 'Used': [], 'Free': []},
'sm_stats': {'Total': [], 'Used': [], 'Free': []},
'pmu_stats': {},
};
export default function Memory({preferences, sid, did, pageVisible, enablePoll=true}) {
const refreshOn = useRef(null);
const prevPrefernces = usePrevious(preferences);
const [memoryUsageInfo, memoryUsageInfoReduce] = useReducer(statsReducer, chartsDefault['m_stats']);
const [swapMemoryUsageInfo, swapMemoryUsageInfoReduce] = useReducer(statsReducer, chartsDefault['sm_stats']);
const [processMemoryUsageStats, setProcessMemoryUsageStats] = useState([]);
const [, setCounterData] = useState({});
const [pollDelay, setPollDelay] = useState(5000);
const [errorMsg, setErrorMsg] = useState(null);
const [chartDrawnOnce, setChartDrawnOnce] = useState(false);
const tableHeader = [
{
Header: gettext('PID'),
accessor: 'pid',
sortable: true,
resizable: true,
disableGlobalFilter: false,
},
{
Header: gettext('Name'),
accessor: 'name',
sortable: true,
resizable: true,
disableGlobalFilter: false,
},
{
2023-10-11 00:57:21 -05:00
Header: gettext('Memory usage'),
accessor: 'memory_usage',
sortable: true,
resizable: true,
disableGlobalFilter: false,
},
{
2023-10-11 00:57:21 -05:00
Header: gettext('Memory bytes'),
accessor: 'memory_bytes',
sortable: true,
resizable: true,
disableGlobalFilter: false,
},
];
useEffect(()=>{
let calcPollDelay = false;
if(prevPrefernces) {
if(prevPrefernces['m_stats_refresh'] != preferences['m_stats_refresh']) {
memoryUsageInfoReduce({reset: chartsDefault['m_stats']});
calcPollDelay = true;
}
if(prevPrefernces['sm_stats_refresh'] != preferences['sm_stats_refresh']) {
swapMemoryUsageInfoReduce({reset: chartsDefault['sm_stats']});
calcPollDelay = true;
}
if(prevPrefernces['pmu_stats_refresh'] != preferences['pmu_stats_refresh']) {
setProcessMemoryUsageStats([]);
calcPollDelay = true;
}
} else {
calcPollDelay = true;
}
if(calcPollDelay) {
const keys = Object.keys(chartsDefault);
const length = keys.length;
if(length == 1){
setPollDelay(
preferences[keys[0]+'_refresh']*1000
);
} else {
setPollDelay(
getGCD(Object.keys(chartsDefault).map((name)=>preferences[name+'_refresh']))*1000
);
}
}
}, [preferences]);
useEffect(()=>{
/* Charts rendered are not visible when, the dashboard is hidden but later visible */
if(pageVisible && !chartDrawnOnce) {
setChartDrawnOnce(true);
}
}, [pageVisible]);
useInterval(()=>{
const currEpoch = getEpoch();
if(refreshOn.current === null) {
let tmpRef = {};
Object.keys(chartsDefault).forEach((name)=>{
tmpRef[name] = currEpoch;
});
refreshOn.current = tmpRef;
}
let getFor = [];
Object.keys(chartsDefault).forEach((name)=>{
if(currEpoch >= refreshOn.current[name]) {
getFor.push(name);
refreshOn.current[name] = currEpoch + preferences[name+'_refresh'];
}
});
let path = getStatsUrl(sid, did, getFor);
if (!pageVisible){
return;
}
axios.get(path)
.then((resp)=>{
let data = resp.data;
setErrorMsg(null);
if(data.hasOwnProperty('m_stats')){
let new_m_stats = {
'Total': data['m_stats']['total_memory']?data['m_stats']['total_memory']:0,
'Used': data['m_stats']['used_memory']?data['m_stats']['used_memory']:0,
'Free': data['m_stats']['free_memory']?data['m_stats']['free_memory']:0,
};
memoryUsageInfoReduce({incoming: new_m_stats});
}
if(data.hasOwnProperty('sm_stats')){
let new_sm_stats = {
'Total': data['sm_stats']['swap_total']?data['sm_stats']['swap_total']:0,
'Used': data['sm_stats']['swap_used']?data['sm_stats']['swap_used']:0,
'Free': data['sm_stats']['swap_free']?data['sm_stats']['swap_free']:0,
};
swapMemoryUsageInfoReduce({incoming: new_sm_stats});
}
if(data.hasOwnProperty('pmu_stats')){
let pmu_info_list = [];
const pmu_info_obj = data['pmu_stats'];
for (const key in pmu_info_obj) {
pmu_info_list.push({ icon: '', pid: pmu_info_obj[key]['pid'], name: gettext(pmu_info_obj[key]['name']), memory_usage: gettext(toPrettySize(pmu_info_obj[key]['memory_usage'])), memory_bytes: gettext(toPrettySize(pmu_info_obj[key]['memory_bytes'])) });
}
setProcessMemoryUsageStats(pmu_info_list);
}
setCounterData((prevCounterData)=>{
return {
...prevCounterData,
...data,
};
});
})
.catch((error)=>{
if(!errorMsg) {
memoryUsageInfoReduce({reset:chartsDefault['m_stats']});
swapMemoryUsageInfoReduce({reset:chartsDefault['sm_stats']});
setProcessMemoryUsageStats([]);
setCounterData({});
if(error.response) {
if (error.response.status === 428) {
setErrorMsg(gettext('Please connect to the selected server to view the graph.'));
} else {
setErrorMsg(gettext('An error occurred whilst rendering the graph.'));
}
} else if(error.request) {
setErrorMsg(gettext('Not connected to the server or the connection to the server has been closed.'));
return;
} else {
console.error(error);
}
}
});
}, enablePoll ? pollDelay : -1);
return (
<Box display="flex" flexDirection="column" height="100%">
<div data-testid='graph-poll-delay' style={{display: 'none'}}>{pollDelay}</div>
{chartDrawnOnce &&
<MemoryWrapper
memoryUsageInfo={transformData(memoryUsageInfo, preferences['m_stats_refresh'])}
swapMemoryUsageInfo={transformData(swapMemoryUsageInfo, preferences['sm_stats_refresh'])}
processMemoryUsageStats={processMemoryUsageStats}
tableHeader={tableHeader}
errorMsg={errorMsg}
showTooltip={preferences['graph_mouse_track']}
showDataPoints={preferences['graph_data_points']}
lineBorderWidth={preferences['graph_line_border_width']}
isTest={false}
/>
}
</Box>
);
}
Memory.propTypes = {
preferences: PropTypes.object.isRequired,
sid: PropTypes.oneOfType([PropTypes.string.isRequired, PropTypes.number.isRequired]),
did: PropTypes.oneOfType([PropTypes.string.isRequired, PropTypes.number.isRequired]),
pageVisible: PropTypes.bool,
enablePoll: PropTypes.bool,
};
export function MemoryWrapper(props) {
const classes = useStyles();
const options = useMemo(()=>({
showDataPoints: props.showDataPoints,
showTooltip: props.showTooltip,
lineBorderWidth: props.lineBorderWidth,
}), [props.showTooltip, props.showDataPoints, props.lineBorderWidth]);
return (
<>
<Grid container spacing={1} className={classes.container}>
<Grid item md={6} sm={12}>
<ChartContainer id='m-graph' title={gettext('Memory')} datasets={props.memoryUsageInfo.datasets} errorMsg={props.errorMsg} isTest={props.isTest}>
<StreamingChart data={props.memoryUsageInfo} dataPointSize={DATA_POINT_SIZE} xRange={X_AXIS_LENGTH} options={options}
valueFormatter={toPrettySize}/>
</ChartContainer>
</Grid>
<Grid item md={6} sm={12}>
<ChartContainer id='sm-graph' title={gettext('Swap memory')} datasets={props.swapMemoryUsageInfo.datasets} errorMsg={props.errorMsg} isTest={props.isTest}>
<StreamingChart data={props.swapMemoryUsageInfo} dataPointSize={DATA_POINT_SIZE} xRange={X_AXIS_LENGTH} options={options}
valueFormatter={toPrettySize}/>
</ChartContainer>
</Grid>
</Grid>
<Grid container spacing={1} className={classes.fixedContainer}>
<div className={classes.tableContainer}>
<PgTable
className={classes.autoResizer}
2023-10-11 00:57:21 -05:00
CustomHeader={() => {
return <div className={classes.containerHeader}>{gettext('Process memory usage')}</div>;
}}
columns={props.tableHeader}
data={props.processMemoryUsageStats}
msg={props.errorMsg}
type={'panel'}
caveTable={false}
></PgTable>
</div>
</Grid>
</>
);
}
const propTypeStats = PropTypes.shape({
datasets: PropTypes.array,
refreshRate: PropTypes.number.isRequired,
});
MemoryWrapper.propTypes = {
memoryUsageInfo: propTypeStats.isRequired,
swapMemoryUsageInfo: propTypeStats.isRequired,
processMemoryUsageStats: PropTypes.array.isRequired,
tableHeader: PropTypes.array.isRequired,
errorMsg: PropTypes.string,
showTooltip: PropTypes.bool.isRequired,
showDataPoints: PropTypes.bool.isRequired,
lineBorderWidth: PropTypes.number.isRequired,
isTest: PropTypes.bool,
};