From 5310ac7d8eb1838a6297117bc7f9fca70291f46a Mon Sep 17 00:00:00 2001 From: Alex Tatiyants Date: Sun, 3 Jan 2016 17:17:48 -0800 Subject: initial commit --- app/components/app/app.html | 6 ++ app/components/app/app.ts | 22 ++++ app/components/plan-list/plan-list.html | 38 +++++++ app/components/plan-list/plan-list.ts | 47 +++++++++ app/components/plan-new/plan-new.html | 18 ++++ app/components/plan-new/plan-new.ts | 26 +++++ app/components/plan-node/plan-node.html | 50 +++++++++ app/components/plan-node/plan-node.ts | 176 ++++++++++++++++++++++++++++++++ app/components/plan-view/plan-view.html | 72 +++++++++++++ app/components/plan-view/plan-view.ts | 96 +++++++++++++++++ 10 files changed, 551 insertions(+) create mode 100644 app/components/app/app.html create mode 100644 app/components/app/app.ts create mode 100644 app/components/plan-list/plan-list.html create mode 100644 app/components/plan-list/plan-list.ts create mode 100644 app/components/plan-new/plan-new.html create mode 100644 app/components/plan-new/plan-new.ts create mode 100644 app/components/plan-node/plan-node.html create mode 100644 app/components/plan-node/plan-node.ts create mode 100644 app/components/plan-view/plan-view.html create mode 100644 app/components/plan-view/plan-view.ts (limited to 'app/components') diff --git a/app/components/app/app.html b/app/components/app/app.html new file mode 100644 index 0000000..a8a1969 --- /dev/null +++ b/app/components/app/app.html @@ -0,0 +1,6 @@ + + diff --git a/app/components/app/app.ts b/app/components/app/app.ts new file mode 100644 index 0000000..f9c83e0 --- /dev/null +++ b/app/components/app/app.ts @@ -0,0 +1,22 @@ +import {Component, ViewEncapsulation} from 'angular2/core'; +import {RouteConfig, ROUTER_DIRECTIVES} from 'angular2/router'; + +import {PlanView} from '../plan-view/plan-view'; +import {PlanList} from '../plan-list/plan-list'; +import {PlanNew} from '../plan-new/plan-new'; + +@Component({ + selector: 'app', + templateUrl: './components/app/app.html', + encapsulation: ViewEncapsulation.None, + directives: [ROUTER_DIRECTIVES] +}) + +@RouteConfig([ + { path: '/', redirectTo: ['/PlanList'] }, + { path: '/plans', component: PlanList, name: 'PlanList' }, + { path: '/plans/new', component: PlanNew, name: 'PlanNew' }, + { path: '/plans/:id', component: PlanView, name: 'PlanView' } +]) + +export class App { } diff --git a/app/components/plan-list/plan-list.html b/app/components/plan-list/plan-list.html new file mode 100644 index 0000000..a2f1a9d --- /dev/null +++ b/app/components/plan-list/plan-list.html @@ -0,0 +1,38 @@ + + +
+
+ Welcome to PEV! Please submit a plan for visualization +
+ + + + + + + +
{{plan.name}}created on {{plan.createdOn | momentDate }} + +
+ + + +
+
+
diff --git a/app/components/plan-list/plan-list.ts b/app/components/plan-list/plan-list.ts new file mode 100644 index 0000000..888e2e2 --- /dev/null +++ b/app/components/plan-list/plan-list.ts @@ -0,0 +1,47 @@ +import {Component, OnInit} from 'angular2/core'; +import {ROUTER_DIRECTIVES} from 'angular2/router'; + +import {IPlan} from '../../interfaces/iplan'; +import {PlanService} from '../../services/plan-service'; +import {PlanNew} from '../plan-new/plan-new'; + +import {MomentDatePipe} from '../../pipes'; + +@Component({ + selector: 'plan-list', + templateUrl: './components/plan-list/plan-list.html', + providers: [PlanService], + directives: [ROUTER_DIRECTIVES, PlanNew], + pipes: [MomentDatePipe] +}) +export class PlanList { + plans: Array; + newPlanName: string; + newPlanContent: any; + newPlanId: string; + openDialog: boolean = false; + + constructor(private _planService: PlanService) { } + + ngOnInit() { + this.plans = this._planService.getPlans(); + } + + requestDelete() { + this.openDialog = true; + } + + deletePlan(plan) { + this.openDialog = false; + this._planService.deletePlan(plan); + this.plans = this._planService.getPlans(); + } + + cancelDelete() { + this.openDialog = false; + } + + deleteAllPlans() { + this._planService.deleteAllPlans(); + } +} diff --git a/app/components/plan-new/plan-new.html b/app/components/plan-new/plan-new.html new file mode 100644 index 0000000..f9babb6 --- /dev/null +++ b/app/components/plan-new/plan-new.html @@ -0,0 +1,18 @@ + + +
+ For best results, use EXPLAIN (ANALYZE, COSTS, VERBOSE, BUFFERS, FORMAT JSON) +
+ + +
+ + + +
diff --git a/app/components/plan-new/plan-new.ts b/app/components/plan-new/plan-new.ts new file mode 100644 index 0000000..4a748ea --- /dev/null +++ b/app/components/plan-new/plan-new.ts @@ -0,0 +1,26 @@ +import {Component, OnInit} from 'angular2/core'; +import {Router, ROUTER_DIRECTIVES} from 'angular2/router'; +import {IPlan} from '../../interfaces/iplan'; + +import {PlanService} from '../../services/plan-service'; + +@Component({ + selector: 'plan-new', + templateUrl: './components/plan-new/plan-new.html', + providers: [PlanService], + directives: [ROUTER_DIRECTIVES] +}) +export class PlanNew { + planIds: string[]; + newPlanName: string; + newPlanContent: string; + newPlanQuery: string; + newPlan: IPlan; + + constructor( private _router: Router, private _planService: PlanService) { } + + submitPlan() { + this.newPlan = this._planService.createPlan(this.newPlanName, this.newPlanContent, this.newPlanQuery); + this._router.navigate( ['PlanView', { id: this.newPlan.id }] ); + } +} diff --git a/app/components/plan-node/plan-node.html b/app/components/plan-node/plan-node.html new file mode 100644 index 0000000..49380a2 --- /dev/null +++ b/app/components/plan-node/plan-node.html @@ -0,0 +1,50 @@ +
+
+

