/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ class RunTimeInformation { runtimePerThreads: RuntimePerThread[]; public get actualRows(): number { let total = 0; if (this.runtimePerThreads) { this.runtimePerThreads.forEach(element => { total += element.actualRow; }); } return total; } public get actualExecutions(): number { let total = 0; if (this.runtimePerThreads) { this.runtimePerThreads.forEach(element => { total += element.actualExecutions; }); } return total; } } class RuntimePerThread { threadId: number; actualRow: number; actualExecutionMode: string; actualExecutions: number; } class IndexObject { database: string; schema: string; table: string; index: string; indexKind: string; public get title() { let title: string = ''; if (this.database && this.schema && this.table) { title = `${this.database}.${this.schema}.${this.table}.${this.index}`; if (this.indexKind && this.indexKind !== '') { title += `(${this.indexKind})`; } } return title; } } class PlanNode { root: PlanNode; subtreeCost: number; private childrenNodes: PlanNode[]; parent: PlanNode; physicalOp: string; logicalOp: string; id: number; estimateRows: string; estimateIo: string; estimateCpu: string; parallel: boolean; partitioned: boolean; estimateRewinds: string; estimateRebinds: string; runtimeInfo: RunTimeInformation; indexObject: IndexObject; public addChildren(children: PlanNode[]): void { if (children) { children.forEach(element => { element.parent = this; }); } this.childrenNodes = children; } public get totalSubTreeCost(): number { let total = this.subtreeCost; if (total === 0) { this.children.forEach(element => { total += element.subtreeCost; }); } return total; } public get children(): PlanNode[] { return this.childrenNodes; } public get cost(): number { let total = this.subtreeCost; if (this.children && total !== 0) { this.children.forEach(element => { total -= element.subtreeCost; }); } return total; } public get relativeCost(): number { let overallCost = this.root.totalSubTreeCost; return overallCost > 0 ? this.cost / overallCost : 0; } public get estimatedOperatorCost(): number { return Math.round(this.relativeCost * 100); } public get estimatedSubtreeCost(): number { let total = this.estimatedOperatorCost; if (this.children) { this.children.forEach(element => { total += element.estimatedSubtreeCost; }); } return total; } public get title(): string { if (this.physicalOp === this.logicalOp) { return this.physicalOp; } else { return `${this.physicalOp}(${this.logicalOp})`; } } public get treeViewPrefix(): string { return this.parent === undefined ? '' : `${this.parent.treeViewPrefix}-----`; } public get treeViewTitle(): string { return `${this.treeViewPrefix}${this.title}`; } } export class PlanXmlParser { parser: DOMParser = new DOMParser(); doc: Document; planXml: string; root: PlanNode; constructor(planXml: string) { this.doc = this.parser.parseFromString(planXml, 'application/xml'); this.planXml = planXml; let queryPlanNode = this.findChildren(this.doc.children[0], 'QueryPlan'); if (queryPlanNode && queryPlanNode.length > 0) { this.root = new PlanNode(); let ops = this.createPlanNodes(queryPlanNode[0], 'RelOp', this.root); this.root.addChildren(ops); this.root.subtreeCost = 0; } } public get topOperations(): PlanNode[] { let operations: PlanNode[] = []; if (this.root && this.root.children) { operations = this.addOperationsToList(operations, this.root.children); operations.sort((a, b) => { if (a.estimatedOperatorCost > b.estimatedOperatorCost) { return -1; } else if (a.estimatedOperatorCost <= b.estimatedOperatorCost) { return 1; } else { return 0; } }); } return operations; } public get toTreeViewList(): PlanNode[] { let operations: PlanNode[] = []; operations = this.addOperationsToList(operations, this.root.children); return operations; } private addOperationsToList(list: PlanNode[], nodes: PlanNode[]): PlanNode[] { list = list.concat(nodes); nodes.forEach(element => { list = this.addOperationsToList(list, element.children); }); return list; } private findChildren(element: Element, elementName: string, untilNode: string = undefined): Element[] { let elements: Element[] = []; if (element === undefined) { return undefined; } for (let index = 0; index < element.childNodes.length; index++) { if (element.childNodes[index].nodeName.toLocaleLowerCase() === elementName.toLocaleLowerCase()) { elements = elements.concat(element.children[index]); } } if (elements.length > 0) { return elements; } for (let index = 0; index < element.childNodes.length; index++) { if (untilNode && element.childNodes[index].nodeName === untilNode) { continue; } let result = this.findChildren(element.children[index], elementName, untilNode); if (result !== undefined) { return result; } } return undefined; } private createPlanNodes(element: Element, elementName: string, root: PlanNode): PlanNode[] { let nodePlans: PlanNode[] = []; let children = this.findChildren(element, elementName); if (children) { for (let index = 0; index < children.length; index++) { let childNode = children[index]; let planNode = this.convertToPlanNode(childNode); planNode.root = root; planNode.addChildren(this.createPlanNodes(childNode, elementName, root)); planNode.runtimeInfo = new RunTimeInformation(); planNode.indexObject = new IndexObject(); let runtimeInfoNodes = this.findChildren(childNode, 'RunTimeCountersPerThread'); if (runtimeInfoNodes) { planNode.runtimeInfo.runtimePerThreads = runtimeInfoNodes.map(x => this.convertToRuntimeInfo(x)); } let objectNodes = this.findChildren(childNode, 'Object', 'RelOp'); if (objectNodes && objectNodes.length > 0) { planNode.indexObject = this.convertToObject(objectNodes[0]); } nodePlans = nodePlans.concat(planNode); } } return nodePlans; } private convertToPlanNode(element: Element): PlanNode { let planNode = new PlanNode(); planNode.id = this.findAttribute(element.attributes, 'NodeId'); planNode.logicalOp = this.findAttribute(element.attributes, 'LogicalOp'); planNode.physicalOp = this.findAttribute(element.attributes, 'PhysicalOp'); planNode.subtreeCost = +this.findAttribute(element.attributes, 'EstimatedTotalSubtreeCost'); planNode.estimateRows = this.findAttribute(element.attributes, 'EstimateRows'); planNode.estimateCpu = this.findAttribute(element.attributes, 'EstimateCPU'); planNode.estimateIo = this.findAttribute(element.attributes, 'EstimateIO'); planNode.estimateRebinds = this.findAttribute(element.attributes, 'EstimateRebinds'); planNode.estimateRewinds = this.findAttribute(element.attributes, 'EstimateRewinds'); planNode.parallel = this.findAttribute(element.attributes, 'Parallel') === '1'; planNode.partitioned = this.findAttribute(element.attributes, 'Partitioned') === '1'; return planNode; } private convertToRuntimeInfo(element: Element): RuntimePerThread { let runtimeNode = new RuntimePerThread(); runtimeNode.actualExecutionMode = this.findAttribute(element.attributes, 'ActualExecutionMode'); runtimeNode.actualExecutions = +this.findAttribute(element.attributes, 'ActualExecutions'); runtimeNode.actualRow = +this.findAttribute(element.attributes, 'ActualRows'); runtimeNode.threadId = +this.findAttribute(element.attributes, 'Thread'); return runtimeNode; } private convertToObject(element: Element): IndexObject { let objectNode = new IndexObject(); objectNode.database = this.findAttribute(element.attributes, 'Database'); objectNode.index = this.findAttribute(element.attributes, 'Index'); objectNode.indexKind = this.findAttribute(element.attributes, 'IndexKind'); objectNode.schema = this.findAttribute(element.attributes, 'Schema'); objectNode.table = this.findAttribute(element.attributes, 'Table'); return objectNode; } private findAttribute(attributes: NamedNodeMap, attName: string): any { for (let index = 0; index < attributes.length; index++) { let attribute = attributes[index]; if (attribute.name === attName) { return attribute.value; } } } }