///////////////////////////////////////////////////////////// // // 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: [ using ] [ on .[ as ]] 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( '' + gettext('Join Filter') + ': ' + _.escape(_planData['Join Filter']) ); } if ('Filter' in _planData) { node_extra_info.push('' + gettext('Filter') + ': ' + _.escape(_planData['Filter'])); } if ('Index Cond' in _planData) { node_extra_info.push('' + gettext('Index Cond') + ': ' + _.escape(_planData['Index Cond'])); } if ('Hash Cond' in _planData) { node_extra_info.push('' + gettext('Hash Cond') + ': ' + _.escape(_planData['Hash Cond'])); } if ('Rows Removed by Filter' in _planData) { node_extra_info.push( '' + gettext('Rows Removed by Filter') + ': ' + _.escape(_planData['Rows Removed by Filter']) ); } if ('Peak Memory Usage' in _planData) { var buffer = [ '' + gettext('Buckets') + ':', _.escape(_planData['Hash Buckets']), '' + gettext('Batches') + ':', _.escape(_planData['Hash Batches']), '' + gettext('Memory Usage') + ':', _.escape(_planData['Peak Memory Usage']), 'kB', ].join(' '); node_extra_info.push(buffer); } if ('Recheck Cond' in _planData) { node_extra_info.push('' + gettext('Recheck Cond') + ': ' + _planData['Recheck Cond']); } if ('Exact Heap Blocks' in _planData) { node_extra_info.push('' + gettext('Heap Blocks') + ': 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 ; } return ( { setTabValue(selTabValue); }} // indicatorColor="primary" variant="scrollable" scrollButtons="auto" action={(ref)=>ref && ref.updateIndicator()} > ); } Explain.propTypes = { plans: PropTypes.array, };