diff --git a/src/sql/base/browser/ui/table/formatters.ts b/src/sql/base/browser/ui/table/formatters.ts
index 07638f9160..44d9286c63 100644
--- a/src/sql/base/browser/ui/table/formatters.ts
+++ b/src/sql/base/browser/ui/table/formatters.ts
@@ -111,6 +111,17 @@ export function textFormatter(row: number | undefined, cell: any | undefined, va
valueToDisplay = escape(valueToDisplay.length > 250 ? valueToDisplay.slice(0, 250) + '...' : valueToDisplay);
titleValue = valueToDisplay;
}
+ else if (value && value.title) {
+ if (value.title) {
+ valueToDisplay = value.title;
+
+ if (value.style) {
+ cellStyle = value.style;
+ }
+ }
+ valueToDisplay = escape(valueToDisplay.length > 250 ? valueToDisplay.slice(0, 250) + '...' : valueToDisplay);
+ titleValue = valueToDisplay;
+ }
return `${valueToDisplay}`;
}
@@ -118,7 +129,7 @@ export function textFormatter(row: number | undefined, cell: any | undefined, va
export function iconCssFormatter(row: number | undefined, cell: any | undefined, value: any, columnDef: any | undefined, dataContext: any | undefined): string {
if (isCssIconCellValue(value)) {
- return `
`;
+ return ``;
}
return textFormatter(row, cell, value, columnDef, dataContext);
}
diff --git a/src/sql/base/common/numbers.ts b/src/sql/base/common/numbers.ts
new file mode 100644
index 0000000000..4607bf2319
--- /dev/null
+++ b/src/sql/base/common/numbers.ts
@@ -0,0 +1,8 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the Source EULA. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+export function isNumber(text: string): boolean {
+ return !isNaN(parseInt(text)) && !isNaN(parseFloat(text));
+}
diff --git a/src/sql/workbench/contrib/executionPlan/browser/executionPlanComparisonPropertiesView.ts b/src/sql/workbench/contrib/executionPlan/browser/executionPlanComparisonPropertiesView.ts
index 9799751c0c..7343bfdb61 100644
--- a/src/sql/workbench/contrib/executionPlan/browser/executionPlanComparisonPropertiesView.ts
+++ b/src/sql/workbench/contrib/executionPlan/browser/executionPlanComparisonPropertiesView.ts
@@ -5,18 +5,20 @@
import { ExecutionPlanPropertiesViewBase, PropertiesSortType } from 'sql/workbench/contrib/executionPlan/browser/executionPlanPropertiesViewBase';
import { IThemeService } from 'vs/platform/theme/common/themeService';
+import { isNumber } from 'sql/base/common/numbers';
import * as azdata from 'azdata';
import { localize } from 'vs/nls';
-import { textFormatter } from 'sql/base/browser/ui/table/formatters';
+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, executionPlanComparisonPropertiesUpArrow, executionPlanComparisonPropertiesDownArrow } from 'sql/workbench/contrib/executionPlan/browser/constants';
+import { executionPlanComparisonPropertiesDifferent } from 'sql/workbench/contrib/executionPlan/browser/constants';
import * as sqlExtHostType from 'sql/workbench/api/common/sqlExtHostTypes';
-import { TextWithIconColumn } from 'sql/base/browser/ui/table/plugins/textWithIconColumn';
import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
+import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfigurationService';
+import { Codicon } from 'vs/base/common/codicons';
export enum ExecutionPlanCompareOrientation {
Horizontal = 'horizontal',
@@ -39,6 +41,26 @@ function getRightOperationLabel(target: string): string {
return localize('nodePropertyViewRightOperation', 'Right operation: {0}', target);
}
+function getTopPlanIsGreaterThanBottomPlanSummaryTextTemplate(rowName: string): string {
+ return localize('nodePropertyViewTopPlanGreaterThanBottomPlan', '{0} is greater for the top plan than it is for the bottom plan.', rowName);
+}
+
+function getBottomPlanIsGreaterThanTopPlanSummaryTextTemplate(rowName: string): string {
+ return localize('nodePropertyViewBottomPlanGreaterThanTopPlan', '{0} is greater for the bottom plan than it is for the top plan.', rowName);
+}
+
+function getLeftPlanIsGreaterThanRightPlanSummaryTextTemplate(rowName: string): string {
+ return localize('nodePropertyViewLeftPlanGreaterThanRightPlan', '{0} is greater for the left plan than it is for the right plan.', rowName);
+}
+
+function getRightPlanIsGreaterThanLeftPlanSummaryTextTemplate(rowName: string): string {
+ return localize('nodePropertyViewRightPlanGreaterThanLeftPlan', '{0} is greater for the right plan than it is for the left plan.', rowName);
+}
+
+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)");
@@ -46,6 +68,7 @@ const bottomTitleColumnHeader = localize('nodePropertyViewNameValueColumnBottomH
export class ExecutionPlanComparisonPropertiesView extends ExecutionPlanPropertiesViewBase {
private _model: ExecutionPlanComparisonPropertiesViewModel;
+ private _summaryTextContainer: HTMLElement;
private _primaryContainer: HTMLElement;
private _secondaryContainer: HTMLElement;
private _orientation: ExecutionPlanCompareOrientation = ExecutionPlanCompareOrientation.Horizontal;
@@ -57,19 +80,34 @@ export class ExecutionPlanComparisonPropertiesView extends ExecutionPlanProperti
@IThemeService themeService: IThemeService,
@IInstantiationService instantiationService: IInstantiationService,
@IContextMenuService contextMenuService: IContextMenuService,
- @IContextViewService contextViewService: IContextViewService
+ @IContextViewService contextViewService: IContextViewService,
+ @ITextResourcePropertiesService private readonly textResourcePropertiesService: ITextResourcePropertiesService
) {
super(parentContainer, themeService, instantiationService, contextMenuService, contextViewService);
this._model = {};
this._parentContainer.style.display = 'none';
const header = DOM.$('.compare-operation-name');
+
+ this._summaryTextContainer = DOM.$('.compare-operation-summary-text');
+ this.setSummary(this._summaryTextContainer);
+
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);
}
+ private setSummaryElement(summary: string[]): void {
+ const EOL = this.textResourcePropertiesService.getEOL(undefined);
+ let summaryText = summary.join(EOL);
+ let summaryContainerText = localize('executionPlanSummaryForExpensiveOperators', "Summary: {0}{1}", EOL, summaryText);
+ this._summaryTextContainer.innerText = summaryContainerText;
+ this._summaryTextContainer.title = summaryContainerText;
+ }
+
public setPrimaryElement(e: InternalExecutionPlanElement): void {
this._model.primaryElement = e;
if ((e).name) {
@@ -134,7 +172,60 @@ export class ExecutionPlanComparisonPropertiesView extends ExecutionPlanProperti
if (this._model.secondaryElement?.properties) {
secondaryProps = this._model.secondaryElement.properties;
}
- this.populateTable(columns, this.convertPropertiesToTableRows(primaryProps, secondaryProps));
+
+ const tableRows = this.convertPropertiesToTableRows(primaryProps, secondaryProps);
+ this.setSummaryElement(this.getExpensivePropertySummary(tableRows));
+ this.populateTable(columns, tableRows);
+ }
+
+ /**
+ * This method returns an array of strings that that will make up the properties summary. The properties summary
+ * will appear above the properties table when execution plans are being compared.
+ * Each segment of that summary is in the following generic format:
+ *
+ * is greater for the top plan than it is for the bottom plan.
+ * is greater for the bottom plan than it is for the top plan.
+ *
+ * @param tableRows The table rows that will appear in the properties table.
+ * @returns The string array containing the segments of the summary.
+ */
+ private getExpensivePropertySummary(tableRows: { [key: string]: string }[]): string[] {
+ let summary: string[] = [];
+
+ tableRows.forEach(row => {
+ const rowName = row.name['text'];
+ if (row.primary && row.secondary) {
+ const primaryText = row.primary['text'].split(' ');
+ const secondaryTitle = row.secondary['title'].split(' ');
+
+ if (primaryText.length === secondaryTitle.length && primaryText.length <= 2 && secondaryTitle.length <= 2) {
+ const MAX_PROPERTY_SUMMARY_LENGTH = 3;
+
+ for (let i = 0; i < primaryText.length && summary.length < MAX_PROPERTY_SUMMARY_LENGTH; ++i) {
+ if (isNumber(primaryText[i]) && isNumber(secondaryTitle[i])) {
+ const primaryValue = Number(primaryText);
+ const secondaryValue = Number(secondaryTitle);
+
+ let summaryText: string;
+ if (primaryValue > secondaryValue) {
+ summaryText = this._orientation === ExecutionPlanCompareOrientation.Horizontal
+ ? getTopPlanIsGreaterThanBottomPlanSummaryTextTemplate(rowName)
+ : getLeftPlanIsGreaterThanRightPlanSummaryTextTemplate(rowName);
+ }
+ else {
+ summaryText = this._orientation === ExecutionPlanCompareOrientation.Horizontal
+ ? getBottomPlanIsGreaterThanTopPlanSummaryTextTemplate(rowName)
+ : getRightPlanIsGreaterThanLeftPlanSummaryTextTemplate(rowName);
+ }
+
+ summary.push(summaryText);
+ }
+ }
+ }
+ }
+ });
+
+ return summary;
}
private getPropertyTableColumns() {
@@ -159,13 +250,23 @@ export class ExecutionPlanComparisonPropertiesView extends ExecutionPlanProperti
});
}
if (this._model.secondaryElement) {
- columns.push(new TextWithIconColumn({
+ 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',
- }).definition);
+ formatter: textFormatter
+ });
}
return columns;
}
@@ -188,16 +289,69 @@ export class ExecutionPlanComparisonPropertiesView extends ExecutionPlanProperti
return new Map([...props.entries()].sort((a, b) => {
if (!a[1]?.displayOrder && !b[1]?.displayOrder) {
return 0;
- } else if (!a[1]?.displayOrder) {
+ }
+ else if (!a[1]?.displayOrder) {
return -1;
- } else if (!b[1]?.displayOrder) {
+ }
+ else if (!b[1]?.displayOrder) {
return 1;
- } else {
+ }
+ 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 props Map of properties that will be organized.
+ * @returns A new map with different values appearing at the top and similar values appearing at the bottom.
+ */
+ public sortPropertiesByDisplayValueEquivalency(props: Map, sortProperties: (props: Map) => Map): Map {
+ let unequalProperties: Map = new Map();
+ let equalProperties: Map = new Map();
+
+ [...props.entries()].forEach(prop => {
+ const [rowKey, rowEntry] = prop;
+ const primaryProp = rowEntry.primaryProp;
+ const secondaryProp = rowEntry.secondaryProp;
+
+ if (primaryProp?.displayValue.localeCompare(secondaryProp?.displayValue) === 0) {
+ equalProperties.set(rowKey, rowEntry);
+ }
+ else {
+ unequalProperties.set(rowKey, rowEntry);
+ }
+ });
+
+ unequalProperties = sortProperties(unequalProperties);
+ equalProperties = sortProperties(equalProperties);
+
+ let map: Map = new Map();
+ unequalProperties.forEach((v, k) => {
+ map.set(k, v);
+ });
+
+ equalProperties.forEach((v, k) => {
+ map.set(k, v);
+ });
+
+ return map;
+ }
+
public sortPropertiesReverseAlphabetically(props: Map): Map {
return new Map([...props.entries()].sort((a, b) => {
if (!a[1]?.displayOrder && !b[1]?.displayOrder) {
@@ -244,13 +398,13 @@ export class ExecutionPlanComparisonPropertiesView extends ExecutionPlanProperti
switch (this.sortType) {
case PropertiesSortType.DisplayOrder:
- propertiesMap = this.sortPropertiesByImportance(propertiesMap);
+ propertiesMap = this.sortPropertiesByDisplayValueEquivalency(propertiesMap, this.sortPropertiesByImportance);
break;
case PropertiesSortType.Alphabetical:
- propertiesMap = this.sortPropertiesAlphabetically(propertiesMap);
+ propertiesMap = this.sortPropertiesByDisplayValueEquivalency(propertiesMap, this.sortPropertiesAlphabetically);
break;
case PropertiesSortType.ReverseAlphabetical:
- propertiesMap = this.sortPropertiesReverseAlphabetically(propertiesMap);
+ propertiesMap = this.sortPropertiesByDisplayValueEquivalency(propertiesMap, this.sortPropertiesReverseAlphabetically);
break;
}
@@ -263,37 +417,56 @@ export class ExecutionPlanComparisonPropertiesView extends ExecutionPlanProperti
const primaryProp = v.primaryProp;
const secondaryProp = v.secondaryProp;
let diffIconClass = '';
+
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:
- diffIconClass = executionPlanComparisonPropertiesDifferent;
+ diffIcon.iconClass = executionPlanComparisonPropertiesDifferent;
+ diffIcon.title = notEqualTitle;
break;
case sqlExtHostType.executionPlan.ExecutionPlanGraphElementPropertyDataType.Number:
- diffIconClass = (parseFloat(v.primaryProp.displayValue) > parseFloat(v.secondaryProp.displayValue)) ? executionPlanComparisonPropertiesDownArrow : executionPlanComparisonPropertiesUpArrow;
+ 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:
- diffIconClass = executionPlanComparisonPropertiesDifferent;
+ diffIcon.iconClass = executionPlanComparisonPropertiesDifferent;
+ diffIcon.title = notEqualTitle;
break;
default:
- diffIconClass = executionPlanComparisonPropertiesDifferent;
+ diffIcon.iconClass = executionPlanComparisonPropertiesDifferent;
+ diffIcon.title = notEqualTitle;
break;
}
}
+
row['primary'] = {
text: removeLineBreaks(v.primaryProp.displayValue, ' ')
};
+
+ row['icon'] = {
+ iconCssClass: diffIcon.iconClass,
+ title: diffIcon.title
+ };
+
row['secondary'] = {
- iconCssClass: diffIconClass,
title: removeLineBreaks(v.secondaryProp.displayValue, ' ')
};
+
if ((primaryProp && !isString(primaryProp.value)) || (secondaryProp && !isString(secondaryProp.value))) {
row['name'].iconCssClass += ` parent-row-styling`;
row['primary'].iconCssClass += ` parent-row-styling`;
+ row['icon'].iconCssClass += 'parent-row-styling';
row['secondary'].iconCssClass += ` parent-row-styling`;
}
+
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);
@@ -324,7 +497,28 @@ export class ExecutionPlanComparisonPropertiesView extends ExecutionPlanProperti
}
});
- return rows;
+
+ let formattedRows: { [key: string]: string }[] = [];
+ let equalRows: { [key: string]: string }[] = [];
+ for (const [_, row] of Object.entries(rows)) {
+ if (row.primary && row.secondary && row.primary['text'] === row.secondary['title']) {
+ equalRows.push(row);
+ }
+ else {
+ formattedRows.push(row);
+ }
+ }
+
+ if (equalRows.length > 0) {
+ let equalRow = {};
+ equalRow['name'] = equivalentPropertiesRowHeader;
+ equalRow['expanded'] = false;
+ equalRow['treeGridChildren'] = equalRows;
+
+ formattedRows.push(equalRow);
+ }
+
+ return formattedRows;
}
set orientation(value: ExecutionPlanCompareOrientation) {
@@ -365,3 +559,8 @@ interface TablePropertiesMapEntry {
displayOrder: number,
name: string
}
+
+interface DiffIcon {
+ iconClass: string;
+ title: string;
+}
diff --git a/src/sql/workbench/contrib/executionPlan/browser/executionPlanPropertiesViewBase.ts b/src/sql/workbench/contrib/executionPlan/browser/executionPlanPropertiesViewBase.ts
index 7570e5063b..0e8a1adb5b 100644
--- a/src/sql/workbench/contrib/executionPlan/browser/executionPlanPropertiesViewBase.ts
+++ b/src/sql/workbench/contrib/executionPlan/browser/executionPlanPropertiesViewBase.ts
@@ -43,6 +43,9 @@ export abstract class ExecutionPlanPropertiesViewBase extends Disposable impleme
// Header container
private _headerContainer: HTMLElement;
+ // Summary container
+ private _summaryContainer: HTMLElement;
+
// Properties actions
private _headerActionsContainer!: HTMLElement;
private _headerActions: ActionBar;
@@ -110,6 +113,9 @@ export abstract class ExecutionPlanPropertiesViewBase extends Disposable impleme
this._headerContainer = DOM.$('.header');
propertiesContent.appendChild(this._headerContainer);
+ this._summaryContainer = DOM.$('.summary');
+ propertiesContent.appendChild(this._summaryContainer);
+
this._searchAndActionBarContainer = DOM.$('.search-action-bar-container');
propertiesContent.appendChild(this._searchAndActionBarContainer);
@@ -222,6 +228,10 @@ export abstract class ExecutionPlanPropertiesViewBase extends Disposable impleme
this._headerContainer.appendChild(c);
}
+ public setSummary(c: HTMLElement): void {
+ this._summaryContainer.appendChild(c);
+ }
+
public set tableHeight(value: number) {
if (this.tableHeight !== value) {
this._tableHeight = value;
@@ -264,6 +274,7 @@ export abstract class ExecutionPlanPropertiesViewBase extends Disposable impleme
private resizeTable(): void {
const spaceOccupied = (this._titleBarContainer.getBoundingClientRect().height
+ this._headerContainer.getBoundingClientRect().height
+ + this._summaryContainer.getBoundingClientRect().height
+ this._headerActionsContainer.getBoundingClientRect().height);
this.tableHeight = (this._parentContainer.getBoundingClientRect().height - spaceOccupied - 15);
diff --git a/src/sql/workbench/contrib/executionPlan/browser/media/executionPlan.css b/src/sql/workbench/contrib/executionPlan/browser/media/executionPlan.css
index 25c0feef7d..cce859a877 100644
--- a/src/sql/workbench/contrib/executionPlan/browser/media/executionPlan.css
+++ b/src/sql/workbench/contrib/executionPlan/browser/media/executionPlan.css
@@ -597,7 +597,7 @@ However we always want it to be the width of the container it is resizing.
background-position: center;
background-repeat: no-repeat;
width: 16px;
- height: auto;
+ height: 16px;
}
.vs-dark .eps-container .ep-properties-different,
@@ -607,9 +607,18 @@ However we always want it to be the width of the container it is resizing.
background-position: center;
background-repeat: no-repeat;
width: 16px;
- height: auto;
+ height: 16px;
}
+.eps-container .comparison-editor .plan-comparison-container .properties .properties-content .table-container .table .monaco-table .ui-widget .slick-viewport .grid-canvas .ui-widget-content.slick-row .slick-cell .grid-cell-value-container.icon.codicon.slick-icon-cell-content.codicon-chevron-left,
+.eps-container .comparison-editor .plan-comparison-container .properties .properties-content .table-container .table .monaco-table .ui-widget .slick-viewport .grid-canvas .ui-widget-content.slick-row .slick-cell .grid-cell-value-container.icon.codicon.slick-icon-cell-content.codicon-chevron-right {
+ background-size: 16px 16px;
+ background-position: center;
+ background-repeat: no-repeat;
+ width: 16px;
+ height: 16px;
+ margin-left: -28px;
+}
.eps-container .ep-properties-down-arrow {
background-image: url(../images/actionIcons/downArrow.svg);
@@ -617,7 +626,7 @@ However we always want it to be the width of the container it is resizing.
background-position: center;
background-repeat: no-repeat;
width: 16px;
- height: auto;
+ height: 16px;
}
.vs-dark .eps-container .ep-properties-down-arrow,
@@ -627,7 +636,7 @@ However we always want it to be the width of the container it is resizing.
background-position: center;
background-repeat: no-repeat;
width: 16px;
- height: auto;
+ height: 16px;
}
.eps-container .ep-properties-up-arrow {
@@ -636,7 +645,7 @@ However we always want it to be the width of the container it is resizing.
background-position: center;
background-repeat: no-repeat;
width: 16px;
- height: auto;
+ height: 16px;
}
.vs-dark .eps-container .ep-properties-up-arrow,
@@ -646,7 +655,7 @@ However we always want it to be the width of the container it is resizing.
background-position: center;
background-repeat: no-repeat;
width: 16px;
- height: auto;
+ height: 16px;
}
.eps-container .ep-top-operations {