import {IPlan} from '../interfaces/iplan'; import {EstimateDirection} from '../enums'; /// /// export class PlanService { // plan property keys NODE_TYPE_PROP: string = 'Node Type'; ACTUAL_ROWS_PROP: string = 'Actual Rows'; PLAN_ROWS_PROP: string = 'Plan Rows'; ACTUAL_TOTAL_TIME_PROP: string = 'Actual Total Time'; ACTUAL_LOOPS_PROP: string = 'Actual Loops'; TOTAL_COST_PROP: string = 'Total Cost'; PLANS_PROP: string = 'Plans'; RELATION_NAME_PROP: string = 'Relation Name'; SCHEMA_PROP: string = 'Schema'; ALIAS_PROP: string = 'Alias'; GROUP_KEY_PROP: string = 'Group Key'; SORT_KEY_PROP: string = 'Sort Key'; JOIN_TYPE_PROP: string = 'Join Type'; INDEX_NAME_PROP: string = 'Index Name'; HASH_CONDITION_PROP: string = 'Hash Cond'; // computed by pev COMPUTED_TAGS_PROP: string = '*Tags'; COSTLIEST_NODE_PROP: string = '*Costiest Node (by cost)'; LARGEST_NODE_PROP: string = '*Largest Node (by rows)'; SLOWEST_NODE_PROP: string = '*Slowest Node (by duration)'; MAXIMUM_COSTS_PROP: string = '*Most Expensive Node (cost)'; MAXIMUM_ROWS_PROP: string = '*Largest Node (rows)'; MAXIMUM_DURATION_PROP: string = '*Slowest Node (time)'; ACTUAL_DURATION_PROP: string = '*Actual Duration'; ACTUAL_COST_PROP: string = '*Actual Cost'; PLANNER_ESTIMATE_FACTOR: string = '*Planner Row Estimate Factor'; PLANNER_ESIMATE_DIRECTION: string = '*Planner Row Estimate Direction'; CTE_SCAN_PROP = 'CTE Scan'; CTE_NAME_PROP = 'CTE Name'; ARRAY_INDEX_KEY: string = 'arrayIndex'; PEV_PLAN_TAG: string = 'plan_'; private _maxRows: number = 0; private _maxCost: number = 0; private _maxDuration: number = 0; getPlans(): Array { var plans: Array = []; for (var i in localStorage) { if (_.startsWith(i, this.PEV_PLAN_TAG)) { plans.push(JSON.parse(localStorage[i])); } } return _.chain(plans) .sortBy('createdOn') .reverse() .value(); } getPlan(id: string): IPlan { return JSON.parse(localStorage.getItem(id)); } createPlan(planName: string, planContent: string, planQuery): IPlan { var plan: IPlan = { id: this.PEV_PLAN_TAG + new Date().getTime().toString(), name: planName || 'plan created on ' + moment().format('LLL'), createdOn: new Date(), content: JSON.parse(planContent)[0], query: planQuery }; this.analyzePlan(plan); return plan; } isJsonString(str) { try { JSON.parse(str); } catch (e) { return false; } return true; } analyzePlan(plan: IPlan) { this.processNode(plan.content.Plan); plan.content[this.MAXIMUM_ROWS_PROP] = this._maxRows; plan.content[this.MAXIMUM_COSTS_PROP] = this._maxCost; plan.content[this.MAXIMUM_DURATION_PROP] = this._maxDuration; this.findOutlierNodes(plan.content.Plan); localStorage.setItem(plan.id, JSON.stringify(plan)); } deletePlan(plan: IPlan) { localStorage.removeItem(plan.id); } deleteAllPlans() { localStorage.clear(); } // recursively walk down the plan to compute various metrics processNode(node) { this.calculatePlannerEstimate(node); this.calculateActuals(node); _.each(node, (value, key) => { this.calculateMaximums(node, key, value); if (key === this.PLANS_PROP) { _.each(value, (value) => { this.processNode(value); }); } }); } calculateMaximums(node, key, value) { if (key === this.ACTUAL_ROWS_PROP && this._maxRows < value) { this._maxRows = value; } if (key === this.ACTUAL_COST_PROP && this._maxCost < value) { this._maxCost = value; } if (key === this.ACTUAL_DURATION_PROP && this._maxDuration < value) { this._maxDuration = value; } } findOutlierNodes(node) { node[this.SLOWEST_NODE_PROP] = false; node[this.LARGEST_NODE_PROP] = false; node[this.COSTLIEST_NODE_PROP] = false; if (node[this.ACTUAL_COST_PROP] === this._maxCost) { node[this.COSTLIEST_NODE_PROP] = true; } if (node[this.ACTUAL_ROWS_PROP] === this._maxRows) { node[this.LARGEST_NODE_PROP] = true; } if (node[this.ACTUAL_DURATION_PROP] === this._maxDuration) { node[this.SLOWEST_NODE_PROP] = true; } _.each(node, (value, key) => { if (key === this.PLANS_PROP) { _.each(value, (value) => { this.findOutlierNodes(value); }); } }); } // actual duration and actual cost are calculated by subtracting child values from the total calculateActuals(node) { node[this.ACTUAL_DURATION_PROP] = node[this.ACTUAL_TOTAL_TIME_PROP]; node[this.ACTUAL_COST_PROP] = node[this.TOTAL_COST_PROP]; console.log (node); _.each(node.Plans, subPlan => { console.log('processing chldren', subPlan) // since CTE scan duration is already included in its subnodes, it should be be // subtracted from the duration of this node if (subPlan[this.NODE_TYPE_PROP] !== this.CTE_SCAN_PROP) { node[this.ACTUAL_DURATION_PROP] = node[this.ACTUAL_DURATION_PROP] - subPlan[this.ACTUAL_TOTAL_TIME_PROP]; node[this.ACTUAL_COST_PROP] = node[this.ACTUAL_COST_PROP] - subPlan[this.TOTAL_COST_PROP]; } }); if (node[this.ACTUAL_COST_PROP] < 0) { node[this.ACTUAL_COST_PROP] = 0; } // since time is reported for an invidual loop, actual duration must be adjusted by number of loops node[this.ACTUAL_DURATION_PROP] = node[this.ACTUAL_DURATION_PROP] * node[this.ACTUAL_LOOPS_PROP]; } // figure out order of magnitude by which the planner mis-estimated how many rows would be // invloved in this node calculatePlannerEstimate(node) { node[this.PLANNER_ESTIMATE_FACTOR] = node[this.ACTUAL_ROWS_PROP] / node[this.PLAN_ROWS_PROP]; node[this.PLANNER_ESIMATE_DIRECTION] = EstimateDirection.under; if (node[this.PLANNER_ESTIMATE_FACTOR] < 1) { node[this.PLANNER_ESIMATE_DIRECTION] = EstimateDirection.over; node[this.PLANNER_ESTIMATE_FACTOR] = node[this.PLAN_ROWS_PROP] / node[this.ACTUAL_ROWS_PROP]; } } }