{{node['Node Type'] | uppercase}}

+ {{duration}}{{durationUnit}} | + {{executionTimePercent}}% + +
+ +
on + {{node['Schema']}}.{{node['Relation Name']}} + ({{node['Alias']}}) +
+ +
by {{node['Group Key']}}
+
by {{node['Sort Key']}}
+
{{node['Join Type']}} join
+
using {{node['Index Name']}}
+ +
+ {{tag}} +
+ +
+
+ +
+ + {{viewOptions.highlightType}}: {{highlightValue | number:'.0-2'}} + +
+ +
+ over estimated rows + under estimated rows + by {{plannerRowEstimateValue}}x +
+ + + + + + +
{{prop.key}}{{prop.value}}
+
+ + diff --git a/app/components/plan-node/plan-node.ts b/app/components/plan-node/plan-node.ts new file mode 100644 index 0000000..3562ea6 --- /dev/null +++ b/app/components/plan-node/plan-node.ts @@ -0,0 +1,176 @@ +import {Component, OnInit} from 'angular2/core'; +import {HighlightType, EstimateDirection} from '../../enums'; +import {PlanService} from '../../services/plan-service'; +/// + +@Component({ + selector: 'plan-node', + inputs: ['node', 'planStats', 'viewOptions'], + templateUrl: './components/plan-node/plan-node.html', + directives: [PlanNode], + providers: [PlanService] +}) + +export class PlanNode { + // consts + MAX_WIDTH: number = 220; + MIN_ESTIMATE_MISS: number = 100; + COSTLY_TAG: string = 'costliest'; + SLOW_TAG: string = 'slowest'; + LARGE_TAG: string = 'largest'; + ESTIMATE_TAG: string = 'bad estimate'; + + // inputs + node: any; + planStats: any; + viewOptions: any; + + // calculated properties + duration: string; + durationUnit: string; + executionTimePercent: number; + backgroundColor: string; + highlightValue: number; + width: number; + props: Array; + tags: Array; + plannerRowEstimateValue: number; + plannerRowEstimateDirection: EstimateDirection; + currentHighlightType: string; + + // expose enum to view + estimateDirections = EstimateDirection; + highlightTypes = HighlightType; + + constructor(private _planService: PlanService) { } + + ngOnInit() { + this.currentHighlightType = this.viewOptions.highlightType; + this.calculateBar(); + this.calculateProps(); + this.calculateDuration(); + this.calculateTags(); + + this.plannerRowEstimateDirection = this.node[this._planService.PLANNER_ESIMATE_DIRECTION]; + this.plannerRowEstimateValue = _.round(this.node[this._planService.PLANNER_ESTIMATE_FACTOR]); + } + + ngDoCheck() { + // console.log("check", this.currentHighlightType, this.viewOptions.highlightType); + if (this.currentHighlightType !== this.viewOptions.highlightType) { + this.currentHighlightType = this.viewOptions.highlightType; + this.calculateBar(); + } + } + + calculateBar() { + switch (this.currentHighlightType) { + case HighlightType.DURATION: + this.highlightValue = (this.node[this._planService.ACTUAL_DURATION_PROP]); + this.width = Math.round((this.highlightValue / this.planStats.maxDuration) * this.MAX_WIDTH); + break; + case HighlightType.ROWS: + this.highlightValue = (this.node[this._planService.ACTUAL_ROWS_PROP]); + this.width = Math.round((this.highlightValue / this.planStats.maxRows) * this.MAX_WIDTH); + break; + case HighlightType.COST: + this.highlightValue = (this.node[this._planService.ACTUAL_COST_PROP]); + this.width = Math.round((this.highlightValue / this.planStats.maxCost) * this.MAX_WIDTH); + break; + } + + if (this.width < 1) { this.width = 1 } + this.backgroundColor = this.numberToColorHsl(1 - this.width / this.MAX_WIDTH); + } + + calculateDuration() { + var dur: number = _.round(this.node[this._planService.ACTUAL_DURATION_PROP]); + // convert duration into approriate units + if (dur < 1) { + this.duration = "<1"; + this.durationUnit = 'ms'; + } else if (dur > 1 && dur < 1000) { + this.duration = dur.toString(); + this.durationUnit = 'ms'; + } else { + this.duration = _.round(dur / 1000, 2).toString(); + this.durationUnit = 'mins'; + } + this.executionTimePercent = (_.round((dur / this.planStats.executionTime) * 100)); + } + + // create an array of node propeties so that they can be displayed in the view + calculateProps() { + this.props = _.chain(this.node) + .omit('Plans') + .map((value, key) => { + return { key: key, value: value }; + }) + .value(); + } + + calculateTags() { + this.tags = []; + if (this.node[this._planService.SLOWEST_NODE_PROP]) { + this.tags.push(this.SLOW_TAG); + } + if (this.node[this._planService.COSTLIEST_NODE_PROP]) { + this.tags.push(this.COSTLY_TAG); + } + if (this.node[this._planService.LARGEST_NODE_PROP]) { + this.tags.push(this.LARGE_TAG); + } + if (this.node[this._planService.PLANNER_ESTIMATE_FACTOR] >= this.MIN_ESTIMATE_MISS) { + this.tags.push(this.ESTIMATE_TAG); + } + } + + /** + * http://stackoverflow.com/questions/2353211/hsl-to-rgb-color-conversion + * + * Converts an HSL color value to RGB. Conversion formula + * adapted from http://en.wikipedia.org/wiki/HSL_color_space. + * Assumes h, s, and l are contained in the set [0, 1] and + * returns r, g, and b in the set [0, 255]. + * + * @param Number h The hue + * @param Number s The saturation + * @param Number l The lightness + * @return Array The RGB representation + */ + hslToRgb(h, s, l) { + var r, g, b; + + if (s == 0) { + r = g = b = l; // achromatic + } else { + function hue2rgb(p, q, t) { + if (t < 0) t += 1; + if (t > 1) t -= 1; + if (t < 1 / 6) return p + (q - p) * 6 * t; + if (t < 1 / 2) return q; + if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; + return p; + } + + var q = l < 0.5 ? l * (1 + s) : l + s - l * s; + var p = 2 * l - q; + r = hue2rgb(p, q, h + 1 / 3); + g = hue2rgb(p, q, h); + b = hue2rgb(p, q, h - 1 / 3); + } + + return [Math.floor(r * 255), Math.floor(g * 255), Math.floor(b * 255)]; + } + + // convert a number to a color using hsl + numberToColorHsl(i) { + // as the function expects a value between 0 and 1, and red = 0° and green = 120° + // we convert the input to the appropriate hue value + var hue = i * 100 * 1.2 / 360; + // we convert hsl to rgb (saturation 100%, lightness 50%) + var rgb = this.hslToRgb(hue, .9, .4); + // we format to css value and return + return 'rgb(' + rgb[0] + ',' + rgb[1] + ',' + rgb[2] + ')'; + } +} diff --git a/app/components/plan-view/plan-view.html b/app/components/plan-view/plan-view.html new file mode 100644 index 0000000..ab9b7dc --- /dev/null +++ b/app/components/plan-view/plan-view.html @@ -0,0 +1,72 @@ + + + + +
+
+
+ {{executionTime}} + execution time ({{executionTimeUnit}}) +
+
+ {{planStats.planningTime | number:'.0-2'}} + planning time (ms) +
+
+ {{planStats.maxDuration | number:'.0-2'}} + slowest node (ms) +
+
+ {{planStats.maxRows | number:'.0-2'}} + largest node (rows) +
+
+ {{planStats.maxCost | number:'.0-2'}} + costliest node +
+
+ +
+
    +
  • + +
  • +
