pgadmin4/web/pgadmin/static/js/Explain/index.jsx
Aditya Toshniwal b5b9ee46a1 1) Port query tool to React. Fixes #6131
2) Added status bar to the Query Tool. Fixes #3253
3) Ensure that row numbers should be visible in view when scrolling horizontally. Fixes #3989
4) Allow removing a single query history. Refs #4113
5) Partially fixed Macros usability issues. Ref #6969
6) Fixed an issue where the Query tool opens on minimum size if the user opens multiple query tool Window quickly. Fixes #6725
7) Relocate GIS Viewer Button to the Left Side of the Results Table. Fixes #6830
8) Fixed an issue where the connection bar is not visible. Fixes #7188
9) Fixed an issue where an Empty message popup after running a query. Fixes #7260
10) Ensure that Autocomplete should work after changing the connection. Fixes #7262
11) Fixed an issue where the copy and paste row does not work if the first column contains no data. Fixes #7294
2022-04-07 17:36:56 +05:30

488 lines
14 KiB
JavaScript

/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import { Box, Tab, Tabs } from '@material-ui/core';
import React from 'react';
import _ from 'lodash';
import Graphical from './Graphical';
import TabPanel from '../components/TabPanel';
import gettext from 'sources/gettext';
import ImageMapper from './ImageMapper';
import { makeStyles } from '@material-ui/styles';
import Analysis from './Analysis';
import ExplainStatistics from './ExplainStatistics';
import PropTypes from 'prop-types';
import EmptyPanelMessage from '../components/EmptyPanelMessage';
const useStyles = makeStyles((theme)=>({
tabPanel: {
padding: 0,
backgroundColor: theme.palette.background.default,
}
}));
// Some predefined constants used to calculate image location and its border
var pWIDTH = 100.;
var pHEIGHT = 100.;
var offsetX = 200,
offsetY = 60;
var xMargin = 25,
yMargin = 25;
const DEFAULT_ARROW_SIZE = 2;
function nodeExplainTableData(_planData, _ctx) {
let node_info,
display_text = [],
tooltip = [],
node_extra_info = [],
info = _ctx.explainTable;
// Display: <NODE>[ using <Index> ] [ on <Schema>.<Table>[ as <Alias>]]
if (/Scan/.test(_planData['Node Type'])) {
display_text.push(_planData['Node Type']);
tooltip.push(_planData['Node Type']);
} else {
display_text.push(_planData['image_text']);
tooltip.push(_planData['image_text']);
}
node_info = tooltip.join('');
if (typeof(_planData['Index Name']) !== 'undefined') {
display_text.push(' using ');
tooltip.push(' using ');
display_text.push(_.escape(_planData['Index Name']));
tooltip.push(_planData['Index Name']);
}
if (typeof(_planData['Relation Name']) !== 'undefined') {
display_text.push(' on ');
tooltip.push(' on ');
if (typeof(_planData['Schema']) !== 'undefined') {
display_text.push(_.escape(_planData['Schema']));
tooltip.push(_planData['Schema']);
display_text.push('.');
tooltip.push('.');
}
display_text.push(_.escape(_planData['Relation Name']));
tooltip.push(_planData['Relation Name']);
if (typeof(_planData['Alias']) !== 'undefined') {
display_text.push(' as ');
tooltip.push(' as ');
display_text.push(_.escape(_planData['Alias']));
tooltip.push(_.escape(_planData['Alias']));
}
}
if (
typeof(_planData['Plan Rows']) !== 'undefined' &&
typeof(_planData['Plan Width']) !== 'undefined'
) {
let cost = [
' (cost=',
(typeof(_planData['Startup Cost']) !== 'undefined' ?
_planData['Startup Cost'] : ''),
'..',
(typeof(_planData['Total Cost']) !== 'undefined' ?
_planData['Total Cost'] : ''),
' rows=',
_planData['Plan Rows'],
' width=',
_planData['Plan Width'],
')',
].join('');
display_text.push(cost);
tooltip.push(cost);
}
if (
typeof(_planData['Actual Startup Time']) !== 'undefined' ||
typeof(_planData['Actual Total Time']) !== 'undefined' ||
typeof(_planData['Actual Rows']) !== 'undefined'
) {
let actual = [
' (',
(typeof(_planData['Actual Startup Time']) !== 'undefined' ?
('actual=' + _planData['Actual Startup Time']) + '..' : ''
),
(typeof(_planData['Actual Total Time']) !== 'undefined' ?
_planData['Actual Total Time'] + ' ' : ''
),
(typeof(_planData['Actual Rows']) !== 'undefined' ?
('rows=' + _planData['Actual Rows']) : ''
),
(typeof(_planData['Actual Loops']) !== 'undefined' ?
(' loops=' + _planData['Actual Loops']) : ''
),
')',
].join('');
display_text.push(actual);
tooltip.push(actual);
}
if ('Join Filter' in _planData) {
node_extra_info.push(
'<strong>' + gettext('Join Filter') + '</strong>: ' + _.escape(_planData['Join Filter'])
);
}
if ('Filter' in _planData) {
node_extra_info.push('<strong>' + gettext('Filter') + '</strong>: ' + _.escape(_planData['Filter']));
}
if ('Index Cond' in _planData) {
node_extra_info.push('<strong>' + gettext('Index Cond') + '</strong>: ' + _.escape(_planData['Index Cond']));
}
if ('Hash Cond' in _planData) {
node_extra_info.push('<strong>' + gettext('Hash Cond') + '</strong>: ' + _.escape(_planData['Hash Cond']));
}
if ('Rows Removed by Filter' in _planData) {
node_extra_info.push(
'<strong>' + gettext('Rows Removed by Filter') + '</strong>: ' +
_.escape(_planData['Rows Removed by Filter'])
);
}
if ('Peak Memory Usage' in _planData) {
var buffer = [
'<strong>' + gettext('Buckets') + '</strong>:', _.escape(_planData['Hash Buckets']),
'<strong>' + gettext('Batches') + '</strong>:', _.escape(_planData['Hash Batches']),
'<strong>' + gettext('Memory Usage') + '</strong>:', _.escape(_planData['Peak Memory Usage']), 'kB',
].join(' ');
node_extra_info.push(buffer);
}
if ('Recheck Cond' in _planData) {
node_extra_info.push('<strong>' + gettext('Recheck Cond') + '</strong>: ' + _planData['Recheck Cond']);
}
if ('Exact Heap Blocks' in _planData) {
node_extra_info.push('<strong>' + gettext('Heap Blocks') + '</strong>: exact=' + _planData['Exact Heap Blocks']);
}
info.rows.push({
data: _planData,
display_text: display_text.join(''),
tooltip_text: tooltip.join(''),
node_extra_info: node_extra_info,
});
if (typeof(_planData['exclusive_flag']) !== 'undefined') {
info.show_timings = true;
}
if (typeof(_planData['rowsx_flag']) !== 'undefined') {
info.show_rowsx = true;
}
if (typeof(_planData['Actual Loops']) !== 'undefined') {
info.show_rows = true;
}
if (typeof(_planData['Plan Rows']) !== 'undefined') {
info.show_plan_rows = true;
}
if (typeof(_planData['total_time']) !== 'undefined') {
info.total_time = _planData['total_time'];
}
let node;
if (typeof(_planData['Relation Name']) !== 'undefined') {
let relationName = (
typeof(_planData['Schema']) !== 'undefined' ?
(_planData['Schema'] + '.') : ''
) + _planData['Relation Name'],
table = info.statistics.tables[relationName] || {
name: relationName,
count: 0,
sum_of_times: 0,
nodes: {},
};
node = table.nodes[node_info] || {
name: node_info,
count: 0,
sum_of_times: 0,
};
table.count++;
table.sum_of_times += _planData['exclusive'];
node.count++;
node.sum_of_times += _planData['exclusive'];
table.nodes[node_info] = node;
info.statistics.tables[relationName] = table;
}
node = info.statistics.nodes[node_info] || {
name: node_info,
count: 0,
sum_of_times: 0,
};
node.count++;
node.sum_of_times += _planData['exclusive'];
info.statistics.nodes[node_info] = node;
}
function parsePlan(data, ctx) {
var idx = 1,
lvl = data.level = data.level || [idx],
plans = [],
nodeType = data['Node Type'],
// Calculating relative xpos of current node from top node
xpos = data.xpos = data.xpos - pWIDTH,
// Calculating relative ypos of current node from top node
ypos = data.ypos,
maxChildWidth = 0;
ctx.totalNodes++;
ctx.explainTable.total_time = data['total_time'] || data['Actual Total Time'];
data['_serial'] = ctx.totalNodes;
data['width'] = pWIDTH;
data['height'] = pHEIGHT;
// Calculate arrow width according to cost of a particular plan
let arrowSize = DEFAULT_ARROW_SIZE;
let startCost = data['Startup Cost'],
totalCost = data['Total Cost'];
if (startCost != undefined && totalCost != undefined) {
arrowSize = Math.round(Math.log((startCost + totalCost) / 2 + startCost));
arrowSize = arrowSize < 1 ? 1 : arrowSize > 10 ? 10 : arrowSize;
}
data['arr_id'] = _.uniqueId('arr');
ctx.arrows[data['arr_id']] = arrowSize;
/*
* calculating xpos, ypos, width and height if current node is a subplan
*/
if (data['Parent Relationship'] === 'SubPlan') {
data['width'] += (xMargin * 2) + (xMargin / 2);
data['height'] += (yMargin * 2);
data['ypos'] += yMargin;
xpos -= xMargin;
ypos += yMargin;
}
if (nodeType.startsWith('(slice'))
nodeType = nodeType.substring(0, 7);
// Get the image information for current node
var mappedImage = (_.isFunction(ImageMapper[nodeType]) &&
ImageMapper[nodeType].apply(undefined, [data])) ||
ImageMapper[nodeType] || {
'image': 'ex_unknown.svg',
'image_text': nodeType,
};
data['image'] = mappedImage['image'];
data['image_text'] = mappedImage['image_text'];
if ('Actual Total Time' in data && 'Actual Loops' in data) {
data['inclusive'] = Math.ceil10(
data['Actual Total Time'] * data['Actual Loops'], -3
);
data['exclusive'] = data['inclusive'];
data['inclusive_factor'] = data['inclusive'] / (
data['total_time'] || data['Actual Total Time']
);
data['inclusive_flag'] = data['inclusive_factor'] <= 0.1 ? '1' :
data['inclusive_factor'] < 0.5 ? '2' :
data['inclusive_factor'] <= 0.9 ? '3' : '4';
}
if ('Actual Rows' in data && 'Plan Rows' in data) {
if (
data['Actual Rows'] === 0 || data['Actual Rows'] > data['Plan Rows']
) {
data['rowsx'] = data['Plan Rows'] === 0 ? 0 :
(data['Actual Rows'] / data['Plan Rows']);
data['rowsx_direction'] = 'negative';
} else {
data['rowsx'] = data['Actual Rows'] === 0 ? 0 : (
data['Plan Rows'] / data['Actual Rows']
);
data['rowsx_direction'] = 'positive';
}
data['rowsx_flag'] = data['rowsx'] <= 10 ? '1' : (
data['rowsx'] <= 100 ? '2' : (data['rowsx'] <= 1000 ? '3' : '4')
);
data['rowsx'] = Math.ceil10(data['rowsx'], -2);
}
// Start calculating xpos, ypos, width and height for child plans if any
if ('Plans' in data) {
data['width'] += offsetX;
data['Plans'].forEach(function(p) {
let level = _.clone(lvl);
level.push(idx);
let plan = parsePlan({
...p,
'level': level,
xpos: xpos - offsetX,
ypos: ypos,
total_time: data['total_time'] || data['Actual Total Time'],
parent_node: lvl.join('_'),
}, ctx);
if (maxChildWidth < plan.width) {
maxChildWidth = plan.width;
}
if ('exclusive' in data) {
if (plan.inclusive) {
data['exclusive'] -= plan.inclusive;
}
}
var childHeight = plan.height;
if (idx !== 1) {
data['height'] = data['height'] + childHeight + offsetY;
} else if (childHeight > data['height']) {
data['height'] = childHeight;
}
ypos += childHeight + offsetY;
plans.push(plan);
idx++;
});
}
if ('exclusive' in data) {
data['exclusive'] = Math.ceil10(data['exclusive'], -3);
data['exclusive_factor'] = (
data['exclusive'] / (data['total_time'] || data['Actual Total Time'])
);
data['exclusive_flag'] = data['exclusive_factor'] <= 0.1 ? '1' :
data['exclusive_factor'] < 0.5 ? '2' :
data['exclusive_factor'] <= 0.9 ? '3' : '4';
}
// Final Width and Height of current node
data['width'] += maxChildWidth;
data['Plans'] = plans;
nodeExplainTableData(data, ctx);
return data;
}
function parsePlanData(data, ctx) {
let retPlan = {};
if(data) {
if ('Plan' in data) {
let plan = parsePlan({
...data['Plan'],
xpos: 0,
ypos: 0,
}, ctx);
retPlan['Plan'] = plan;
retPlan['xpos'] = 0;
retPlan['ypos'] = 0;
retPlan['width'] = plan.width + (xMargin * 2);
retPlan['height'] = plan.height + (yMargin * 4);
}
retPlan['Statistics'] = {
'JIT': [],
'Triggers': [],
'Summary': {},
};
if (data && 'JIT' in data) {
retPlan['Statistics']['JIT'] = retPlan['JIT'];
}
if (data && 'Triggers' in data) {
retPlan['Statistics']['Triggers'] = retPlan['JITriggersT'];
}
if(data) {
let summKeys = ['Planning Time', 'Execution Time'],
summary = {};
summKeys.forEach((key)=>{
if (key in data) {
summary[key] = data[key];
}
});
retPlan['Statistics']['Summary'] = summary;
}
if (data && 'Settings' in data) {
retPlan['Statistics']['Settings'] = data['Settings'];
}
}
return retPlan;
}
export default function Explain({plans=[]}) {
const classes = useStyles();
const [tabValue, setTabValue] = React.useState(0);
let ctx = React.useRef({
totalNodes: 0,
totalDownloadedNodes: 0,
isDownloaded: 0,
explainTable: {
rows: [],
statistics: {
tables: {},
nodes: {},
},
},
arrows: {},
});
let planData = React.useMemo(()=>(plans && parsePlanData(plans[0], ctx.current)), [plans]);
if(_.isEmpty(plans)) {
return <Box height="100%" display="flex" flexDirection="column">
<EmptyPanelMessage text={gettext('Use Explain/Explain analyze button to generate the plan for a query. Alternatively, you can also execute "EXPLAIN (FORMAT JSON) [QUERY]".')} />
</Box>;
}
return (
<Box height="100%" display="flex" flexDirection="column">
<Box>
<Tabs
value={tabValue}
onChange={(_e, selTabValue) => {
setTabValue(selTabValue);
}}
// indicatorColor="primary"
variant="scrollable"
scrollButtons="auto"
action={(ref)=>ref && ref.updateIndicator()}
>
<Tab label="Graphical" />
<Tab label="Analysis" />
<Tab label="Statistics" />
</Tabs>
</Box>
<TabPanel value={tabValue} index={0} classNameRoot={classes.tabPanel}>
<Graphical planData={planData} ctx={ctx.current}/>
</TabPanel>
<TabPanel value={tabValue} index={1} classNameRoot={classes.tabPanel}>
<Analysis explainTable={ctx.current.explainTable} />
</TabPanel>
<TabPanel value={tabValue} index={2} classNameRoot={classes.tabPanel}>
<ExplainStatistics explainTable={ctx.current.explainTable} />
</TabPanel>
</Box>
);
}
Explain.propTypes = {
plans: PropTypes.array,
};