mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-13 17:22:15 -05:00
Query Execution Plan Expensive Operator Highlighting (#20579)
* Boilerplate for new context menu options * Enables checkmarks for expensive operator context menu actions * Updates azdataGraph version to 0.0.44 * Adds clearing logic to actions and retrieves additional info from nodes * Removes unnecessary actions that aren't supported by other providers * Finishes setting up the rest of the context menu actions * Corrects context menu action label * Defines new widget type for finding expensive operations * Adds TODO for class icon name * Creates action to open the expensive operation widget * Adds escape and enter key shortcuts to findExpensiveOperation widget * Styles find expensive operation widget * Corrects class name for finding expensive operator action * Corrects import statement. * Code clean up * Bumps azdataGraph version to 0.0.45 * Adds an info box for when a metric doesn't find any nodes * Adds label to find expensive operator widget * Invokes dispose when widget controller removes widget * Implements disposable in all execution plan widgets. * Adds off action to clear highlighting * Adds a default setting and default prompt for highlighting metric * Fixes not all code paths return error * Removes shortcut key from text for widget actions * Adds enum description * Removes added dictionary type, and renames class name * Removes unnecessary null checks * Removes cost metric dictionary and adds corresponding properties * Code review changes * Removes incorrectly implemented key down event for widget. * Renames widget and action class names for highlighting expensive ops * File rename * Cleans up labels to better reflect highlight action * Removes hardcoded button width style and sets it to auto * More clean up * Clean up import statement * Code review changes * Drop down list only shows available metrics * Updates highlight expensive operation icon * Update STS version
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
19
src/sql/azdata.proposed.d.ts
vendored
19
src/sql/azdata.proposed.d.ts
vendored
@@ -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 {
|
||||
|
||||
@@ -81,6 +81,7 @@ export const enum TelemetryAction {
|
||||
GeneratePreviewReport = 'GeneratePreviewReport',
|
||||
GetDataGridItems = 'GetDataGridItems',
|
||||
GetDataGridColumns = 'GetDataGridColumns',
|
||||
HighlightExpensiveOperation = 'HighlightExpensiveOperation',
|
||||
ModelViewDashboardOpened = 'ModelViewDashboardOpened',
|
||||
ModalDialogClosed = 'ModalDialogClosed',
|
||||
ModalDialogOpened = 'ModalDialogOpened',
|
||||
|
||||
@@ -25,6 +25,7 @@ export class AzdataGraphView {
|
||||
private _diagram: any;
|
||||
private _diagramModel: AzDataGraphCell;
|
||||
private _cellInFocus: AzDataGraphCell;
|
||||
public expensiveMetricTypes: Set<ExpensiveMetricType> = new Set();
|
||||
|
||||
private _graphElementPropertiesSet: Set<string> = 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
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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<void> {
|
||||
@@ -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<void> {
|
||||
@@ -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<void> {
|
||||
@@ -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<void> {
|
||||
@@ -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<void> {
|
||||
@@ -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<void> {
|
||||
@@ -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<void> {
|
||||
@@ -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<void> {
|
||||
@@ -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<void> {
|
||||
@@ -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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
@@ -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<void> {
|
||||
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<void> {
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<g id="f4eb8f61-ee01-4fe9-a111-7a076ae7c7be" data-name="Icons">
|
||||
<path d="M5,5H0V0H5Z"/>
|
||||
<path d="M16,5H11V0h5ZM12,4h3V1H12Z"/>
|
||||
<path d="M7.975.525l.75.75-.684.692H11V3.033H8.041l.684.692-.75.75L6,2.5Z"/>
|
||||
<path d="M16,14H11V9h5Zm-4-1h3V10H12Z"/>
|
||||
<rect x="13" y="5" width="1" height="5"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 407 B |
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 25.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 16 16" style="enable-background:new 0 0 16 16;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
</style>
|
||||
<g id="f4eb8f61-ee01-4fe9-a111-7a076ae7c7be">
|
||||
<path class="st0" d="M5,5H0V0h5V5z"/>
|
||||
<path class="st0" d="M16,5h-5V0h5V5z M12,4h3V1h-3V4z"/>
|
||||
<path class="st0" d="M8,0.5l0.8,0.8L8,2h3V3H8l0.7,0.7L8,4.5l-2-2L8,0.5z"/>
|
||||
<path class="st0" d="M16,14h-5V9h5V14z M12,13h3v-3h-3V13z"/>
|
||||
<rect x="13" y="5" class="st0" width="1" height="4"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 746 B |
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<string>('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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
context.widgetController.removeWidget(context);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user