+
+
diff --git a/app/components/plan-view/plan-view.ts b/app/components/plan-view/plan-view.ts new file mode 100644 index 0000000..ab4e6a3 --- /dev/null +++ b/app/components/plan-view/plan-view.ts @@ -0,0 +1,96 @@ +import {Component, OnInit} from 'angular2/core'; +import {RouteParams} from 'angular2/router'; +import {ROUTER_DIRECTIVES} from 'angular2/router'; + +import {IPlan} from '../../interfaces/iplan'; +import {PlanService} from '../../services/plan-service'; +import {HighlightType} from '../../enums'; +import {PlanNode} from '../plan-node/plan-node'; + +@Component({ + selector: 'plan-view', + templateUrl: './components/plan-view/plan-view.html', + directives: [ROUTER_DIRECTIVES, PlanNode], + providers: [PlanService] +}) +export class PlanView { + id: string; + plan: IPlan; + rootContainer: any; + executionTime: string; + executionTimeUnit: string; + hideMenu: boolean = true; + + planStats: any = { + executionTime: 0, + maxRows: 0, + maxCost: 0, + maxDuration: 0 + }; + + viewOptions: any = { + showPlanStats: true, + showHighlightBar: true, + showPlannerEstimate: false, + showTags: true, + highlightType: HighlightType.NONE + }; + + showPlannerEstimate: boolean = true; + showMenu: boolean = false; + + highlightTypes = HighlightType; // exposing the enum to the view + + constructor(private _planService: PlanService, routeParams: RouteParams) { + this.id = routeParams.get('id'); + } + + getPlan() { + if (!this.id) { + return; + } + + this.plan = this._planService.getPlan(this.id); + this.rootContainer = this.plan.content; + + var executionTime: number = this.rootContainer['Execution Time'] || this.rootContainer['Total Runtime']; + [this.executionTime, this.executionTimeUnit] = this.calculateDuration(executionTime); + + this.planStats = { + executionTime: executionTime, + planningTime: this.rootContainer['Planning Time'], + maxRows: this.rootContainer[this._planService.MAXIMUM_ROWS_PROP], + maxCost: this.rootContainer[this._planService.MAXIMUM_COSTS_PROP], + maxDuration: this.rootContainer[this._planService.MAXIMUM_DURATION_PROP] + } + } + + ngOnInit() { + this.getPlan(); + } + + toggleHighlight(type: HighlightType) { + this.viewOptions.highlightType = type; + } + + analyzePlan() { + this._planService.analyzePlan(this.plan); + } + + calculateDuration(originalValue: number) { + var duration: string = ''; + var unit: string = ''; + + if (originalValue < 1) { + duration = "<1"; + unit = 'ms'; + } else if (originalValue > 1 && originalValue < 1000) { + duration = originalValue.toString(); + unit = 'ms'; + } else { + duration = _.round(originalValue / 1000, 2).toString(); + unit = 'mins'; + } + return [duration, unit]; + } +} -- cgit v1.2.3