diff --git a/extensions/mssql/config.json b/extensions/mssql/config.json index 0ec9c8fe7a..013b247f57 100644 --- a/extensions/mssql/config.json +++ b/extensions/mssql/config.json @@ -1,6 +1,6 @@ { "downloadUrl": "https://github.com/Microsoft/sqltoolsservice/releases/download/{#version#}/microsoft.sqltools.servicelayer-{#fileName#}", - "version": "4.3.0.30", + "version": "4.3.0.32", "downloadFileNames": { "Windows_86": "win-x86-net6.0.zip", "Windows_64": "win-x64-net6.0.zip", diff --git a/extensions/mssql/package.json b/extensions/mssql/package.json index 599f32cb53..09d7a35212 100644 --- a/extensions/mssql/package.json +++ b/extensions/mssql/package.json @@ -261,6 +261,29 @@ "default": false, "description": "%mssql.intelliSense.lowerCaseSuggestions%" }, + "mssql.executionPlan.expensiveOperationMetric": { + "type": "string", + "description": "%mssql.executionPlan.expensiveOperationMetric%", + "default": "cost", + "enum": [ + "off", + "actualElapsedTime", + "actualElapsedCpuTime", + "cost", + "subtreeCost", + "actualNumberOfRowsForAllExecutions", + "numberOfRowsRead" + ], + "enumDescriptions": [ + "%mssql.executionPlan.expensiveOperationMetric.off%", + "%mssql.executionPlan.expensiveOperationMetric.actualElapsedTime%", + "%mssql.executionPlan.expensiveOperationMetric.actualElapsedCpuTime%", + "%mssql.executionPlan.cost%", + "%mssql.executionPlan.subtreeCost%", + "%mssql.executionPlan.actualNumberOfRowsForAllExecutions%", + "%mssql.executionPlan.numberOfRowsRead%" + ] + }, "mssql.query.rowCount": { "type": "number", "default": 0, diff --git a/extensions/mssql/package.nls.json b/extensions/mssql/package.nls.json index 91e45eedaa..8c7d32fe5c 100644 --- a/extensions/mssql/package.nls.json +++ b/extensions/mssql/package.nls.json @@ -39,6 +39,14 @@ "mssql.disabled": "Disabled", "mssql.enabled": "Enabled", + "mssql.executionPlan.expensiveOperationMetric": "The default metric to use to highlight an expensive operation in query execution plans", + "mssql.executionPlan.expensiveOperationMetric.off": "Expensive operation highlighting will be turned off for execution plans.", + "mssql.executionPlan.expensiveOperationMetric.actualElapsedTime": "Highlights the execution plan operation that took the most time.", + "mssql.executionPlan.expensiveOperationMetric.actualElapsedCpuTime": "Highlights the execution plan operation that used the most CPU time.", + "mssql.executionPlan.cost": "Highlights the execution plan operation with the highest cost.", + "mssql.executionPlan.subtreeCost": "Highlights the execution plan operation with the highest subtree cost.", + "mssql.executionPlan.actualNumberOfRowsForAllExecutions": "Highlights the execution plan operation with the greatest actual number of rows for all executions.", + "mssql.executionPlan.numberOfRowsRead": "Highlights the execution plan operation with the greatest number of rows read.", "mssql.exportNotebookToSql": "Export Notebook as SQL", "mssql.exportSqlAsNotebook": "Export SQL as Notebook", "mssql.configuration.title": "MSSQL configuration", diff --git a/package.json b/package.json index 2a48ef6271..620bde08f8 100755 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ "angular2-grid": "2.0.6", "ansi_up": "^5.1.0", "applicationinsights": "1.0.8", - "azdataGraph": "github:Microsoft/azdataGraph#0.0.42", + "azdataGraph": "github:Microsoft/azdataGraph#0.0.45", "chart.js": "^2.9.4", "chokidar": "3.5.1", "graceful-fs": "4.2.8", diff --git a/remote/package.json b/remote/package.json index 7e84be5f55..875e81e0e6 100755 --- a/remote/package.json +++ b/remote/package.json @@ -17,7 +17,7 @@ "applicationinsights": "1.0.8", "angular2-grid": "2.0.6", "ansi_up": "^5.1.0", - "azdataGraph": "github:Microsoft/azdataGraph#0.0.42", + "azdataGraph": "github:Microsoft/azdataGraph#0.0.45", "chart.js": "^2.9.4", "cookie": "^0.4.0", "graceful-fs": "4.2.8", diff --git a/remote/web/package.json b/remote/web/package.json index 17846a39d7..ab6f1df06e 100755 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -15,7 +15,7 @@ "@vscode/vscode-languagedetection": "1.0.21", "angular2-grid": "2.0.6", "ansi_up": "^5.1.0", - "azdataGraph": "github:Microsoft/azdataGraph#0.0.42", + "azdataGraph": "github:Microsoft/azdataGraph#0.0.45", "chart.js": "^2.9.4", "gridstack": "^3.1.3", "kburtram-query-plan": "2.6.1", diff --git a/remote/web/yarn.lock b/remote/web/yarn.lock index 55183e7089..643fcea9b0 100644 --- a/remote/web/yarn.lock +++ b/remote/web/yarn.lock @@ -150,9 +150,9 @@ array-uniq@^1.0.2: resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" integrity sha1-r2rId6Jcx/dOBYiUdThY39sk/bY= -"azdataGraph@github:Microsoft/azdataGraph#0.0.42": - version "0.0.42" - resolved "https://codeload.github.com/Microsoft/azdataGraph/tar.gz/86f72ee9c9ea78a31c9ad2f402fb24d40e50c75b" +"azdataGraph@github:Microsoft/azdataGraph#0.0.45": + version "0.0.45" + resolved "https://codeload.github.com/Microsoft/azdataGraph/tar.gz/8675ac6dfc60aad331ccbf7e4145b9eaa25e7304" chalk@^2.3.0, chalk@^2.4.1: version "2.4.2" diff --git a/remote/yarn.lock b/remote/yarn.lock index 283cede2a7..93062c2717 100644 --- a/remote/yarn.lock +++ b/remote/yarn.lock @@ -198,9 +198,9 @@ array-uniq@^1.0.2: resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" integrity sha1-r2rId6Jcx/dOBYiUdThY39sk/bY= -"azdataGraph@github:Microsoft/azdataGraph#0.0.42": - version "0.0.42" - resolved "https://codeload.github.com/Microsoft/azdataGraph/tar.gz/86f72ee9c9ea78a31c9ad2f402fb24d40e50c75b" +"azdataGraph@github:Microsoft/azdataGraph#0.0.45": + version "0.0.45" + resolved "https://codeload.github.com/Microsoft/azdataGraph/tar.gz/8675ac6dfc60aad331ccbf7e4145b9eaa25e7304" bindings@^1.5.0: version "1.5.0" diff --git a/src/sql/azdata.proposed.d.ts b/src/sql/azdata.proposed.d.ts index 26c870841e..97f7308cb2 100644 --- a/src/sql/azdata.proposed.d.ts +++ b/src/sql/azdata.proposed.d.ts @@ -1311,6 +1311,10 @@ declare module 'azdata' { * Time take by the node operation in milliseconds */ elapsedTimeInMs: number; + /** + * CPU time taken by the node operation in milliseconds + */ + elapsedCpuTimeInMs: number; /** * Node properties to be shown in the tooltip */ @@ -1351,6 +1355,21 @@ declare module 'azdata' { * Cost string for the node */ costDisplayString: string; + /** + * Cost metrics for the node + */ + costMetrics: CostMetric[]; + } + + export interface CostMetric { + /** + * Name of the cost metric. + */ + name: string; + /** + * The value of the cost metric + */ + value: number | undefined; } export interface ExecutionPlanBadge { diff --git a/src/sql/platform/telemetry/common/telemetryKeys.ts b/src/sql/platform/telemetry/common/telemetryKeys.ts index dc21bf84c5..c7783e8333 100644 --- a/src/sql/platform/telemetry/common/telemetryKeys.ts +++ b/src/sql/platform/telemetry/common/telemetryKeys.ts @@ -81,6 +81,7 @@ export const enum TelemetryAction { GeneratePreviewReport = 'GeneratePreviewReport', GetDataGridItems = 'GetDataGridItems', GetDataGridColumns = 'GetDataGridColumns', + HighlightExpensiveOperation = 'HighlightExpensiveOperation', ModelViewDashboardOpened = 'ModelViewDashboardOpened', ModalDialogClosed = 'ModalDialogClosed', ModalDialogOpened = 'ModalDialogOpened', diff --git a/src/sql/workbench/contrib/executionPlan/browser/azdataGraphView.ts b/src/sql/workbench/contrib/executionPlan/browser/azdataGraphView.ts index 782600e188..9ad51c7075 100644 --- a/src/sql/workbench/contrib/executionPlan/browser/azdataGraphView.ts +++ b/src/sql/workbench/contrib/executionPlan/browser/azdataGraphView.ts @@ -25,6 +25,7 @@ export class AzdataGraphView { private _diagram: any; private _diagramModel: AzDataGraphCell; private _cellInFocus: AzDataGraphCell; + public expensiveMetricTypes: Set = new Set(); private _graphElementPropertiesSet: Set = new Set(); @@ -230,6 +231,13 @@ export class AzdataGraphView { return resultNodes; } + public clearExpensiveOperatorHighlighting(): void { + this._diagram.clearExpensiveOperatorHighlighting(); + } + + public highlightExpensiveOperator(predicate: (cell: AzDataGraphCell) => number): boolean { + return this._diagram.highlightExpensiveOperator(predicate); + } /** * Brings a graph element to the center of the parent view. @@ -295,40 +303,65 @@ export class AzdataGraphView { if (!node.id.toString().startsWith(`element-`)) { node.id = `element-${node.id}`; } + + this.expensiveMetricTypes.add(ExpensiveMetricType.Off); + diagramNode.id = node.id; + diagramNode.icon = node.type; + diagramNode.metrics = this.populateProperties(node.properties); - if (node.type) { - diagramNode.icon = node.type; + diagramNode.badges = []; + for (let i = 0; node.badges && i < node.badges.length; i++) { + diagramNode.badges.push(this.getBadgeTypeString(node.badges[i].type)); } - if (node.properties) { - diagramNode.metrics = this.populateProperties(node.properties); + diagramNode.edges = this.populateEdges(node.edges); + + diagramNode.children = []; + for (let i = 0; node.children && i < node.children.length; ++i) { + diagramNode.children.push(this.populate(node.children[i])); } - if (node.badges) { - diagramNode.badges = []; - for (let i = 0; i < node.badges.length; i++) { - diagramNode.badges.push(this.getBadgeTypeString(node.badges[i].type)); - } + diagramNode.description = node.description; + diagramNode.cost = node.cost; + if (node.cost) { + this.expensiveMetricTypes.add(ExpensiveMetricType.Cost); } - if (node.edges) { - diagramNode.edges = this.populateEdges(node.edges); + diagramNode.subTreeCost = node.subTreeCost; + if (node.subTreeCost) { + this.expensiveMetricTypes.add(ExpensiveMetricType.SubtreeCost); } - if (node.children) { - diagramNode.children = []; - for (let i = 0; i < node.children.length; ++i) { - diagramNode.children.push(this.populate(node.children[i])); - } + diagramNode.relativeCost = node.relativeCost; + diagramNode.elapsedTimeInMs = node.elapsedTimeInMs; + if (node.elapsedTimeInMs) { + this.expensiveMetricTypes.add(ExpensiveMetricType.ActualElapsedTime); } - if (node.description) { - diagramNode.description = node.description; + let costMetrics = []; + for (let i = 0; node.costMetrics && i < node.costMetrics.length; ++i) { + costMetrics.push(node.costMetrics[i]); + + this.loadMetricTypesFromCostMetrics(node.costMetrics[i].name); } + diagramNode.costMetrics = costMetrics; + return diagramNode; } + private loadMetricTypesFromCostMetrics(costMetricName: string): void { + if (costMetricName === 'ElapsedCpuTime') { + this.expensiveMetricTypes.add(ExpensiveMetricType.ActualElapsedCpuTime); + } + else if (costMetricName === 'EstimateRowsAllExecs' || costMetricName === 'ActualRows') { + this.expensiveMetricTypes.add(ExpensiveMetricType.ActualNumberOfRowsForAllExecutions); + } + else if (costMetricName === 'EstimatedRowsRead' || costMetricName === 'ActualRowsRead') { + this.expensiveMetricTypes.add(ExpensiveMetricType.NumberOfRowsRead); + } + } + private getBadgeTypeString(badgeType: sqlExtHostType.executionPlan.BadgeType): { type: string, tooltip: string @@ -357,7 +390,11 @@ export class AzdataGraphView { } } - private populateProperties(props: azdata.executionPlan.ExecutionPlanGraphElementProperty[]): AzDataGraphCellMetric[] { + private populateProperties(props: azdata.executionPlan.ExecutionPlanGraphElementProperty[] | undefined): AzDataGraphCellMetric[] { + if (!props) { + return []; + } + props.forEach(p => { this._graphElementPropertiesSet.add(p.name); }); @@ -372,7 +409,11 @@ export class AzdataGraphView { }); } - private populateEdges(edges: InternalExecutionPlanEdge[]): AzDataGraphCellEdge[] { + private populateEdges(edges: InternalExecutionPlanEdge[] | undefined): AzDataGraphCellEdge[] { + if (!edges) { + return []; + } + return edges.map(e => { e.id = this.createGraphElementId(); return { @@ -473,6 +514,37 @@ export interface AzDataGraphCell { */ description: string; badges: AzDataGraphNodeBadge[]; + /** + * Cost associated with the node + */ + cost: number; + /** + * Cost of the node subtree + */ + subTreeCost: number; + /** + * Relative cost of the node compared to its siblings. + */ + relativeCost: number; + /** + * Time taken by the node operation in milliseconds + */ + elapsedTimeInMs: number; + /** + * cost metrics for the node + */ + costMetrics: CostMetric[]; +} + +export interface CostMetric { + /** + * Name of the cost metric. + */ + name: string; + /** + * The value of the cost metric + */ + value: number | undefined; } export interface AzDataGraphNodeBadge { @@ -529,6 +601,17 @@ export enum SearchType { LesserThanEqualTo, LesserAndGreaterThan } + +export enum ExpensiveMetricType { + Off = 'off', + ActualElapsedTime = 'actualElapsedTime', + ActualElapsedCpuTime = 'actualElapsedCpuTime', + Cost = 'cost', + SubtreeCost = 'subtreeCost', + ActualNumberOfRowsForAllExecutions = 'actualNumberOfRowsForAllExecutions', + NumberOfRowsRead = 'numberOfRowsRead' +} + export interface SearchQuery { /** * property name to be searched diff --git a/src/sql/workbench/contrib/executionPlan/browser/constants.ts b/src/sql/workbench/contrib/executionPlan/browser/constants.ts index d3a864faa0..a78a7eae4a 100644 --- a/src/sql/workbench/contrib/executionPlan/browser/constants.ts +++ b/src/sql/workbench/contrib/executionPlan/browser/constants.ts @@ -265,6 +265,7 @@ export const collapseExpandNodeIconPaths = { }; export const savePlanIconClassNames = 'ep-save-plan-icon'; +export const highlightExpensiveOperationClassNames = 'ep-highlight-expensive-operation-icon'; export const openPropertiesIconClassNames = 'ep-open-properties-icon'; export const openQueryIconClassNames = 'ep-open-query-icon'; export const openPlanFileIconClassNames = 'ep-open-plan-file-icon'; diff --git a/src/sql/workbench/contrib/executionPlan/browser/executionPlanView.ts b/src/sql/workbench/contrib/executionPlan/browser/executionPlanView.ts index 312d042dae..34f09c2e50 100644 --- a/src/sql/workbench/contrib/executionPlan/browser/executionPlanView.ts +++ b/src/sql/workbench/contrib/executionPlan/browser/executionPlanView.ts @@ -26,7 +26,7 @@ import { Progress } from 'vs/platform/progress/common/progress'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Action, Separator } from 'vs/base/common/actions'; import { localize } from 'vs/nls'; -import { customZoomIconClassNames, disableTooltipIconClassName, enableTooltipIconClassName, executionPlanCompareIconClassName, executionPlanTopOperations, openPlanFileIconClassNames, openPropertiesIconClassNames, openQueryIconClassNames, savePlanIconClassNames, searchIconClassNames, zoomInIconClassNames, zoomOutIconClassNames, zoomToFitIconClassNames } from 'sql/workbench/contrib/executionPlan/browser/constants'; +import * as constants from 'sql/workbench/contrib/executionPlan/browser/constants'; import { URI } from 'vs/base/common/uri'; import { VSBuffer } from 'vs/base/common/buffer'; import { CustomZoomWidget } from 'sql/workbench/contrib/executionPlan/browser/widgets/customZoomWidget'; @@ -37,6 +37,7 @@ import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys'; import { ExecutionPlanComparisonInput } from 'sql/workbench/contrib/executionPlan/browser/compareExecutionPlanInput'; import { ExecutionPlanFileView } from 'sql/workbench/contrib/executionPlan/browser/executionPlanFileView'; import { QueryResultsView } from 'sql/workbench/contrib/query/browser/queryResultsView'; +import { HighlightExpensiveOperationWidget } from 'sql/workbench/contrib/executionPlan/browser/widgets/highlightExpensiveNodeWidget'; export class ExecutionPlanView implements ISashLayoutProvider { @@ -66,6 +67,9 @@ export class ExecutionPlanView implements ISashLayoutProvider { // plan diagram public executionPlanDiagram: AzdataGraphView; + // previous expensive operator action selected + public previousExpensiveOperatorAction: Action; + public actionBarToggleTopTip: Action; public contextMenuToggleTooltipAction: Action; constructor( @@ -169,6 +173,7 @@ export class ExecutionPlanView implements ISashLayoutProvider { this._instantiationService.createInstance(SearchNodeAction, 'ActionBar'), this._instantiationService.createInstance(PropertiesAction, 'ActionBar'), this._instantiationService.createInstance(CompareExecutionPlanAction, 'ActionBar'), + this._instantiationService.createInstance(HighlightExpensiveOperationAction, 'ActionBar'), this.actionBarToggleTopTip ]; // Setting up context menu @@ -186,7 +191,9 @@ export class ExecutionPlanView implements ISashLayoutProvider { this._instantiationService.createInstance(SearchNodeAction, 'ContextMenu'), this._instantiationService.createInstance(PropertiesAction, 'ContextMenu'), this._instantiationService.createInstance(CompareExecutionPlanAction, 'ContextMenu'), - this.contextMenuToggleTooltipAction + this._instantiationService.createInstance(HighlightExpensiveOperationAction, 'ContextMenu'), + this.contextMenuToggleTooltipAction, + new Separator(), ]; if (this._queryResultsView) { @@ -320,7 +327,7 @@ export class OpenQueryAction extends Action { constructor(private source: ExecutionPlanActionSource, @IAdsTelemetryService private readonly telemetryService: IAdsTelemetryService ) { - super(OpenQueryAction.ID, OpenQueryAction.LABEL, openQueryIconClassNames); + super(OpenQueryAction.ID, OpenQueryAction.LABEL, constants.openQueryIconClassNames); } public override async run(context: ExecutionPlanView): Promise { @@ -340,7 +347,7 @@ export class PropertiesAction extends Action { constructor(private source: ExecutionPlanActionSource, @IAdsTelemetryService private readonly telemetryService: IAdsTelemetryService ) { - super(PropertiesAction.ID, PropertiesAction.LABEL, openPropertiesIconClassNames); + super(PropertiesAction.ID, PropertiesAction.LABEL, constants.openPropertiesIconClassNames); } public override async run(context: ExecutionPlanView): Promise { @@ -360,7 +367,7 @@ export class ZoomInAction extends Action { constructor(private source: ExecutionPlanActionSource, @IAdsTelemetryService private readonly telemetryService: IAdsTelemetryService ) { - super(ZoomInAction.ID, ZoomInAction.LABEL, zoomInIconClassNames); + super(ZoomInAction.ID, ZoomInAction.LABEL, constants.zoomInIconClassNames); } public override async run(context: ExecutionPlanView): Promise { @@ -380,7 +387,7 @@ export class ZoomOutAction extends Action { constructor(private source: ExecutionPlanActionSource, @IAdsTelemetryService private readonly telemetryService: IAdsTelemetryService ) { - super(ZoomOutAction.ID, ZoomOutAction.LABEL, zoomOutIconClassNames); + super(ZoomOutAction.ID, ZoomOutAction.LABEL, constants.zoomOutIconClassNames); } public override async run(context: ExecutionPlanView): Promise { @@ -400,7 +407,7 @@ export class ZoomToFitAction extends Action { constructor(private source: ExecutionPlanActionSource, @IAdsTelemetryService private readonly telemetryService: IAdsTelemetryService ) { - super(ZoomToFitAction.ID, ZoomToFitAction.LABEL, zoomToFitIconClassNames); + super(ZoomToFitAction.ID, ZoomToFitAction.LABEL, constants.zoomToFitIconClassNames); } public override async run(context: ExecutionPlanView): Promise { @@ -418,7 +425,7 @@ export class SavePlanFile extends Action { public static LABEL = localize('executionPlanSavePlanXML', "Save Plan File"); constructor() { - super(SavePlanFile.ID, SavePlanFile.LABEL, savePlanIconClassNames); + super(SavePlanFile.ID, SavePlanFile.LABEL, constants.savePlanIconClassNames); } public override async run(context: ExecutionPlanView): Promise { @@ -453,7 +460,7 @@ export class CustomZoomAction extends Action { constructor(private source: ExecutionPlanActionSource, @IAdsTelemetryService private readonly telemetryService: IAdsTelemetryService ) { - super(CustomZoomAction.ID, CustomZoomAction.LABEL, customZoomIconClassNames); + super(CustomZoomAction.ID, CustomZoomAction.LABEL, constants.customZoomIconClassNames); } public override async run(context: ExecutionPlanView): Promise { @@ -473,7 +480,7 @@ export class SearchNodeAction extends Action { constructor(private source: ExecutionPlanActionSource, @IAdsTelemetryService private readonly telemetryService: IAdsTelemetryService ) { - super(SearchNodeAction.ID, SearchNodeAction.LABEL, searchIconClassNames); + super(SearchNodeAction.ID, SearchNodeAction.LABEL, constants.searchIconClassNames); } public override async run(context: ExecutionPlanView): Promise { @@ -491,7 +498,7 @@ export class OpenPlanFile extends Action { public static Label = localize('executionPlanOpenGraphFile', "Show Query Plan XML"); //TODO: add a contribution point for providers to set this text constructor() { - super(OpenPlanFile.ID, OpenPlanFile.Label, openPlanFileIconClassNames); + super(OpenPlanFile.ID, OpenPlanFile.Label, constants.openPlanFileIconClassNames); } public override async run(context: ExecutionPlanView): Promise { @@ -505,17 +512,17 @@ export class ActionBarToggleTooltip extends Action { public static WHEN_TOOLTIPS_DISABLED_LABEL = localize('executionPlanDisableTooltip', "Tooltips disabled"); constructor() { - super(ActionBarToggleTooltip.ID, ActionBarToggleTooltip.WHEN_TOOLTIPS_ENABLED_LABEL, enableTooltipIconClassName); + super(ActionBarToggleTooltip.ID, ActionBarToggleTooltip.WHEN_TOOLTIPS_ENABLED_LABEL, constants.enableTooltipIconClassName); } public override async run(context: ExecutionPlanView): Promise { const state = context.executionPlanDiagram.toggleTooltip(); if (!state) { - this.class = disableTooltipIconClassName; + this.class = constants.disableTooltipIconClassName; this.label = ActionBarToggleTooltip.WHEN_TOOLTIPS_DISABLED_LABEL; context.contextMenuToggleTooltipAction.label = ContextMenuTooltipToggle.WHEN_TOOLTIPS_DISABLED_LABEL; } else { - this.class = enableTooltipIconClassName; + this.class = constants.enableTooltipIconClassName; this.label = ActionBarToggleTooltip.WHEN_TOOLTIPS_ENABLED_LABEL; context.contextMenuToggleTooltipAction.label = ContextMenuTooltipToggle.WHEN_TOOLTIPS_ENABLED_LABEL; } @@ -528,18 +535,18 @@ export class ContextMenuTooltipToggle extends Action { public static WHEN_TOOLTIPS_DISABLED_LABEL = localize('executionPlanContextMenuEnableTooltip', "Enable Tooltips"); constructor() { - super(ContextMenuTooltipToggle.ID, ContextMenuTooltipToggle.WHEN_TOOLTIPS_ENABLED_LABEL, enableTooltipIconClassName); + super(ContextMenuTooltipToggle.ID, ContextMenuTooltipToggle.WHEN_TOOLTIPS_ENABLED_LABEL, constants.enableTooltipIconClassName); } public override async run(context: ExecutionPlanView): Promise { const state = context.executionPlanDiagram.toggleTooltip(); if (!state) { this.label = ContextMenuTooltipToggle.WHEN_TOOLTIPS_DISABLED_LABEL; - context.actionBarToggleTopTip.class = disableTooltipIconClassName; + context.actionBarToggleTopTip.class = constants.disableTooltipIconClassName; context.actionBarToggleTopTip.label = ActionBarToggleTooltip.WHEN_TOOLTIPS_DISABLED_LABEL; } else { this.label = ContextMenuTooltipToggle.WHEN_TOOLTIPS_ENABLED_LABEL; - context.actionBarToggleTopTip.class = enableTooltipIconClassName; + context.actionBarToggleTopTip.class = constants.enableTooltipIconClassName; context.actionBarToggleTopTip.label = ActionBarToggleTooltip.WHEN_TOOLTIPS_ENABLED_LABEL; } } @@ -552,7 +559,7 @@ export class CompareExecutionPlanAction extends Action { constructor(private source: ExecutionPlanActionSource, @IAdsTelemetryService private readonly telemetryService: IAdsTelemetryService ) { - super(CompareExecutionPlanAction.COMPARE_PLAN, CompareExecutionPlanAction.COMPARE_PLAN, executionPlanCompareIconClassName); + super(CompareExecutionPlanAction.COMPARE_PLAN, CompareExecutionPlanAction.COMPARE_PLAN, constants.executionPlanCompareIconClassName); } public override async run(context: ExecutionPlanView): Promise { @@ -572,10 +579,30 @@ export class TopOperationsAction extends Action { constructor () { - super(TopOperationsAction.ID, TopOperationsAction.LABEL, executionPlanTopOperations); + super(TopOperationsAction.ID, TopOperationsAction.LABEL, constants.executionPlanTopOperations); } public override async run(context: ExecutionPlanView): Promise { context.openTopOperations(); } } + +export class HighlightExpensiveOperationAction extends Action { + public static ID = 'ep.highlightExpensiveOperation'; + public static LABEL = localize('executionPlanHighlightExpensiveOperationAction', 'Highlight Expensive Operation'); + + constructor(private source: ExecutionPlanActionSource, + @IAdsTelemetryService private readonly telemetryService: IAdsTelemetryService + ) { + super(HighlightExpensiveOperationAction.ID, HighlightExpensiveOperationAction.LABEL, constants.highlightExpensiveOperationClassNames); + } + + public override async run(context: ExecutionPlanView): Promise { + this.telemetryService + .createActionEvent(TelemetryKeys.TelemetryView.ExecutionPlan, TelemetryKeys.TelemetryAction.HighlightExpensiveOperation) + .withAdditionalProperties({ source: this.source }) + .send(); + + context.widgetController.toggleWidget(context._instantiationService.createInstance(HighlightExpensiveOperationWidget, context.widgetController, context.executionPlanDiagram)); + } +} diff --git a/src/sql/workbench/contrib/executionPlan/browser/executionPlanWidgetBase.ts b/src/sql/workbench/contrib/executionPlan/browser/executionPlanWidgetBase.ts index b4f8d5758a..04791432a1 100644 --- a/src/sql/workbench/contrib/executionPlan/browser/executionPlanWidgetBase.ts +++ b/src/sql/workbench/contrib/executionPlan/browser/executionPlanWidgetBase.ts @@ -3,13 +3,17 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -export abstract class ExecutionPlanWidgetBase { +import { Disposable } from 'vs/base/common/lifecycle'; + +export abstract class ExecutionPlanWidgetBase extends Disposable { /** * * @param container HTML Element that contains the UI for the plan action view. * @param identifier Uniquely identify the view to be added or removed. Note: Only 1 view with the same id can be added to the controller */ constructor(public container: HTMLElement, public identifier: string) { + super(); + this.container = container; this.identifier = identifier; } diff --git a/src/sql/workbench/contrib/executionPlan/browser/executionPlanWidgetController.ts b/src/sql/workbench/contrib/executionPlan/browser/executionPlanWidgetController.ts index bb0b2ffdc0..7076f93e2a 100644 --- a/src/sql/workbench/contrib/executionPlan/browser/executionPlanWidgetController.ts +++ b/src/sql/workbench/contrib/executionPlan/browser/executionPlanWidgetController.ts @@ -26,6 +26,7 @@ export class ExecutionPlanWidgetController { public removeWidget(widget: ExecutionPlanWidgetBase) { if (widget.identifier) { if (this._executionPlanWidgetMap.has(widget.identifier)) { + widget.dispose(); this._parentContainer.removeChild(this._executionPlanWidgetMap.get(widget.identifier).container); this._executionPlanWidgetMap.delete(widget.identifier); } else { diff --git a/src/sql/workbench/contrib/executionPlan/browser/images/actionIcons/highlightExpensiveOperation.svg b/src/sql/workbench/contrib/executionPlan/browser/images/actionIcons/highlightExpensiveOperation.svg new file mode 100644 index 0000000000..e3bbb6e3c5 --- /dev/null +++ b/src/sql/workbench/contrib/executionPlan/browser/images/actionIcons/highlightExpensiveOperation.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/sql/workbench/contrib/executionPlan/browser/images/actionIcons/highlightExpensiveOperationDark.svg b/src/sql/workbench/contrib/executionPlan/browser/images/actionIcons/highlightExpensiveOperationDark.svg new file mode 100644 index 0000000000..74af781dc9 --- /dev/null +++ b/src/sql/workbench/contrib/executionPlan/browser/images/actionIcons/highlightExpensiveOperationDark.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/src/sql/workbench/contrib/executionPlan/browser/media/executionPlan.css b/src/sql/workbench/contrib/executionPlan/browser/media/executionPlan.css index cce859a877..6e65ecef0e 100644 --- a/src/sql/workbench/contrib/executionPlan/browser/media/executionPlan.css +++ b/src/sql/workbench/contrib/executionPlan/browser/media/executionPlan.css @@ -92,11 +92,42 @@ However we always want it to be the width of the container it is resizing. width: 240px; } +/* Find expensive operation view */ +.eps-container .execution-plan .plan .plan-action-container .find-expensive-operation-widget { + display: flex; + flex-direction: row; + padding: 5px; + height: auto; + width: auto; +} + +.eps-container .execution-plan .plan .plan-action-container .find-expensive-operation-widget .select-container { + margin-left: 5px; + display: flex; + align-items: center; +} + +.eps-container .execution-plan .plan .plan-action-container .find-expensive-operation-widget .select-container expensive-operation-name-select-box-label { + margin-right: 5px; +} + +.eps-container .execution-plan .plan .plan-action-container .find-expensive-operation-widget .select-container>select { + height: 100%; +} + +.eps-container .execution-plan .plan .plan-action-container .find-expensive-operation-widget .monaco-button.monaco-text-button { + width: auto; + padding-left: 15px; + padding-right: 15px; +} + /* execution plan header that contains the relative query cost, query statement and recommendations */ .eps-container .execution-plan .plan .header, .top-operations-tab .top-operations-container .query-row { padding: 5px; font-weight: bolder; + margin-left: 5px; + margin-right: 5px; } /* execution plan header that contains the relative query cost, query statement and recommendations */ @@ -478,6 +509,21 @@ However we always want it to be the width of the container it is resizing. background-repeat: no-repeat; } +.eps-container .ep-highlight-expensive-operation-icon { + background-image: url(../images/actionIcons/highlightExpensiveOperation.svg); + background-size: 16px 16px; + background-position: center; + background-repeat: no-repeat; +} + +.vs-dark .eps-container .ep-highlight-expensive-operation-icon, +.hc-black .eps-container .ep-highlight-expensive-operation-icon { + background-image: url(../images/actionIcons/highlightExpensiveOperationDark.svg); + background-size: 16px 16px; + background-position: center; + background-repeat: no-repeat; +} + .eps-container .ep-enable-tooltip-icon { background-image: url(../images/actionIcons/enableTooltip.svg); background-size: 16px 16px; diff --git a/src/sql/workbench/contrib/executionPlan/browser/widgets/customZoomWidget.ts b/src/sql/workbench/contrib/executionPlan/browser/widgets/customZoomWidget.ts index b53e8a4f77..edc021d8f9 100644 --- a/src/sql/workbench/contrib/executionPlan/browser/widgets/customZoomWidget.ts +++ b/src/sql/workbench/contrib/executionPlan/browser/widgets/customZoomWidget.ts @@ -39,7 +39,7 @@ export class CustomZoomWidget extends ExecutionPlanWidgetBase { ariaLabel: zoomValueLabel, flexibleWidth: false }); - attachInputBoxStyler(this.customZoomInputBox, this.themeService); + this._register(attachInputBoxStyler(this.customZoomInputBox, this.themeService)); const currentZoom = this.executionPlanDiagram.getZoomLevel(); @@ -57,14 +57,14 @@ export class CustomZoomWidget extends ExecutionPlanWidgetBase { }; const applyButton = new Button(this.container, { - title: localize('customZoomApplyButtonTitle', "Apply Zoom (Enter)") + title: localize('customZoomApplyButtonTitle', "Apply Zoom") }); applyButton.setWidth('60px'); applyButton.label = localize('customZoomApplyButton', "Apply"); - applyButton.onDidClick(async e => { + this._register(applyButton.onDidClick(async e => { await new CustomZoomAction().run(self); - }); + })); // Adding action bar this._actionBar = new ActionBar(this.container); @@ -80,7 +80,7 @@ export class CustomZoomWidget extends ExecutionPlanWidgetBase { export class CustomZoomAction extends Action { public static ID = 'qp.customZoomAction'; - public static LABEL = localize('zoomAction', "Zoom (Enter)"); + public static LABEL = localize('zoomAction', "Zoom"); constructor() { super(CustomZoomAction.ID, CustomZoomAction.LABEL, zoomIconClassNames); @@ -101,7 +101,7 @@ export class CustomZoomAction extends Action { export class CancelZoom extends Action { public static ID = 'qp.cancelCustomZoomAction'; - public static LABEL = localize('cancelCustomZoomAction', "Close (Escape)"); + public static LABEL = localize('cancelCustomZoomAction', "Close"); constructor() { super(CancelZoom.ID, CancelZoom.LABEL, Codicon.chromeClose.classNames); diff --git a/src/sql/workbench/contrib/executionPlan/browser/widgets/highlightExpensiveNodeWidget.ts b/src/sql/workbench/contrib/executionPlan/browser/widgets/highlightExpensiveNodeWidget.ts new file mode 100644 index 0000000000..e5f60336df --- /dev/null +++ b/src/sql/workbench/contrib/executionPlan/browser/widgets/highlightExpensiveNodeWidget.ts @@ -0,0 +1,337 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ExecutionPlanWidgetBase } from 'sql/workbench/contrib/executionPlan/browser/executionPlanWidgetBase'; +import { ActionBar } from 'sql/base/browser/ui/taskbar/actionbar'; +import * as DOM from 'vs/base/browser/dom'; +import { localize } from 'vs/nls'; +import * as errors from 'vs/base/common/errors'; +import { Codicon } from 'vs/base/common/codicons'; +import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { attachSelectBoxStyler } from 'sql/platform/theme/common/styler'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { Action } from 'vs/base/common/actions'; +import { SelectBox } from 'sql/base/browser/ui/selectBox/selectBox'; +import { AzDataGraphCell, AzdataGraphView, ExpensiveMetricType } from 'sql/workbench/contrib/executionPlan/browser/azdataGraphView'; +import { ExecutionPlanWidgetController } from 'sql/workbench/contrib/executionPlan/browser/executionPlanWidgetController'; +import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; +import { Button } from 'sql/base/browser/ui/button/button'; +import { searchIconClassNames } from 'sql/workbench/contrib/executionPlan/browser/constants'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; + +const OFF_STRING = localize('executionPlanOff', 'Off'); +const ACTUAL_ELAPSED_TIME_STRING = localize('executionPlanActualElapsedTime', 'Actual Elapsed Time'); +const ACTUAL_ELAPSED_CPU_TIME_STRING = localize('executionPlanActualElapsedCpuTime', 'Actual Elapsed CPU Time'); +const COST_STRING = localize('executionPlanCost', 'Cost'); +const SUBTREE_COST_STRING = localize('executionPlanSubtreeCost', 'Subtree Cost'); +const ACTUAL_NUMBER_OF_ROWS_FOR_ALL_EXECUTIONS_STRING = localize('actualNumberOfRowsForAllExecutionsAction', 'Actual Number of Rows For All Executions'); +const NUMBER_OF_ROWS_READ_STRING = localize('executionPlanNumberOfRowsRead', 'Number of Rows Read'); + +export class HighlightExpensiveOperationWidget extends ExecutionPlanWidgetBase { + private _actionBar: ActionBar; + + public expenseMetricSelectBox: SelectBox; + private _expenseMetricSelectBoxContainer: HTMLElement; + private _selectedExpensiveOperationType: ExpensiveMetricType = ExpensiveMetricType.Cost; + + constructor( + public readonly widgetController: ExecutionPlanWidgetController, + public readonly executionPlanDiagram: AzdataGraphView, + @IContextViewService public readonly contextViewService: IContextViewService, + @IThemeService public readonly themeService: IThemeService, + @INotificationService public readonly notificationService: INotificationService, + @IConfigurationService private readonly _configurationService: IConfigurationService, + @IStorageService private readonly _storageService: IStorageService + ) { + super(DOM.$('.find-expensive-operation-widget'), 'findExpensiveOperation'); + + this.renderAndStyleWidget(); + } + + private getDefaultExpensiveOperationMetric(): ExpensiveMetricType { + const defaultMetricConfiguration = this._configurationService.getValue('mssql.executionPlan.expensiveOperationMetric'); + + switch (defaultMetricConfiguration) { + case 'actualElapsedTime': + return ExpensiveMetricType.ActualElapsedTime; + case 'actualElapsedCpuTime': + return ExpensiveMetricType.ActualElapsedCpuTime; + case 'cost': + return ExpensiveMetricType.Cost; + case 'subtreeCost': + return ExpensiveMetricType.SubtreeCost; + case 'actualNumberOfRowsForAllExecutions': + return ExpensiveMetricType.ActualNumberOfRowsForAllExecutions; + case 'numberOfRowsRead': + return ExpensiveMetricType.NumberOfRowsRead; + default: + return ExpensiveMetricType.Off; + } + } + + private renderAndStyleWidget(): void { + // Expensive Operation Dropdown + this._expenseMetricSelectBoxContainer = DOM.$('expensive-operation-name-select-box .dropdown-container'); + const operationLabel = DOM.$('expensive-operation-name-select-box-label'); + operationLabel.innerText = localize('expensiveOperationLabel', 'Metric:'); + + this._expenseMetricSelectBoxContainer.appendChild(operationLabel); + this.container.appendChild(this._expenseMetricSelectBoxContainer); + + const selectBoxOptions = this.getSelectBoxOptionsFromExecutionPlanDiagram(); + this.expenseMetricSelectBox = new SelectBox(selectBoxOptions, COST_STRING, this.contextViewService, this._expenseMetricSelectBoxContainer); + + this.expenseMetricSelectBox.render(this._expenseMetricSelectBoxContainer); + this._register(attachSelectBoxStyler(this.expenseMetricSelectBox, this.themeService)); + + this._expenseMetricSelectBoxContainer.style.width = '200px'; + this._expenseMetricSelectBoxContainer.style.marginRight = '5px'; + + this._register(this.expenseMetricSelectBox.onDidSelect(e => { + switch (e.selected) { + case ACTUAL_ELAPSED_TIME_STRING: + this._selectedExpensiveOperationType = ExpensiveMetricType.ActualElapsedTime; + break; + case ACTUAL_ELAPSED_CPU_TIME_STRING: + this._selectedExpensiveOperationType = ExpensiveMetricType.ActualElapsedCpuTime; + break; + case COST_STRING: + this._selectedExpensiveOperationType = ExpensiveMetricType.Cost; + break; + case SUBTREE_COST_STRING: + this._selectedExpensiveOperationType = ExpensiveMetricType.SubtreeCost; + break; + case ACTUAL_NUMBER_OF_ROWS_FOR_ALL_EXECUTIONS_STRING: + this._selectedExpensiveOperationType = ExpensiveMetricType.ActualNumberOfRowsForAllExecutions; + break; + case NUMBER_OF_ROWS_READ_STRING: + this._selectedExpensiveOperationType = ExpensiveMetricType.NumberOfRowsRead; + break; + default: + this._selectedExpensiveOperationType = ExpensiveMetricType.Off; + } + })); + + // Apply Button + const highlightExpensiveOperationAction = new HighlightExpensiveOperationAction(); + this._register(highlightExpensiveOperationAction); + + const clearHighlightExpensiveOperationAction = new TurnOffExpensiveHighlightingOperationAction(); + this._register(clearHighlightExpensiveOperationAction); + + const cancelHighlightExpensiveOperationAction = new CancelHIghlightExpensiveOperationAction(); + this._register(cancelHighlightExpensiveOperationAction); + + const self = this; + const applyButton = new Button(this.container, { + title: localize('highlightExpensiveOperationButtonTitle', 'Highlight Expensive Operation') + }); + applyButton.label = localize('highlightExpensiveOperationApplyButton', 'Apply'); + + this._register(applyButton.onDidClick(async e => { + if (this._selectedExpensiveOperationType === ExpensiveMetricType.Off) { + await clearHighlightExpensiveOperationAction.run(self); + } + else { + await highlightExpensiveOperationAction.run(self); + } + + this.showStoreDefaultMetricPrompt(); + })); + + // Adds Action bar + this._actionBar = new ActionBar(this.container); + this._actionBar.context = this; + this._actionBar.pushAction(cancelHighlightExpensiveOperationAction, { label: false, icon: true }); + } + + private getSelectBoxOptionsFromExecutionPlanDiagram(): string[] { + const selectBoxOptions: string[] = []; + + for (let expenseMetricType of this.executionPlanDiagram.expensiveMetricTypes) { + switch (expenseMetricType) { + case ExpensiveMetricType.Off: + selectBoxOptions.push(OFF_STRING); + break; + case ExpensiveMetricType.ActualElapsedTime: + selectBoxOptions.push(ACTUAL_ELAPSED_TIME_STRING); + break; + case ExpensiveMetricType.ActualElapsedCpuTime: + selectBoxOptions.push(ACTUAL_ELAPSED_CPU_TIME_STRING); + break; + case ExpensiveMetricType.Cost: + selectBoxOptions.push(COST_STRING); + break; + case ExpensiveMetricType.SubtreeCost: + selectBoxOptions.push(SUBTREE_COST_STRING); + break; + case ExpensiveMetricType.ActualNumberOfRowsForAllExecutions: + selectBoxOptions.push(ACTUAL_NUMBER_OF_ROWS_FOR_ALL_EXECUTIONS_STRING); + break; + case ExpensiveMetricType.NumberOfRowsRead: + selectBoxOptions.push(NUMBER_OF_ROWS_READ_STRING); + break; + } + } + + return selectBoxOptions; + } + + public showStoreDefaultMetricPrompt(): void { + const currentDefaultExpensiveOperationMetric = this.getDefaultExpensiveOperationMetric(); + if (this._selectedExpensiveOperationType === currentDefaultExpensiveOperationMetric || !this._storageService.getBoolean('qp.expensiveOperationMetric.showChangeDefaultExpensiveMetricPrompt', StorageScope.GLOBAL, true)) { + return; + } + + const infoMessage = localize('queryExecutionPlan.showUpdateDefaultMetricInfo', 'Set chosen metric as the default for query execution plans?'); + const promptChoices = [ + { + label: localize('qp.expensiveOperationMetric.yes', 'Yes'), + run: () => this._configurationService.updateValue('mssql.executionPlan.expensiveOperationMetric', this._selectedExpensiveOperationType.toString()).catch(e => errors.onUnexpectedError(e)) + }, + { + label: localize('qp.expensiveOperationMetric.no', 'No'), + run: () => { } + }, + { + label: localize('qp.expensiveOperationMetric.dontShowAgain', "Don't Show Again"), + run: () => this._storageService.store('qp.expensiveOperationMetric.showChangeDefaultExpensiveMetricPrompt', false, StorageScope.GLOBAL, StorageTarget.USER) + } + ]; + + this.notificationService.prompt(Severity.Info, infoMessage, promptChoices, { sticky: true }); + } + + public focus() { + this.expenseMetricSelectBox.focus(); + } + + public getExpensiveOperationDelegate(): (cell: AzDataGraphCell) => number | undefined { + const getElapsedTimeInMs = (cell: AzDataGraphCell): number | undefined => cell.elapsedTimeInMs; + + const getElapsedCpuTimeInMs = (cell: AzDataGraphCell): number | undefined => { + const elapsedCpuMetric = cell.costMetrics.find(m => m.name === 'ElapsedCpuTime'); + + if (elapsedCpuMetric === undefined) { + return undefined; + } + else { + return Number(elapsedCpuMetric.value); + } + + }; + + const getCost = (cell: AzDataGraphCell): number | undefined => cell.cost; + const getSubtreeCost = (cell: AzDataGraphCell): number | undefined => cell.subTreeCost; + + const getRowsForAllExecutions = (cell: AzDataGraphCell): number | undefined => { + const actualRowsMetric = cell.costMetrics.find(m => m.name === 'ActualRows'); + const estimateRowsForAllExecutionsMetric = cell.costMetrics.find(m => m.name === 'EstimateRowsAllExecs'); + + if (actualRowsMetric === undefined && estimateRowsForAllExecutionsMetric === undefined) { + return undefined; + } + + let result = Number(actualRowsMetric?.value); + if (!result) { + result = Number(estimateRowsForAllExecutionsMetric?.value); + } + + if (isNaN(result)) { + return undefined; + } + + return result; + }; + + const getNumberOfRowsRead = (cell: AzDataGraphCell): number | undefined => { + const actualRowsReadMetric = cell.costMetrics.find(m => m.name === 'ActualRowsRead'); + const estimatedRowsReadMetric = cell.costMetrics.find(m => m.name === 'EstimatedRowsRead'); + + if (actualRowsReadMetric === undefined && estimatedRowsReadMetric === undefined) { + return undefined; + } + + let result = Number(actualRowsReadMetric?.value); + if (!result) { + result = Number(estimatedRowsReadMetric?.value); + } + + if (isNaN(result)) { + return undefined; + } + + return result; + }; + + let expensiveOperationDelegate = getCost; + switch (this._selectedExpensiveOperationType) { + case ExpensiveMetricType.ActualElapsedTime: + expensiveOperationDelegate = getElapsedTimeInMs; + break; + case ExpensiveMetricType.ActualElapsedCpuTime: + expensiveOperationDelegate = getElapsedCpuTimeInMs; + break; + case ExpensiveMetricType.SubtreeCost: + expensiveOperationDelegate = getSubtreeCost; + break; + case ExpensiveMetricType.ActualNumberOfRowsForAllExecutions: + expensiveOperationDelegate = getRowsForAllExecutions; + break; + case ExpensiveMetricType.NumberOfRowsRead: + expensiveOperationDelegate = getNumberOfRowsRead; + break; + } + + return expensiveOperationDelegate; + } +} + +export class HighlightExpensiveOperationAction extends Action { + public static ID = 'qp.highlightExpensiveOperationAction'; + public static LABEL = localize('highlightExpensiveOperationAction', 'Apply'); + + constructor() { + super(HighlightExpensiveOperationAction.ID, HighlightExpensiveOperationAction.LABEL, searchIconClassNames); + } + + public override async run(context: HighlightExpensiveOperationWidget): Promise { + const expensiveOperationDelegate: (cell: AzDataGraphCell) => number | undefined = context.getExpensiveOperationDelegate(); + + context.executionPlanDiagram.clearExpensiveOperatorHighlighting(); + let result = context.executionPlanDiagram.highlightExpensiveOperator(expensiveOperationDelegate); + if (!result) { + const metric = context.expenseMetricSelectBox.value; + context.notificationService.warn(localize('invalidPropertyExecutionPlanMetric', 'No nodes found with the {0} metric.', metric)); + } + } +} + +export class TurnOffExpensiveHighlightingOperationAction extends Action { + public static ID = 'qp.turnOffExpensiveHighlightingOperationAction'; + public static LABEL = localize('turnOffExpensiveHighlightingOperationAction', 'Off'); + + constructor() { + super(TurnOffExpensiveHighlightingOperationAction.ID, TurnOffExpensiveHighlightingOperationAction.LABEL); + } + + public override async run(context: HighlightExpensiveOperationWidget): Promise { + context.executionPlanDiagram.clearExpensiveOperatorHighlighting(); + } +} + +export class CancelHIghlightExpensiveOperationAction extends Action { + public static ID = 'qp.cancelExpensiveOperationAction'; + public static LABEL = localize('cancelExpensiveOperationAction', 'Close'); + + constructor() { + super(CancelHIghlightExpensiveOperationAction.ID, CancelHIghlightExpensiveOperationAction.LABEL, Codicon.chromeClose.classNames); + } + + public override async run(context: HighlightExpensiveOperationWidget): Promise { + context.widgetController.removeWidget(context); + } +} diff --git a/src/sql/workbench/contrib/executionPlan/browser/widgets/nodeSearchWidget.ts b/src/sql/workbench/contrib/executionPlan/browser/widgets/nodeSearchWidget.ts index db85f5a0cc..72fbc58917 100644 --- a/src/sql/workbench/contrib/executionPlan/browser/widgets/nodeSearchWidget.ts +++ b/src/sql/workbench/contrib/executionPlan/browser/widgets/nodeSearchWidget.ts @@ -55,12 +55,12 @@ export class NodeSearchWidget extends ExecutionPlanWidgetBase { this.container.appendChild(this._propertyNameSelectBoxContainer); const propDropdownOptions = this._executionPlanDiagram.getUniqueElementProperties(); this._propertyNameSelectBox = new SelectBox(propDropdownOptions, propDropdownOptions[0], this.contextViewService, this._propertyNameSelectBoxContainer); - attachSelectBoxStyler(this._propertyNameSelectBox, this.themeService); + this._register(attachSelectBoxStyler(this._propertyNameSelectBox, this.themeService)); this._propertyNameSelectBoxContainer.style.width = '150px'; this._propertyNameSelectBox.render(this._propertyNameSelectBoxContainer); - this._propertyNameSelectBox.onDidSelect(e => { + this._register(this._propertyNameSelectBox.onDidSelect(e => { this._usePreviousSearchResult = false; - }); + })); // search type dropdown this._searchTypeSelectBoxContainer = DOM.$('.search-widget-search-type-select-box .dropdown-container'); @@ -75,9 +75,9 @@ export class NodeSearchWidget extends ExecutionPlanWidgetBase { LESSER_AND_GREATER_DISPLAY_STRING ], EQUALS_DISPLAY_STRING, this.contextViewService, this._searchTypeSelectBoxContainer); this._searchTypeSelectBox.render(this._searchTypeSelectBoxContainer); - attachSelectBoxStyler(this._searchTypeSelectBox, this.themeService); + this._register(attachSelectBoxStyler(this._searchTypeSelectBox, this.themeService)); this._searchTypeSelectBoxContainer.style.width = '100px'; - this._searchTypeSelectBox.onDidSelect(e => { + this._register(this._searchTypeSelectBox.onDidSelect(e => { this._usePreviousSearchResult = false; switch (e.selected) { case EQUALS_DISPLAY_STRING: @@ -101,35 +101,44 @@ export class NodeSearchWidget extends ExecutionPlanWidgetBase { case LESSER_AND_GREATER_DISPLAY_STRING: this._selectedSearchType = SearchType.LesserAndGreaterThan; } - }); + })); // search text input box this._searchTextInputBox = new InputBox(this.container, this.contextViewService, {}); - attachInputBoxStyler(this._searchTextInputBox, this.themeService); + this._register(attachInputBoxStyler(this._searchTextInputBox, this.themeService)); this._searchTextInputBox.element.style.marginLeft = '5px'; - this._searchTextInputBox.onDidChange(e => { + this._register(this._searchTextInputBox.onDidChange(e => { this._usePreviousSearchResult = false; - }); + })); // setting up key board shortcuts + const goToPreviousMatchAction = new GoToPreviousMatchAction(); + this._register(goToPreviousMatchAction); + + const goToNextMatchAction = new GoToNextMatchAction(); + this._register(goToNextMatchAction); + + const cancelSearchAction = new CancelSearch(); + this._register(cancelSearchAction); + const self = this; this._searchTextInputBox.element.onkeydown = async e => { if (e.key === 'Enter' && e.shiftKey) { - await new GoToPreviousMatchAction().run(self); + await goToPreviousMatchAction.run(self); } else if (e.key === 'Enter') { - await new GoToNextMatchAction().run(self); + await goToNextMatchAction.run(self); } else if (e.key === 'Escape') { - await new CancelSearch().run(self); + await cancelSearchAction.run(self); } }; // Adding action bar this._actionBar = new ActionBar(this.container); this._actionBar.context = this; - this._actionBar.pushAction(new GoToPreviousMatchAction(), { label: false, icon: true }); - this._actionBar.pushAction(new GoToNextMatchAction(), { label: false, icon: true }); - this._actionBar.pushAction(new CancelSearch(), { label: false, icon: true }); + this._actionBar.pushAction(goToPreviousMatchAction, { label: false, icon: true }); + this._actionBar.pushAction(goToNextMatchAction, { label: false, icon: true }); + this._actionBar.pushAction(cancelSearchAction, { label: false, icon: true }); } // Initial focus is set to the search text input box @@ -175,7 +184,7 @@ export class NodeSearchWidget extends ExecutionPlanWidgetBase { export class GoToNextMatchAction extends Action { public static ID = 'qp.NextSearchAction'; - public static LABEL = localize('nextSearchItemAction', "Next Match (Enter)"); + public static LABEL = localize('nextSearchItemAction', "Next Match"); constructor() { super(GoToNextMatchAction.ID, GoToNextMatchAction.LABEL, Codicon.arrowDown.classNames); @@ -188,7 +197,7 @@ export class GoToNextMatchAction extends Action { export class GoToPreviousMatchAction extends Action { public static ID = 'qp.PreviousSearchAction'; - public static LABEL = localize('previousSearchItemAction', "Previous Match (Shift+Enter)"); + public static LABEL = localize('previousSearchItemAction', "Previous Match"); constructor() { super(GoToPreviousMatchAction.ID, GoToPreviousMatchAction.LABEL, Codicon.arrowUp.classNames); @@ -201,7 +210,7 @@ export class GoToPreviousMatchAction extends Action { export class CancelSearch extends Action { public static ID = 'qp.cancelSearchAction'; - public static LABEL = localize('cancelSearchAction', "Close (Escape)"); + public static LABEL = localize('cancelSearchAction', "Close"); constructor() { super(CancelSearch.ID, CancelSearch.LABEL, Codicon.chromeClose.classNames); diff --git a/yarn.lock b/yarn.lock index 5f677a9673..15eb3f8244 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1891,9 +1891,9 @@ aws4@^1.8.0: resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f" integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ== -"azdataGraph@github:Microsoft/azdataGraph#0.0.42": - version "0.0.42" - resolved "https://codeload.github.com/Microsoft/azdataGraph/tar.gz/86f72ee9c9ea78a31c9ad2f402fb24d40e50c75b" +"azdataGraph@github:Microsoft/azdataGraph#0.0.45": + version "0.0.45" + resolved "https://codeload.github.com/Microsoft/azdataGraph/tar.gz/8675ac6dfc60aad331ccbf7e4145b9eaa25e7304" bach@^1.0.0: version "1.2.0"