/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { ExecutionPlanPropertiesViewBase, PropertiesSortType } from 'sql/workbench/contrib/executionPlan/browser/executionPlanPropertiesViewBase'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import * as azdata from 'azdata'; import { localize } from 'vs/nls'; import { iconCssFormatter, textFormatter } from 'sql/base/browser/ui/table/formatters'; import { isString } from 'vs/base/common/types'; import { removeLineBreaks } from 'sql/base/common/strings'; import * as DOM from 'vs/base/browser/dom'; import { InternalExecutionPlanElement } from 'sql/workbench/contrib/executionPlan/browser/azdataGraphView'; import { executionPlanComparisonPropertiesDifferent } from 'sql/workbench/contrib/executionPlan/browser/constants'; import * as sqlExtHostType from 'sql/workbench/api/common/sqlExtHostTypes'; import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Codicon } from 'vs/base/common/codicons'; import { deepClone } from 'vs/base/common/objects'; export enum ExecutionPlanCompareOrientation { Horizontal = 'horizontal', Vertical = 'vertical' } function getTopOperationLabel(target: string): string { return localize('nodePropertyViewTopOperation', 'Top operation: {0}', target); } function getBottomOperationLabel(target: string): string { return localize('nodePropertyViewBottomOperation', 'Bottom operation: {0}', target); } function getLeftOperationLabel(target: string): string { return localize('nodePropertyViewLeftOperation', 'Left operation: {0}', target); } function getRightOperationLabel(target: string): string { return localize('nodePropertyViewRightOperation', 'Right operation: {0}', target); } const notEqualTitle = localize('nodePropertyViewNameNotEqualTitle', 'Not equal to'); const lessThanTitle = localize('nodePropertyViewNameLessThanTitle', 'Less than'); const greaterThanTitle = localize('nodePropertyViewNameGreaterThanTitle', 'Greater than'); const equivalentPropertiesRowHeader = localize('nodePropertyViewNameEquivalentPropertiesRowHeader', 'Equivalent Properties'); const topTitleColumnHeader = localize('nodePropertyViewNameValueColumnTopHeader', "Value (Top Plan)"); const leftTitleColumnHeader = localize('nodePropertyViewNameValueColumnLeftHeader', "Value (Left Plan)"); const rightTitleColumnHeader = localize('nodePropertyViewNameValueColumnRightHeader', "Value (Right Plan)"); const bottomTitleColumnHeader = localize('nodePropertyViewNameValueColumnBottomHeader', "Value (Bottom Plan)"); export class ExecutionPlanComparisonPropertiesView extends ExecutionPlanPropertiesViewBase { private _model: ExecutionPlanComparisonPropertiesViewModel; private _primaryContainer: HTMLElement; private _secondaryContainer: HTMLElement; private _orientation: ExecutionPlanCompareOrientation = ExecutionPlanCompareOrientation.Horizontal; private _primaryTarget: string; private _secondaryTarget: string; public constructor( parentContainer: HTMLElement, @IThemeService themeService: IThemeService, @IInstantiationService instantiationService: IInstantiationService, @IContextMenuService contextMenuService: IContextMenuService, @IContextViewService contextViewService: IContextViewService ) { super(parentContainer, themeService, instantiationService, contextMenuService, contextViewService); this._model = {}; this._parentContainer.style.display = 'none'; const header = DOM.$('.compare-operation-name'); this._primaryContainer = DOM.$('.compare-operation-name-text'); header.appendChild(this._primaryContainer); this._secondaryContainer = DOM.$('.compare-operation-name-text'); header.appendChild(this._secondaryContainer); this.setHeader(header); } public setPrimaryElement(e: InternalExecutionPlanElement): void { this._model.primaryElement = e; if ((e).name) { this._primaryTarget = removeLineBreaks((e).name); } else { this._primaryTarget = localize('executionPlanPropertiesEdgeOperationName', "Edge"); } const primaryTitleText = this._orientation === ExecutionPlanCompareOrientation.Horizontal ? getTopOperationLabel(this._primaryTarget) : getLeftOperationLabel(this._primaryTarget); this._primaryContainer.innerText = primaryTitleText; this._primaryContainer.title = primaryTitleText; this.refreshPropertiesTable(); } public setSecondaryElement(e: InternalExecutionPlanElement): void { this._model.secondaryElement = e; if ((e)?.name) { this._secondaryTarget = removeLineBreaks((e).name); } else { this._secondaryTarget = localize('executionPlanPropertiesEdgeOperationName', "Edge"); } const secondaryTitleText = this._orientation === ExecutionPlanCompareOrientation.Horizontal ? getBottomOperationLabel(this._secondaryTarget) : getRightOperationLabel(this._secondaryTarget); this._secondaryContainer.innerText = secondaryTitleText; this._secondaryContainer.title = secondaryTitleText; this.refreshPropertiesTable(); } private updatePropertyContainerTitles(): void { const [primaryTitleText, secondaryTitleText] = this._orientation === ExecutionPlanCompareOrientation.Horizontal ? [getTopOperationLabel(this._primaryTarget), getBottomOperationLabel(this._secondaryTarget)] : [getLeftOperationLabel(this._primaryTarget), getRightOperationLabel(this._secondaryTarget)]; this._primaryContainer.innerText = primaryTitleText; this._primaryContainer.title = primaryTitleText; this._secondaryContainer.innerText = secondaryTitleText; this._secondaryContainer.title = secondaryTitleText; this.updatePropertiesTableColumnHeaders(); } public updatePropertiesTableColumnHeaders() { const columns: Slick.Column[] = this.getPropertyTableColumns(); this.updateTableColumns(columns); } public refreshPropertiesTable() { const columns: Slick.Column[] = this.getPropertyTableColumns(); let primaryProps = []; if (this._model.primaryElement?.properties) { primaryProps = this._model.primaryElement.properties; } let secondaryProps = []; if (this._model.secondaryElement?.properties) { secondaryProps = this._model.secondaryElement.properties; } let tableRows = this.convertPropertiesToTableRows(primaryProps, secondaryProps); tableRows = this.sortPropertiesByDisplayValueEquivalency(tableRows); this.populateTable(columns, tableRows); } private getPropertyTableColumns() { const columns: Slick.Column[] = []; if (this._model.primaryElement) { columns.push({ id: 'name', name: localize('nodePropertyViewNameNameColumnHeader', "Name"), field: 'name', width: 200, headerCssClass: 'prop-table-header', formatter: textFormatter }); columns.push({ id: 'value1', name: getPropertyViewNameValueColumnTopHeaderForOrientation(this._orientation), field: 'primary', width: 150, headerCssClass: 'prop-table-header', formatter: textFormatter }); } if (this._model.secondaryElement) { columns.push({ id: 'comparison', name: '', field: 'icon', width: 40, headerCssClass: 'prop-table-header', formatter: iconCssFormatter }); columns.push({ id: 'value2', name: getPropertyViewNameValueColumnBottomHeaderForOrientation(this._orientation), field: 'secondary', width: 150, headerCssClass: 'prop-table-header', formatter: textFormatter }); } return columns; } public sortPropertiesAlphabetically(props: Map): Map { return new Map([...props.entries()].sort((a, b) => { if (!a[1]?.name && !b[1]?.name) { return 0; } else if (!a[1]?.name) { return -1; } else if (!b[1]?.name) { return 1; } else { return a[1].name.localeCompare(b[1].name); } })); } public sortPropertiesByImportance(props: Map): Map { return new Map([...props.entries()].sort((a, b) => { if (!a[1]?.displayOrder && !b[1]?.displayOrder) { return 0; } else if (!a[1]?.displayOrder) { return -1; } else if (!b[1]?.displayOrder) { return 1; } else { return a[1].displayOrder - b[1].displayOrder; } })); } /** * This method will sort properties by having those with different values appear at the top, * and similar values appearing at the bottom of the table. * * An example of this sort of sorting looks like this: * * Name Value (Top plan) Value (Bottom Plan) * ------------------------------------------------------------------------- * Compile Time 38 37 * CompileCpu 38 37 * CompileMemory 5816 6424 * Estimated Number of Rows 1000 1000 * Optimization Level FULL FULL * RetrievedFromCache false false * * @param rows An array of TableRows that contains all the properties that will be organized. * @returns A new array of TableRows with unequal values appearing at the top and equal values appearing at the bottom. */ public sortPropertiesByDisplayValueEquivalency(rows: TableRow[]): TableRow[] { const [unequalPropertyRows, equalPropertyRows] = this.splitEqualFromUnequalProperties(rows); const organizedPropertyRows: TableRow[] = [...unequalPropertyRows]; if (equalPropertyRows.length > 0) { const equivalentPropertiesRow: TableRow = new Object() as TableRow; equivalentPropertiesRow.name = equivalentPropertiesRowHeader; equivalentPropertiesRow.expanded = false; equivalentPropertiesRow.treeGridChildren = equalPropertyRows; organizedPropertyRows.push(equivalentPropertiesRow); } return organizedPropertyRows; } private splitEqualFromUnequalProperties(rows: TableRow[]): [TableRow[], TableRow[]] { const unequalRows: TableRow[] = []; const equalRows: TableRow[] = []; for (let row of rows) { const treeGridChildren = row.treeGridChildren; if (treeGridChildren?.length > 0) { const [unequalSubRows, equalSubRows] = this.splitEqualFromUnequalProperties(treeGridChildren); if (unequalSubRows.length > 0) { const currentRow = deepClone(row); currentRow.treeGridChildren = unequalSubRows; currentRow.expanded = true; currentRow.icon = { iconCssClass: executionPlanComparisonPropertiesDifferent, title: notEqualTitle }; unequalRows.push(currentRow); } if (equalSubRows.length > 0) { const currentRow = deepClone(row); currentRow.treeGridChildren = equalSubRows; equalRows.push(currentRow); } } else { const primary = row.primary; const secondary = row.secondary; if (primary && secondary && primary.text === secondary.title) { equalRows.push(row); } else { unequalRows.push(row); } } } return [unequalRows, equalRows]; } public sortPropertiesReverseAlphabetically(props: Map): Map { return new Map([...props.entries()].sort((a, b) => { if (!a[1]?.displayOrder && !b[1]?.displayOrder) { return 0; } else if (!a[1]?.displayOrder) { return -1; } else if (!b[1]?.displayOrder) { return 1; } else { return b[1].displayOrder - a[1].displayOrder; } })); } private convertPropertiesToTableRows(primaryNode: azdata.executionPlan.ExecutionPlanGraphElementProperty[], secondaryNode: azdata.executionPlan.ExecutionPlanGraphElementProperty[]): TableRow[] { const rows: TableRow[] = []; let propertiesMap: Map = new Map(); if (primaryNode) { primaryNode.forEach(p => { propertiesMap.set(p.name, { primaryProp: p, secondaryProp: undefined, displayOrder: p.displayOrder, name: p.name }); }); } if (secondaryNode) { secondaryNode.forEach(p => { if (propertiesMap.has(p.name)) { propertiesMap.get(p.name).secondaryProp = p; } else { propertiesMap.set(p.name, { primaryProp: undefined, secondaryProp: p, displayOrder: p.displayOrder, name: p.name }); } }); } switch (this.sortType) { case PropertiesSortType.DisplayOrder: propertiesMap = this.sortPropertiesByImportance(propertiesMap); break; case PropertiesSortType.Alphabetical: propertiesMap = this.sortPropertiesAlphabetically(propertiesMap); break; case PropertiesSortType.ReverseAlphabetical: propertiesMap = this.sortPropertiesReverseAlphabetically(propertiesMap); break; } propertiesMap.forEach((v, k) => { let row: TableRow = new Object() as TableRow; row.name = { text: k }; const primaryProp = v.primaryProp; const secondaryProp = v.secondaryProp; if (primaryProp && secondaryProp) { row.displayOrder = v.primaryProp.displayOrder; let diffIcon = new Object() as DiffIcon; if (v.primaryProp.displayValue !== v.secondaryProp.displayValue) { switch (v.primaryProp.dataType) { case sqlExtHostType.executionPlan.ExecutionPlanGraphElementPropertyDataType.Boolean: diffIcon.iconClass = executionPlanComparisonPropertiesDifferent; diffIcon.title = notEqualTitle; break; case sqlExtHostType.executionPlan.ExecutionPlanGraphElementPropertyDataType.Number: if (v.primaryProp.betterValue === sqlExtHostType.executionPlan.ExecutionPlanGraphElementPropertyBetterValue.None) { diffIcon.title = notEqualTitle; diffIcon.iconClass = executionPlanComparisonPropertiesDifferent; } else { diffIcon = (parseFloat(v.primaryProp.displayValue) > parseFloat(v.secondaryProp.displayValue)) ? { iconClass: Codicon.chevronRight.classNames, title: greaterThanTitle } : { iconClass: Codicon.chevronLeft.classNames, title: lessThanTitle }; } break; case sqlExtHostType.executionPlan.ExecutionPlanGraphElementPropertyDataType.String: diffIcon.iconClass = executionPlanComparisonPropertiesDifferent; diffIcon.title = notEqualTitle; break; default: diffIcon.iconClass = executionPlanComparisonPropertiesDifferent; diffIcon.title = notEqualTitle; break; } } row.primary = { text: removeLineBreaks(v.primaryProp.displayValue, ' ') }; row.icon = { iconCssClass: diffIcon.iconClass ?? '', title: diffIcon.title ?? '' }; row.secondary = { title: removeLineBreaks(v.secondaryProp.displayValue, ' '), }; if ((primaryProp && !isString(primaryProp.value)) || (secondaryProp && !isString(secondaryProp.value))) { const parentRowStyling = ' parent-row-styling'; row.name.iconCssClass = !row.name.iconCssClass ? parentRowStyling : row.name.iconCssClass + parentRowStyling; row.primary.iconCssClass = !row.primary.iconCssClass ? parentRowStyling : row.primary.iconCssClass + parentRowStyling; row.icon.iconCssClass = !row.icon.iconCssClass ? parentRowStyling : row.icon.iconCssClass + parentRowStyling; row.secondary.iconCssClass = !row.secondary.iconCssClass ? parentRowStyling : row.secondary.iconCssClass + parentRowStyling; } rows.push(row); const topPropValue = isString(primaryProp.value) ? undefined : primaryProp.value; const bottomPropValue = isString(secondaryProp.value) ? undefined : secondaryProp.value; row.treeGridChildren = this.convertPropertiesToTableRows(topPropValue, bottomPropValue); } else if (primaryProp && !secondaryProp) { row.displayOrder = v.primaryProp.displayOrder; row.primary = { text: v.primaryProp.displayValue }; row.icon = { iconCssClass: executionPlanComparisonPropertiesDifferent, title: notEqualTitle }; rows.push(row); if (!isString(primaryProp.value)) { row.name.iconCssClass += ` parent-row-styling`; row.primary.iconCssClass += ` parent-row-styling`; row.treeGridChildren = this.convertPropertiesToTableRows(primaryProp.value, undefined); } } else if (!primaryProp && secondaryProp) { row.displayOrder = v.secondaryProp.displayOrder; row.secondary = { title: v.secondaryProp.displayValue, iconCssClass: '' }; row.icon = { iconCssClass: executionPlanComparisonPropertiesDifferent, title: notEqualTitle }; rows.push(row); if (!isString(secondaryProp.value)) { row.name.iconCssClass += ` parent-row-styling`; row.secondary.iconCssClass += ` parent-row-styling`; row.treeGridChildren = this.convertPropertiesToTableRows(undefined, secondaryProp.value); } } }); return rows; } set orientation(value: ExecutionPlanCompareOrientation) { if (this._orientation === value) { return; } this._orientation = value; this.updatePropertyContainerTitles(); } } function getPropertyViewNameValueColumnTopHeaderForOrientation(orientation: ExecutionPlanCompareOrientation): string { if (orientation === ExecutionPlanCompareOrientation.Horizontal) { return topTitleColumnHeader; } else { return leftTitleColumnHeader; } } function getPropertyViewNameValueColumnBottomHeaderForOrientation(orientation: ExecutionPlanCompareOrientation): string { if (orientation === ExecutionPlanCompareOrientation.Horizontal) { return bottomTitleColumnHeader; } else { return rightTitleColumnHeader; } } export interface ExecutionPlanComparisonPropertiesViewModel { primaryElement: InternalExecutionPlanElement, secondaryElement: InternalExecutionPlanElement } interface TablePropertiesMapEntry { primaryProp: azdata.executionPlan.ExecutionPlanGraphElementProperty, secondaryProp: azdata.executionPlan.ExecutionPlanGraphElementProperty, displayOrder: number, name: string } interface DiffIcon { iconClass: string; title: string; } interface TableRow extends Slick.SlickData { displayOrder: number; icon: RowContent; name: RowContent | string; primary: RowContent; secondary: RowContent; expanded: boolean; treeGridChildren: TableRow[]; } interface RowContent { iconCssClass?: string; text?: string; title?: string; }