diff --git a/package.json b/package.json index 8fa031a060..0f6a4eed50 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ "angular2-grid": "2.0.6", "ansi_up": "^3.0.0", "applicationinsights": "1.0.8", - "azdataGraph": "github:Microsoft/azdataGraph#0.0.7", + "azdataGraph": "github:Microsoft/azdataGraph#0.0.9", "chart.js": "^2.9.4", "chokidar": "3.5.2", "graceful-fs": "4.2.6", diff --git a/src/sql/azdata.proposed.d.ts b/src/sql/azdata.proposed.d.ts index a81dec934b..ca56f6a16f 100644 --- a/src/sql/azdata.proposed.d.ts +++ b/src/sql/azdata.proposed.d.ts @@ -915,9 +915,9 @@ declare module 'azdata' { export interface QueryExecuteResultSetNotificationParams { /** - * Contains query plans returned by the database in ResultSets. + * Contains execution plans returned by the database in ResultSets. */ - executionPlans: QueryPlanGraph[]; + executionPlans: ExecutionPlanGraph[]; } export interface ResultSetSummary { @@ -925,10 +925,6 @@ declare module 'azdata' { * The visualization options for the result set. */ visualization?: VisualizationOptions; - /** - * Generic query plan graph to be displayed in the results view. - */ - showplangraph?: QueryPlanGraph; } /** @@ -1435,18 +1431,26 @@ declare module 'azdata' { } } - export interface QueryPlanGraph { + export interface ExecutionPlanGraph { /** - * Root of the query plan tree + * Root of the execution plan tree */ - root: QueryPlanGraphNode; + root: ExecutionPlanNode; /** - * Underlying query for the query plan graph. + * Underlying query for the execution plan graph. */ query: string; + /** + * String representation of graph + */ + graphFile: ExecutionPlanGraphFile; + /** + * Query recommendations for optimizing performance + */ + recommendations: ExecutionPlanRecommendations[]; } - export interface QueryPlanGraphNode { + export interface ExecutionPlanNode { /** * Type of the node. This property determines the icon that is displayed for it */ @@ -1470,7 +1474,7 @@ declare module 'azdata' { /** * Node properties to be shown in the tooltip */ - properties: QueryPlanGraphElementProperty[]; + properties: ExecutionPlanGraphElementProperty[]; /** * Display name for the node */ @@ -1486,14 +1490,14 @@ declare module 'azdata' { /** * Direct children of the nodes. */ - children: QueryPlanGraphNode[]; + children: ExecutionPlanNode[]; /** * Edges corresponding to the children. */ - edges: QueryGraphEdge[]; + edges: ExecutionPlanEdge[]; } - export interface QueryGraphEdge { + export interface ExecutionPlanEdge { /** * Count of the rows returned by the subtree of the edge. */ @@ -1505,18 +1509,18 @@ declare module 'azdata' { /** * Edge properties to be shown in the tooltip. */ - properties: QueryPlanGraphElementProperty[] + properties: ExecutionPlanGraphElementProperty[] } - export interface QueryPlanGraphElementProperty { + export interface ExecutionPlanGraphElementProperty { /** * Name of the property */ name: string; /** - * Formatted value for the property + * value for the property */ - formattedValue: string; + value: string | ExecutionPlanGraphElementProperty[]; /** * Flag to show/hide props in tooltip */ @@ -1530,4 +1534,30 @@ declare module 'azdata' { */ isLongString: boolean; } + + export interface ExecutionPlanRecommendations { + /** + * Text displayed in the show plan graph control description + */ + displayString: string; + /** + * Query that is recommended to the user + */ + queryText: string; + /** + * Query that will be opened in a new file once the user click on the recommendation + */ + queryWithDescription: string; + } + + export interface ExecutionPlanGraphFile { + /** + * File contents + */ + graphFileContent: string; + /** + * File type for execution plan. This will be the file type of the editor when the user opens the graph file + */ + graphFileType: string; + } } diff --git a/src/sql/base/common/strings.ts b/src/sql/base/common/strings.ts index c91ea3a319..da7e1921fc 100644 --- a/src/sql/base/common/strings.ts +++ b/src/sql/base/common/strings.ts @@ -71,3 +71,10 @@ export function endsWith(haystack: string, needle: string): boolean { return false; } } + +/** + * Remove line breaks/eols from a string across different operating systems. + */ +export function removeLineBreaks(str: string): string { + return str.replace(/(\r\n|\n|\r)/gm, ''); +} diff --git a/src/sql/workbench/common/editor/query/queryPlan2State.ts b/src/sql/workbench/common/editor/query/queryPlan2State.ts new file mode 100644 index 0000000000..475149fd56 --- /dev/null +++ b/src/sql/workbench/common/editor/query/queryPlan2State.ts @@ -0,0 +1,13 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type * as azdata from 'azdata'; + +export class QueryPlan2State { + graphs: azdata.ExecutionPlanGraph[] = []; + clearQueryPlan2State() { + this.graphs = []; + } +} diff --git a/src/sql/workbench/common/editor/query/queryResultsInput.ts b/src/sql/workbench/common/editor/query/queryResultsInput.ts index 14fc022e62..da4dd4a368 100644 --- a/src/sql/workbench/common/editor/query/queryResultsInput.ts +++ b/src/sql/workbench/common/editor/query/queryResultsInput.ts @@ -12,6 +12,7 @@ import { QueryPlanState } from 'sql/workbench/common/editor/query/queryPlanState import { GridPanelState } from 'sql/workbench/common/editor/query/gridTableState'; import { QueryModelViewState } from 'sql/workbench/common/editor/query/modelViewState'; import { URI } from 'vs/base/common/uri'; +import { QueryPlan2State } from 'sql/workbench/common/editor/query/queryPlan2State'; export class ResultsViewState { public readonly gridPanelState: GridPanelState = new GridPanelState(); @@ -19,6 +20,7 @@ export class ResultsViewState { public readonly queryPlanState: QueryPlanState = new QueryPlanState(); public readonly topOperationsState = new TopOperationsState(); public readonly dynamicModelViewTabsState: Map = new Map(); + public readonly queryPlan2State: QueryPlan2State = new QueryPlan2State(); public activeTab?: string; public readonly visibleTabs: Set = new Set(); @@ -27,6 +29,7 @@ export class ResultsViewState { this.gridPanelState.dispose(); this.chartState.dispose(); this.queryPlanState.dispose(); + this.queryPlan2State.clearQueryPlan2State(); this.dynamicModelViewTabsState.forEach((state: QueryModelViewState, identifier: string) => { state.dispose(); }); diff --git a/src/sql/workbench/contrib/query/browser/queryResultsView.ts b/src/sql/workbench/contrib/query/browser/queryResultsView.ts index 0de3759434..184990353a 100644 --- a/src/sql/workbench/contrib/query/browser/queryResultsView.ts +++ b/src/sql/workbench/contrib/query/browser/queryResultsView.ts @@ -185,7 +185,7 @@ export class QueryResultsView extends Disposable { this._panelView = this._register(new TabbedPanel(container, { showHeaderWhenSingleView: true })); this._register(attachTabbedPanelStyler(this._panelView, themeService)); this.qpTab = this._register(new QueryPlanTab()); - this.qp2Tab = this._register(new QueryPlan2Tab()); + this.qp2Tab = this._register(this.instantiationService.createInstance(QueryPlan2Tab)); this.topOperationsTab = this._register(new TopOperationsTab(instantiationService)); this._panelView.pushTab(this.resultsTab); @@ -254,6 +254,8 @@ export class QueryResultsView extends Disposable { if (!this.input.state.visibleTabs.has(this.qp2Tab.identifier)) { this.showPlan2(); } + // Adding graph to state and tab as they become available + this.input.state.queryPlan2State.graphs.push(...e.planGraphs); this.qp2Tab.view.addGraphs(e.planGraphs); } })); @@ -334,6 +336,7 @@ export class QueryResultsView extends Disposable { if (input) { this.resultsTab.view.state = input.state.gridPanelState; this.qpTab.view.setState(input.state.queryPlanState); + this.qp2Tab.view.addGraphs(input.state.queryPlan2State.graphs); this.topOperationsTab.view.setState(input.state.topOperationsState); this.chartTab.view.state = input.state.chartState; this.dynamicModelViewTabs.forEach((dynamicTab: QueryModelViewTab) => { @@ -454,6 +457,7 @@ export class QueryResultsView extends Disposable { public hidePlan2() { if (this._panelView.contains(this.qp2Tab)) { this.qp2Tab.clear(); + this.input.state.queryPlan2State.clearQueryPlan2State(); this._panelView.removeTab(this.qp2Tab.identifier); } } diff --git a/src/sql/workbench/contrib/queryplan2/browser/actions/propertiesAction.ts b/src/sql/workbench/contrib/queryplan2/browser/actions/propertiesAction.ts deleted file mode 100644 index 8549ef138c..0000000000 --- a/src/sql/workbench/contrib/queryplan2/browser/actions/propertiesAction.ts +++ /dev/null @@ -1,23 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { QueryPlan2 } from 'sql/workbench/contrib/queryplan2/browser/queryPlan'; -import { Action } from 'vs/base/common/actions'; -import { Codicon } from 'vs/base/common/codicons'; -import { localize } from 'vs/nls'; - - -export class PropertiesAction extends Action { - public static ID = 'qp.propertiesAction'; - public static LABEL = localize('queryPlanPropertiesActionLabel', "Properties"); - - constructor() { - super(PropertiesAction.ID, PropertiesAction.LABEL, Codicon.listUnordered.classNames); - } - - public override async run(context: QueryPlan2): Promise { - context.propContainer.style.visibility = context.propContainer.style.visibility === 'visible' ? 'hidden' : 'visible'; - } -} diff --git a/src/sql/workbench/contrib/queryplan2/browser/constants.ts b/src/sql/workbench/contrib/queryplan2/browser/constants.ts new file mode 100644 index 0000000000..78f9b9c16f --- /dev/null +++ b/src/sql/workbench/contrib/queryplan2/browser/constants.ts @@ -0,0 +1,250 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +let imageBasePath = require.toUrl('./images/icons/'); +export let queryPlanNodeIconPaths = +{ + // generic icons + iteratorCatchAll: imageBasePath + 'iterator_catch_all.png', + + cursorCatchAll: imageBasePath + 'cursor_catch_all.png', + + languageConstructCatchAll: imageBasePath + 'language_construct_catch_all.png', + + // operator icons + adaptiveJoin: imageBasePath + 'adaptive_join.png', + + assert: imageBasePath + 'assert.png', + + bitmap: imageBasePath + 'bitmap.png', + + clusteredIndexDelete: imageBasePath + 'clustered_index_delete.png', + + clusteredIndexInsert: imageBasePath + 'clustered_index_insert.png', + + clusteredIndexScan: imageBasePath + 'clustered_index_scan.png', + + clusteredIndexSeek: imageBasePath + 'clustered_index_seek.png', + + clusteredIndexUpdate: imageBasePath + 'clustered_index_update.png', + + clusteredIndexMerge: imageBasePath + 'clustered_index_merge.png', + + clusteredUpdate: imageBasePath + 'clustered_update.png', + + collapse: imageBasePath + 'collapse.png', + + computeScalar: imageBasePath + 'compute_scalar.png', + + concatenation: imageBasePath + 'concatenation.png', + + constantScan: imageBasePath + 'constant_scan.png', + + deletedScan: imageBasePath + 'deleted_scan.png', + + filter: imageBasePath + 'filter.png', + + hashMatch: imageBasePath + 'hash_match.png', + + indexDelete: imageBasePath + 'index_delete.png', + + indexInsert: imageBasePath + 'index_insert.png', + + indexScan: imageBasePath + 'index_scan.png', + + columnstoreIndexDelete: imageBasePath + 'columnstore_index_delete.png', + + columnstoreIndexInsert: imageBasePath + 'columnstore_index_insert.png', + + columnstoreIndexMerge: imageBasePath + 'columnstore_index_merge.png', + + columnstoreIndexScan: imageBasePath + 'columnstore_index_scan.png', + + columnstoreIndexUpdate: imageBasePath + 'columnstore_index_update.png', + + indexSeek: imageBasePath + 'index_seek.png', + + indexSpool: imageBasePath + 'index_spool.png', + + indexUpdate: imageBasePath + 'index_update.png', + + insertedScan: imageBasePath + 'inserted_scan.png', + + logRowScan: imageBasePath + 'log_row_scan.png', + + mergeInterval: imageBasePath + 'merge_interval.png', + + mergeJoin: imageBasePath + 'merge_join.png', + + nestedLoops: imageBasePath + 'nested_loops.png', + + parallelism: imageBasePath + 'parallelism.png', + + parameterTableScan: imageBasePath + 'parameter_table_scan.png', + + print: imageBasePath + 'print.png', + + rank: imageBasePath + 'rank.png', + + foreignKeyReferencesCheck: imageBasePath + 'foreign_key_references_check.png', + + remoteDelete: imageBasePath + 'remote_delete.png', + + remoteIndexScan: imageBasePath + 'remote_index_scan.png', + + remoteIndexSeek: imageBasePath + 'remote_index_seek.png', + + remoteInsert: imageBasePath + 'remote_insert.png', + + remoteQuery: imageBasePath + 'remote_query.png', + + remoteScan: imageBasePath + 'remote_scan.png', + + remoteUpdate: imageBasePath + 'remote_update.png', + + ridLookup: imageBasePath + 'rid_lookup.png', + + rowCountSpool: imageBasePath + 'row_count_spool.png', + + segment: imageBasePath + 'segment.png', + + sequence: imageBasePath + 'sequence.png', + + sequenceProject: imageBasePath + 'sequence_project.png', + + sort: imageBasePath + 'sort.png', + + split: imageBasePath + 'split.png', + + streamAggregate: imageBasePath + 'stream_aggregate.png', + + switchStatement: imageBasePath + 'switch.png', + + tableValuedFunction: imageBasePath + 'table_valued_function.png', + + tableDelete: imageBasePath + 'table_delete.png', + + tableInsert: imageBasePath + 'table_insert.png', + + tableScan: imageBasePath + 'table_scan.png', + + tableSpool: imageBasePath + 'table_spool.png', + + tableUpdate: imageBasePath + 'table_update.png', + + tableMerge: imageBasePath + 'table_merge.png', + + tfp: imageBasePath + 'predict.png', + + top: imageBasePath + 'top.png', + + udx: imageBasePath + 'udx.png', + + batchHashTableBuild: imageBasePath + 'batch_hash_table_build.png', + + windowSpool: imageBasePath + 'table_spool.png', + + windowAggregate: imageBasePath + 'window_aggregate.png', + + // cursor operators + fetchQuery: imageBasePath + 'fetch_query.png', + + populateQuery: imageBasePath + 'population_query.png', + + refreshQuery: imageBasePath + 'refresh_query.png', + + // shiloh operators + result: imageBasePath + 'result.png', + + aggregate: imageBasePath + 'aggregate.png', + + assign: imageBasePath + 'assign.png', + + arithmeticExpression: imageBasePath + 'arithmetic_expression.png', + + bookmarkLookup: imageBasePath + 'bookmark_lookup.png', + + convert: imageBasePath + 'convert.png', + + declare: imageBasePath + 'declare.png', + + deleteOperator: imageBasePath + 'delete.png', + + dynamic: imageBasePath + 'dynamic.png', + + hashMatchRoot: imageBasePath + 'hash_match_root.png', + + hashMatchTeam: imageBasePath + 'hash_match_team.png', + + ifOperator: imageBasePath + 'if.png', + + insert: imageBasePath + 'insert.png', + + intrinsic: imageBasePath + 'intrinsic.png', + + keyset: imageBasePath + 'keyset.png', + + locate: imageBasePath + 'locate.png', + + populationQuery: imageBasePath + 'population_query.png', + + setFunction: imageBasePath + 'set_function.png', + + snapshot: imageBasePath + 'snapshot.png', + + spool: imageBasePath + 'spool.png', + + tsql: imageBasePath + 'sql.png', + + update: imageBasePath + 'update.png', + + // fake operators + keyLookup: imageBasePath + 'bookmark_lookup.png', + + // PDW operators + apply: imageBasePath + 'apply.png', + + broadcast: imageBasePath + 'broadcast.png', + + computeToControlNode: imageBasePath + 'compute_to_control_node.png', + + constTableGet: imageBasePath + 'const_table_get.png', + + controlToComputeNodes: imageBasePath + 'control_to_compute_nodes.png', + + externalBroadcast: imageBasePath + 'external_broadcast.png', + + externalExport: imageBasePath + 'external_export.png', + + externalLocalStreaming: imageBasePath + 'external_local_streaming.png', + + externalRoundRobin: imageBasePath + 'external_round_robin.png', + + externalShuffle: imageBasePath + 'external_shuffle.png', + + get: imageBasePath + 'get.png', + + groupByApply: imageBasePath + 'apply.png', + + groupByAggregate: imageBasePath + 'group_by_aggregate.png', + + join: imageBasePath + 'join.png', + + localCube: imageBasePath + 'intrinsic.png', + + project: imageBasePath + 'project.png', + + shuffle: imageBasePath + 'shuffle.png', + + singleSourceRoundRobin: imageBasePath + 'single_source_round_robin.png', + + singleSourceShuffle: imageBasePath + 'single_source_shuffle.png', + + trim: imageBasePath + 'trim.png', + + union: imageBasePath + 'union.png', + + unionAll: imageBasePath + 'union_all.png' +}; diff --git a/src/sql/workbench/contrib/queryplan2/browser/graphElementPropertiesView.ts b/src/sql/workbench/contrib/queryplan2/browser/graphElementPropertiesView.ts new file mode 100644 index 0000000000..1dab069ae8 --- /dev/null +++ b/src/sql/workbench/contrib/queryplan2/browser/graphElementPropertiesView.ts @@ -0,0 +1,260 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as DOM from 'vs/base/browser/dom'; +import type * as azdata from 'azdata'; +import { localize } from 'vs/nls'; +import { Action } from 'vs/base/common/actions'; +import { Codicon } from 'vs/base/common/codicons'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { attachTableStyler } from 'sql/platform/theme/common/styler'; +import { TableDataView } from 'sql/base/browser/ui/table/tableDataView'; +import { Table } from 'sql/base/browser/ui/table/table'; +import { RESULTS_GRID_DEFAULTS } from 'sql/workbench/common/constants'; +import { isString } from 'vs/base/common/types'; +import { ActionBar } from 'sql/base/browser/ui/taskbar/actionbar'; +import { ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar'; + + +export class GraphElementPropertiesView { + + // Title bar with close button action + private _propertiesTitle!: HTMLElement; + private _titleText!: HTMLElement; + private _titleActionBarContainer!: HTMLElement; + private _titleActionBar: ActionBar; + + // Div that holds the name of the element selected + private _operationName!: HTMLElement; + + // Action bar that contains sorting option for the table + private _tableActionBarContainer!: HTMLElement; + private _tableActionBar!: ActionBar; + + // Properties table + private _table: Table; + private _dataView: TableDataView; + private _data: { [key: string]: string }[]; + private _tableContainer!: HTMLElement; + private _actualTable!: HTMLElement; + + // Table dimensions. + private _tableWidth = 485; + private _tableHeight = 420; + + public constructor( + private _parentContainer: HTMLElement, + private _themeService: IThemeService, + private _model: GraphElementPropertyViewData = {} + ) { + this._parentContainer.style.display = 'none'; + + this._propertiesTitle = DOM.$('.title'); + this._parentContainer.appendChild(this._propertiesTitle); + + this._titleText = DOM.$('h3'); + this._titleText.classList.add('text'); + this._titleText.innerText = localize('nodePropertyViewTitle', "Properties"); + this._propertiesTitle.appendChild(this._titleText); + + this._titleActionBarContainer = DOM.$('.action-bar'); + this._propertiesTitle.appendChild(this._titleActionBarContainer); + this._titleActionBar = new ActionBar(this._titleActionBarContainer, { + orientation: ActionsOrientation.HORIZONTAL, context: this + }); + this._titleActionBar.pushAction([new ClosePropertyViewAction()], { icon: true, label: false }); + + this._operationName = DOM.$('h3'); + this._operationName.classList.add('operation-name'); + this._parentContainer.appendChild(this._operationName); + + this._tableActionBarContainer = DOM.$('.table-action-bar'); + this._parentContainer.appendChild(this._tableActionBarContainer); + this._tableActionBar = new ActionBar(this._tableActionBarContainer, { + orientation: ActionsOrientation.HORIZONTAL, context: this + }); + this._tableActionBar.pushAction([new SortPropertiesByDisplayOrderAction(), new SortPropertiesAlphabeticallyAction()], { icon: true, label: false }); + + + this._tableContainer = DOM.$('.table-container'); + this._parentContainer.appendChild(this._tableContainer); + + this._actualTable = DOM.$('.table'); + this._tableContainer.appendChild(this._actualTable); + + this._dataView = new TableDataView(); + this._data = []; + + const columns: Slick.Column[] = [ + { + id: 'name', + name: localize('nodePropertyViewNameNameColumnHeader', "Name"), + field: 'name', + width: 250, + editor: Slick.Editors.Text, + headerCssClass: 'prop-table-header' + }, + { + id: 'value', + name: localize('nodePropertyViewNameValueColumnHeader', "Value"), + field: 'value', + width: 250, + editor: Slick.Editors.Text, + headerCssClass: 'prop-table-header' + } + ]; + + this._table = new Table(this._actualTable, { + dataProvider: this._dataView, columns: columns + }, { + rowHeight: RESULTS_GRID_DEFAULTS.rowHeight, + forceFitColumns: true, + defaultColumnWidth: 120 + }); + + attachTableStyler(this._table, this._themeService); + } + + public set graphElement(element: azdata.ExecutionPlanNode | azdata.ExecutionPlanEdge) { + this._model.graphElement = element; + this.sortPropertiesByImportance(); + this.renderView(); + } + + public sortPropertiesAlphabetically(): void { + this._model.graphElement.properties = this._model.graphElement.properties.sort((a, b) => { + if (!a?.name && !b?.name) { + return 0; + } else if (!a?.name) { + return -1; + } else if (!b?.name) { + return 1; + } else { + return a.name.localeCompare(b.name); + } + }); + this.renderView(); + } + + public sortPropertiesByImportance(): void { + this._model.graphElement.properties = this._model.graphElement.properties.sort((a, b) => { + if (!a?.displayOrder && !b?.displayOrder) { + return 0; + } else if (!a?.displayOrder) { + return -1; + } else if (!b?.displayOrder) { + return 1; + } else { + return a.displayOrder - b.displayOrder; + } + }); + this.renderView(); + } + + public set tableHeight(value: number) { + if (this.tableHeight !== value) { + this._tableHeight = value; + this.renderView(); + } + } + + public get tableHeight(): number { + return this._tableHeight; + } + + public set tableWidth(value: number) { + if (this._tableWidth !== value) { + this._tableWidth = value; + this.renderView(); + } + } + + public get tableWidth(): number { + return this._tableWidth; + } + + private renderView(): void { + if (this._model.graphElement) { + const nodeName = (this._model.graphElement).name; + this._operationName.innerText = nodeName ?? localize('queryPlanPropertiesEdgeOperationName', "Edge"); //since edges do not have names like node, we set the operation name to 'Edge' + } + this._tableContainer.scrollTo(0, 0); + this._dataView.clear(); + this._data = this.convertPropertiesToTableRows(this._model.graphElement.properties, -1, 0); + this._dataView.push(this._data); + this._table.setData(this._dataView); + this._table.autosizeColumns(); + this._table.updateRowCount(); + this._table.layout(new DOM.Dimension(this._tableWidth, this._tableHeight)); + this._table.resizeCanvas(); + } + + private convertPropertiesToTableRows(props: azdata.ExecutionPlanGraphElementProperty[], parentIndex: number, indent: number, rows: { [key: string]: string }[] = []): { [key: string]: string }[] { + if (!props) { + return rows; + } + props.forEach((p, i) => { + let row = {}; + row['name'] = '\t'.repeat(indent) + p.name; + row['parent'] = parentIndex; + rows.push(row); + if (!isString(p.value)) { + row['value'] = ''; + this.convertPropertiesToTableRows(p.value, rows.length - 1, indent + 2, rows); + } else { + row['value'] = p.value; + } + }); + return rows; + } + + public toggleVisibility(): void { + this._parentContainer.style.display = this._parentContainer.style.display === 'none' ? 'block' : 'none'; + this.renderView(); + } +} + +export interface GraphElementPropertyViewData { + graphElement: azdata.ExecutionPlanNode | azdata.ExecutionPlanEdge; +} + +export class ClosePropertyViewAction extends Action { + public static ID = 'qp.propertiesView.close'; + public static LABEL = localize('queryPlanPropertyViewClose', "Close"); + + constructor() { + super(ClosePropertyViewAction.ID, ClosePropertyViewAction.LABEL, Codicon.close.classNames); + } + + public override async run(context: GraphElementPropertiesView): Promise { + context.toggleVisibility(); + } +} + +export class SortPropertiesAlphabeticallyAction extends Action { + public static ID = 'qp.propertiesView.sortByAlphabet'; + public static LABEL = localize('queryPlanPropertyViewSortAlphabetically', "Alphabetical"); + + constructor() { + super(SortPropertiesAlphabeticallyAction.ID, SortPropertiesAlphabeticallyAction.LABEL, Codicon.sortPrecedence.classNames); + } + + public override async run(context: GraphElementPropertiesView): Promise { + context.sortPropertiesAlphabetically(); + } +} + +export class SortPropertiesByDisplayOrderAction extends Action { + public static ID = 'qp.propertiesView.sortByDisplayOrder'; + public static LABEL = localize('queryPlanPropertyViewSortByDisplayOrde', "Categorized"); + + constructor() { + super(SortPropertiesByDisplayOrderAction.ID, SortPropertiesByDisplayOrderAction.LABEL, Codicon.listOrdered.classNames); + } + + public override async run(context: GraphElementPropertiesView): Promise { + context.sortPropertiesByImportance(); + } +} diff --git a/src/sql/workbench/contrib/queryplan2/browser/media/queryPlan2.css b/src/sql/workbench/contrib/queryplan2/browser/media/queryPlan2.css index 206b41c403..4afdc4a24e 100644 --- a/src/sql/workbench/contrib/queryplan2/browser/media/queryPlan2.css +++ b/src/sql/workbench/contrib/queryplan2/browser/media/queryPlan2.css @@ -3,67 +3,213 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.qp-container .query-plan { +/* Styling for the a queryplan container in the tab */ +.qps-container .query-plan { width: 100%; height: 500px; + display: flex; + overflow: hidden; +} + +/* horizontal height resizing sash container that is below a queryplan */ +.qps-container .query-plan-sash { + width: 100%; + height: 3px; position: relative; +} + +/* +The actual sash element constructed by code. Important is used here because the width of the sash is fixed. +However we always want it to be the width of the container it is resizing. +*/ +.qps-container .query-plan-sash > div { + width: 100% !important; +} + +/* Container that contains showplan header, description and graph diagram */ +.qps-container .query-plan .plan { + flex: 1; + overflow: hidden; + display: flex; + flex-direction: column; + position: relative; + border: 1px solid; +} + +/* Container that contains views made by the action-bar actions */ +.qps-container .query-plan .plan .plan-action-container { + display: flex; + flex-direction: column; + position: absolute; + right: 0; +} + +/* views created by the action-bar actions */ +.qps-container .query-plan .plan .plan-action-container .child { + flex: 0 0 25px; + margin-left: auto; +} + +/* Search node action view */ +.qps-container .query-plan .plan .plan-action-container .search-node-view { + display: flex; + flex-direction: row; + border: 1px solid; + padding: 5px; + height: auto; + width: 470px; +} + +/* input bar styling in search node action view */ +.qps-container .query-plan .plan .plan-action-container .search-node-view .search-bar-container{ + margin-left: 5px; +} + +/* styling for select element in search node action view */ +.qps-container .query-plan .plan .plan-action-container .search-node-view .search-bar-container > select{ + height: 100%; +} + +/* Custom zoom action view */ +.qps-container .query-plan .plan .plan-action-container .custom-zoom-view { + display: flex; + flex-direction: row; + border: 1px solid; + padding: 5px; + height: auto; + width: 180px; +} + +/* query plan header that contains the relative query cost, query statement and recommendations */ +.qps-container .query-plan .plan .header { + font-family: 'Courier New', Courier, monospace; + border-bottom: solid 1px; + font-weight: bolder; + padding-left: 5px; + font-size: 13px; +} + +/* each link in query plan recommendations */ +.qps-container .query-plan .plan .header .recommendations > a { + width: fit-content; + align-items: left; + text-align: left; +} + +/* graph diagram in query plan */ +.qps-container .query-plan .plan .diagram { + width: 100%; + height: 100%; + overflow: scroll; +} + +/* Properties view in query plan */ +.qps-container .query-plan .properties { + flex: 0 0 500px; + overflow: hidden; + width: 500px; + height: 100%; + border: 1px solid; + display: flex; + flex-direction: column; + border: 1px solid; +} + +/* Title container of the properties view */ +.qps-container .query-plan .properties .title { + line-height: 22px; + height: 22px; + font-size: 11px; + font-weight: bold; + text-transform: uppercase; + overflow: hidden; + display: flex; + align-items: center; + box-sizing: border-box; + margin-top: 3px; + margin-bottom: 5px; + padding-left: 5px; + border-bottom: solid 1px; + flex: 0 0 25px; +} + +/* text in title container of properties view */ +.qps-container .query-plan .properties .title .text { + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + font-size: 11px; + -webkit-margin-before: 0; + -webkit-margin-after: 0; + flex: 1; +} + +/* action bar in the title container for the properties view. This contains the close icon */ +.qps-container .query-plan .properties .title .action-bar { + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + font-size: 11px; + -webkit-margin-before: 0; + -webkit-margin-after: 0; + flex: 0 0 30px; +} + +/* Operation name styling in the properties view. */ +.qps-container .query-plan .properties .operation-name { + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + font-size: 13px; + -webkit-margin-before: 0; + -webkit-margin-after: 0; + flex: 0 0 25px; + margin-top: 3px; + margin-bottom: 5px; + margin-left: 5px; +} + +/* Properties table container in properties view */ +.qps-container .query-plan .properties .table-container { + overflow-y: scroll; + flex: 1; + flex-grow: 1; border-top: 1px solid; } -.qp-container .query-plan .actionbar-container { -height: 100%; -width: 30px; -position: absolute; -top: 0px; -right: 0px; +/* Action bar for the query plan */ +.qps-container .query-plan .action-bar-container { + flex: 0 0 25px; } -.qp-container .query-plan .properties-container { - position: absolute; - top: 0px; - right: 32px; - height: 100%; - overflow-y: scroll; - background-color: #eeeeee; - width: 510px; -} - -.qp-container .prop-table-header { +/* styling for the column headers in the properties table */ +.qps-container .properties .table-container .prop-table-header { + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + font-size: 11px; + min-width: 10px; + -webkit-margin-before: 0; + -webkit-margin-after: 0; font-weight: bold; + text-transform: uppercase; } -.qp-container .properties-header { +.qps-container .properties-header { font-weight: bold; padding: 5px; } -.qp-container .properties-toggle { +.qps-container .properties-toggle { height: 9px; width: 9px; display: inline-block; } -.qp-container .properties-toggle .expand { +.qps-container .properties-toggle .expand { background: url(../images/expand.gif) no-repeat center center; } -.qp-container .properties-toggle .collapse { +.qps-container .properties-toggle .collapse { background: url(../images/collapse.gif) no-repeat center center; } - -.qp-container .mxTooltip { - -webkit-box-shadow: 3px 3px 12px #C0C0C0; - -moz-box-shadow: 3px 3px 12px #C0C0C0; - box-shadow: 3px 3px 12px #C0C0C0; - background: #FFFFCC; - border-style: solid; - border-width: 1px; - border-color: black; - font-family: Arial; - font-size: 8pt; - position: absolute; - cursor: default; - padding: 4px; - color: black; - z-index: 3; -} diff --git a/src/sql/workbench/contrib/queryplan2/browser/planHeader.ts b/src/sql/workbench/contrib/queryplan2/browser/planHeader.ts new file mode 100644 index 0000000000..99431d0345 --- /dev/null +++ b/src/sql/workbench/contrib/queryplan2/browser/planHeader.ts @@ -0,0 +1,117 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type * as azdata from 'azdata'; +import * as DOM from 'vs/base/browser/dom'; +import { localize } from 'vs/nls'; +import { openNewQuery } from 'sql/workbench/contrib/query/browser/queryActions'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { RunQueryOnConnectionMode } from 'sql/platform/connection/common/connectionManagement'; +import { Button } from 'sql/base/browser/ui/button/button'; +import { removeLineBreaks } from 'sql/base/common/strings'; + +export class PlanHeader { + + private _graphIndex: number; // Index of the graph in the view + private _relativeCost: number; // Relative cost of the graph to the script + private _graphIndexAndCostContainer: HTMLElement; //Container that holds the graph index and relative cost + + + private _query: string; + private _queryContainer: HTMLElement; // container that holds query text + + private _recommendations: azdata.ExecutionPlanRecommendations[]; + private _recommendationsContainer: HTMLElement; // container that holds graph recommendations + + public constructor( + private _parentContainer: HTMLElement, + headerData: PlanHeaderData, + @IInstantiationService public readonly _instantiationService: IInstantiationService) { + + this._graphIndex = headerData.planIndex; + this._relativeCost = headerData.relativeCost; + this._query = headerData.query; + this._recommendations = headerData.recommendations ?? []; + + this._graphIndexAndCostContainer = DOM.$('.index-row'); + this._queryContainer = DOM.$('.query-row'); + this._recommendationsContainer = DOM.$('.recommendations'); + + this._parentContainer.appendChild(this._graphIndexAndCostContainer); + this._parentContainer.appendChild(this._queryContainer); + this._parentContainer.appendChild(this._recommendationsContainer); + + this.renderGraphIndexAndCost(); + this.renderQueryText(); + this.renderRecommendations(); + } + + public set graphIndex(index: number) { + this._graphIndex = index; + this.renderGraphIndexAndCost(); + } + public set relativeCost(cost: number) { + this._relativeCost = cost; + this.renderGraphIndexAndCost(); + } + public set query(query: string) { + this._query = removeLineBreaks(query); + this.renderQueryText(); + } + + public set recommendations(recommendations: azdata.ExecutionPlanRecommendations[]) { + recommendations.forEach(r => { + r.displayString = removeLineBreaks(r.displayString); + }); + this._recommendations = recommendations; + this.renderRecommendations(); + } + + private renderGraphIndexAndCost(): void { + if (this._graphIndex && this._relativeCost) { + this._graphIndexAndCostContainer.innerText = localize( + { + key: 'planHeaderIndexAndCost', + comment: [ + '{0} is the index of the graph in the execution plan tab', + '{1} is the relative cost in percentage of the graph to the rest of the graphs in execution plan tab ' + ] + }, + "Query {0}: Query cost (relative to the script): {1}%", this._graphIndex, this._relativeCost.toFixed(2)); + } + } + + private renderQueryText(): void { + this._queryContainer.innerText = this._query; + } + + private renderRecommendations(): void { + while (this._recommendationsContainer.firstChild) { + this._recommendationsContainer.removeChild(this._recommendationsContainer.firstChild); + } + this._recommendations.forEach(r => { + + const link = new Button(this._recommendationsContainer, { + title: r.displayString, + secondary: true, + }); + + link.label = r.displayString; + + //Enabling on click action for recommendations. It will open the recommendation File + link.onDidClick(e => { + this._instantiationService.invokeFunction(openNewQuery, undefined, r.queryWithDescription, RunQueryOnConnectionMode.none); + }); + }); + + } +} + +export interface PlanHeaderData { + planIndex?: number; + relativeCost?: number; + query?: string; + recommendations?: azdata.ExecutionPlanRecommendations[]; +} diff --git a/src/sql/workbench/contrib/queryplan2/browser/queryPlan.ts b/src/sql/workbench/contrib/queryplan2/browser/queryPlan.ts index 7aa0194590..a6f10fd956 100644 --- a/src/sql/workbench/contrib/queryplan2/browser/queryPlan.ts +++ b/src/sql/workbench/contrib/queryplan2/browser/queryPlan.ts @@ -6,16 +6,33 @@ import 'vs/css!./media/queryPlan2'; import type * as azdata from 'azdata'; import { IPanelView, IPanelTab } from 'sql/base/browser/ui/panel/panel'; -import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; import { dispose } from 'vs/base/common/lifecycle'; import { IConfigurationRegistry, Extensions as ConfigExtensions } from 'vs/platform/configuration/common/configurationRegistry'; import { Registry } from 'vs/platform/registry/common/platform'; -import { ActionBar, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar'; +import { ActionBar } from 'sql/base/browser/ui/taskbar/actionbar'; import * as DOM from 'vs/base/browser/dom'; -import { PropertiesAction } from 'sql/workbench/contrib/queryplan2/browser/actions/propertiesAction'; import * as azdataGraphModule from 'azdataGraph'; -import { escape } from 'sql/base/common/strings'; +import { queryPlanNodeIconPaths } from 'sql/workbench/contrib/queryplan2/browser/constants'; +import { isString } from 'vs/base/common/types'; +import { PlanHeader } from 'sql/workbench/contrib/queryplan2/browser/planHeader'; +import { GraphElementPropertiesView } from 'sql/workbench/contrib/queryplan2/browser/graphElementPropertiesView'; +import { Action } from 'vs/base/common/actions'; +import { Codicon } from 'vs/base/common/codicons'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { openNewQuery } from 'sql/workbench/contrib/query/browser/queryActions'; +import { RunQueryOnConnectionMode } from 'sql/platform/connection/common/connectionManagement'; +import { IColorTheme, ICssStyleCollector, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { editorBackground, foreground, textLinkForeground } from 'vs/platform/theme/common/colorRegistry'; +import { ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar'; +import { ISashEvent, ISashLayoutProvider, Orientation, Sash } from 'vs/base/browser/ui/sash/sash'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; +import { formatDocumentWithSelectedProvider, FormattingMode } from 'vs/editor/contrib/format/format'; +import { Progress } from 'vs/platform/progress/common/progress'; +import { CancellationToken } from 'vs/base/common/cancellation'; + let azdataGraph = azdataGraphModule(); export class QueryPlan2Tab implements IPanelTab { @@ -23,8 +40,10 @@ export class QueryPlan2Tab implements IPanelTab { public readonly identifier = 'QueryPlan2Tab'; public readonly view: QueryPlan2View; - constructor() { - this.view = new QueryPlan2View(); + constructor( + @IInstantiationService instantiationService: IInstantiationService, + ) { + this.view = instantiationService.createInstance(QueryPlan2View); } public dispose() { @@ -34,12 +53,18 @@ export class QueryPlan2Tab implements IPanelTab { public clear() { this.view.clear(); } + } export class QueryPlan2View implements IPanelView { private _qps?: QueryPlan2[] = []; - private _graphs?: azdata.QueryPlanGraph[] = []; - private _container = DOM.$('.qp-container'); + private _graphs?: azdata.ExecutionPlanGraph[] = []; + private _container = DOM.$('.qps-container'); + + constructor( + @IInstantiationService private instantiationService: IInstantiationService, + ) { + } public render(container: HTMLElement): void { container.appendChild(this._container); @@ -63,14 +88,16 @@ export class QueryPlan2View implements IPanelView { DOM.clearNode(this._container); } - public addGraphs(newGraphs: azdata.QueryPlanGraph[]) { - newGraphs.forEach(g => { - const qp2 = new QueryPlan2(this._container, this._qps.length + 1); - qp2.graph = g; - this._qps.push(qp2); - this._graphs.push(g); - this.updateRelativeCosts(); - }); + public addGraphs(newGraphs: azdata.ExecutionPlanGraph[] | undefined) { + if (newGraphs) { + newGraphs.forEach(g => { + const qp2 = this.instantiationService.createInstance(QueryPlan2, this._container, this._qps.length + 1); + qp2.graph = g; + this._qps.push(qp2); + this._graphs.push(g); + this.updateRelativeCosts(); + }); + } } private updateRelativeCosts() { @@ -80,420 +107,130 @@ export class QueryPlan2View implements IPanelView { if (sum > 0) { this._qps.forEach(qp => { - qp.relativeCost = ((qp.graph.root.subTreeCost + qp.graph.root.cost) / sum) * 100; + qp.planHeader.relativeCost = ((qp.graph.root.subTreeCost + qp.graph.root.cost) / sum) * 100; }); } } } -export class QueryPlan2 { - private _graph?: azdata.QueryPlanGraph; - private _relativeCost?: globalThis.Text; - private _actionBar: ActionBar; - private _table: Slick.Grid; - private _dataView: Slick.Data.DataView; - private _container: HTMLElement; - private _actionBarContainer: HTMLElement; - private _data: any[]; - private _iconMap: any = new Object(); - private _iconPaths: any = new Object(); +export class QueryPlan2 implements ISashLayoutProvider { + private _graph?: azdata.ExecutionPlanGraph; - public propContainer: HTMLElement; + private _container: HTMLElement; + + private _actionBarContainer: HTMLElement; + private _actionBar: ActionBar; + + public planHeader: PlanHeader; + private _planContainer: HTMLElement; + private _planHeaderContainer: HTMLElement; + + public propertiesView: GraphElementPropertiesView; + private _propContainer: HTMLElement; + + private _azdataGraphDiagram: any; constructor( parent: HTMLElement, private _graphIndex: number, - + @IInstantiationService public readonly _instantiationService: IInstantiationService, + @IThemeService private readonly _themeService: IThemeService, + @IContextViewService public readonly contextViewService: IContextViewService, + @IUntitledTextEditorService private readonly _untitledEditorService: IUntitledTextEditorService, + @IEditorService private readonly editorService: IEditorService ) { + // parent container for query plan. this._container = DOM.$('.query-plan'); parent.appendChild(this._container); + const sashContainer = DOM.$('.query-plan-sash'); + parent.appendChild(sashContainer); + const sash = new Sash(sashContainer, this, { orientation: Orientation.HORIZONTAL }); + let originalHeight = this._container.offsetHeight; + let originalTableHeight = 0; + let change = 0; + sash.onDidStart((e: ISashEvent) => { + originalHeight = this._container.offsetHeight; + originalTableHeight = this.propertiesView.tableHeight; + }); - this._actionBarContainer = DOM.$('.actionbar-container'); + /** + * Using onDidChange for the smooth resizing of the graph diagram + */ + sash.onDidChange((evt: ISashEvent) => { + change = evt.startY - evt.currentY; + const newHeight = originalHeight - change; + if (newHeight < 200) { + return; + } + this._container.style.height = `${newHeight}px`; + }); + + /** + * Resizing properties window table only once at the end as it is a heavy operation and worsens the smooth resizing experience + */ + sash.onDidEnd(() => { + this.propertiesView.tableHeight = originalTableHeight - change; + }); + + this._planContainer = DOM.$('.plan'); + this._container.appendChild(this._planContainer); + + // container that holds plan header info + this._planHeaderContainer = DOM.$('.header'); + this._planContainer.appendChild(this._planHeaderContainer); + this.planHeader = this._instantiationService.createInstance(PlanHeader, this._planHeaderContainer, { + planIndex: this._graphIndex, + }); + + // container properties + this._propContainer = DOM.$('.properties'); + this._container.appendChild(this._propContainer); + this.propertiesView = new GraphElementPropertiesView(this._propContainer, this._themeService); + + // container that holds actionbar icons + this._actionBarContainer = DOM.$('.action-bar-container'); + this._container.appendChild(this._actionBarContainer); this._actionBar = new ActionBar(this._actionBarContainer, { orientation: ActionsOrientation.VERTICAL, context: this }); - this.propContainer = DOM.$('.properties-container'); - const propHeader = document.createElement('div'); - propHeader.className = 'properties-header'; - propHeader.innerText = 'Properties'; - this.propContainer.appendChild(propHeader); - - this.propContainer.style.visibility = 'hidden'; - - this._dataView = new Slick.Data.DataView({ inlineFilters: false }); - let self = this; - this._data = []; - const TaskNameFormatter = function (row, cell, value, columnDef, dataContext) { - value = escape(value); - const spacer = ''; - const idx = self._dataView.getIdxById(dataContext.id); - if (self._data[idx + 1] && self._data[idx + 1].indent > self._data[idx].indent) { - if (dataContext._collapsed) { - return spacer + ' ' + value; - } else { - return spacer + ' ' + value; - } - } else { - return spacer + ' ' + value; - } - }; - - const columns: Slick.Column[] = [ - { - id: 'name', - name: 'Name', - field: 'name', - width: 250, - editor: Slick.Editors.Text, - formatter: TaskNameFormatter, - headerCssClass: 'prop-table-header' - }, - { - id: 'value', - name: 'Value', - field: 'propValue', - width: 250, - editor: Slick.Editors.Text, - headerCssClass: 'prop-table-header' - } - ]; - - const options: Slick.GridOptions = { - editable: false, - enableAddRow: false, - enableCellNavigation: true, - autoHeight: true - }; - - const tableContainer = DOM.$('.table-container'); - tableContainer.style.height = '500px'; - tableContainer.style.width = '490px'; - this.propContainer.appendChild(tableContainer); - this._table = new Slick.Grid(tableContainer, this._dataView, columns, options); - - this._table.onClick.subscribe((e: any, args) => { - - const item = this._dataView.getItem(args.row); - if (item) { - item._collapsed = !item._collapsed; - this._dataView.updateItem(item.id, item); - } - e.stopImmediatePropagation(); - }); - - this._dataView.setFilter((item) => { - if (item.parent !== null) { - let parent = this._data[item.parent]; - while (parent) { - if (parent._collapsed) { - return false; - } - - parent = this._data[parent.parent]; - } - } - return true; - }); - - - // wire up model events to drive the grid - this._dataView.onRowCountChanged.subscribe((e, args) => { - this._table.updateRowCount(); - this._table.render(); - }); - - this._dataView.onRowsChanged.subscribe((e, args) => { - this._table.invalidateRows(args.rows); - this._table.render(); - }); const actions = [ - new PropertiesAction() + new SaveXml(), + new OpenGraphFile(), + new OpenQueryAction(), + new SearchNodeAction(), + new ZoomInAction(), + new ZoomOutAction(), + new ZoomToFitAction(), + new CustomZoomAction(), + new PropertiesAction(), ]; - this._actionBar.push(actions, { icon: true, label: false }); + this._actionBar.pushAction(actions, { icon: true, label: false }); - this._iconMap['Adaptive_Join_32x.ico'] = 'adaptiveJoin'; - this._iconMap['Assert_32x.ico'] = 'assert'; - this._iconMap['Bitmap_32x.ico'] = 'bitmap'; - this._iconMap['Clustered_index_delete_32x.ico'] = 'clusteredIndexDelete'; - this._iconMap['Clustered_index_insert_32x.ico'] = 'ClusteredIndexInsert'; - this._iconMap['Clustered_index_scan_32x.ico'] = 'ClusteredIndexScan'; - this._iconMap['Clustered_index_seek_32x.ico'] = 'ClusteredIndexSeek'; - this._iconMap['Clustered_index_update_32x.ico'] = 'ClusteredIndexUpdate'; - this._iconMap['Clustered_index_merge_32x.icoo'] = 'ClusteredIndexMerge'; - - this._iconMap['Filter_32x.ico'] = 'filter'; - this._iconMap['Clustered_index_scan_32x.ico'] = 'clusteredIndexScan'; - this._iconMap['Clustered_index_seek_32x.ico'] = 'clusteredIndexSeek'; - this._iconMap['Compute_scalar_32x.ico'] = 'computeScalar'; - this._iconMap['Concatenation_32x.ico'] = 'concatenation'; - - this._iconMap['Concatenation_32x.ico'] = 'concatenation'; - - this._iconMap['Nested_loops_32x.ico'] = 'nestedLoops'; - this._iconMap['Result_32x.ico'] = 'result'; - this._iconMap['Table_spool_32x.ico'] = 'tableSpool'; - this._iconMap['Top_32x.ico'] = 'top'; - let imageBasePath = URI.parse(decodeURI(require.toUrl('./images/icons/'))).fsPath; - this._iconPaths = - { - // generic icons - iteratorCatchAll: imageBasePath + 'iterator_catch_all.png', - - cursorCatchAll: imageBasePath + 'cursor_catch_all.png', - - languageConstructCatchAll: imageBasePath + 'language_construct_catch_all.png', - - // operator icons - adaptiveJoin: imageBasePath + 'adaptive_join.png', - - assert: imageBasePath + 'assert.png', - - bitmap: imageBasePath + 'bitmap.png', - - clusteredIndexDelete: imageBasePath + 'clustered_index_delete.png', - - clusteredIndexInsert: imageBasePath + 'clustered_index_insert.png', - - clusteredIndexScan: imageBasePath + 'clustered_index_scan.png', - - clusteredIndexSeek: imageBasePath + 'clustered_index_seek.png', - - clusteredIndexUpdate: imageBasePath + 'clustered_index_update.png', - - clusteredIndexMerge: imageBasePath + 'clustered_index_merge.png', - - clusteredUpdate: imageBasePath + 'clustered_update.png', - - collapse: imageBasePath + 'collapse.png', - - computeScalar: imageBasePath + 'compute_scalar.png', - - concatenation: imageBasePath + 'concatenation.png', - - constantScan: imageBasePath + 'constant_scan.png', - - deletedScan: imageBasePath + 'deleted_scan.png', - - filter: imageBasePath + 'filter.png', - - hashMatch: imageBasePath + 'hash_match.png', - - indexDelete: imageBasePath + 'index_delete.png', - - indexInsert: imageBasePath + 'index_insert.png', - - indexScan: imageBasePath + 'index_scan.png', - - columnstoreIndexDelete: imageBasePath + 'columnstore_index_delete.png', - - columnstoreIndexInsert: imageBasePath + 'columnstore_index_insert.png', - - columnstoreIndexMerge: imageBasePath + 'columnstore_index_merge.png', - - columnstoreIndexScan: imageBasePath + 'columnstore_index_scan.png', - - columnstoreIndexUpdate: imageBasePath + 'columnstore_index_update.png', - - indexSeek: imageBasePath + 'index_seek.png', - - indexSpool: imageBasePath + 'index_spool.png', - - indexUpdate: imageBasePath + 'index_update.png', - - insertedScan: imageBasePath + 'inserted_scan.png', - - logRowScan: imageBasePath + 'log_row_scan.png', - - mergeInterval: imageBasePath + 'merge_interval.png', - - mergeJoin: imageBasePath + 'merge_join.png', - - nestedLoops: imageBasePath + 'nested_loops.png', - - parallelism: imageBasePath + 'parallelism.png', - - parameterTableScan: imageBasePath + 'parameter_table_scan.png', - - print: imageBasePath + 'print.png', - - rank: imageBasePath + 'rank.png', - - foreignKeyReferencesCheck: imageBasePath + 'foreign_key_references_check.png', - - remoteDelete: imageBasePath + 'remote_delete.png', - - remoteIndexScan: imageBasePath + 'remote_index_scan.png', - - remoteIndexSeek: imageBasePath + 'remote_index_seek.png', - - remoteInsert: imageBasePath + 'remote_insert.png', - - remoteQuery: imageBasePath + 'remote_query.png', - - remoteScan: imageBasePath + 'remote_scan.png', - - remoteUpdate: imageBasePath + 'remote_update.png', - - ridLookup: imageBasePath + 'rid_lookup.png', - - rowCountSpool: imageBasePath + 'row_count_spool.png', - - segment: imageBasePath + 'segment.png', - - sequence: imageBasePath + 'sequence.png', - - sequenceProject: imageBasePath + 'sequence_project.png', - - sort: imageBasePath + 'sort.png', - - split: imageBasePath + 'split.png', - - streamAggregate: imageBasePath + 'stream_aggregate.png', - - switchStatement: imageBasePath + 'switch.png', - - tableValuedFunction: imageBasePath + 'table_valued_function.png', - - tableDelete: imageBasePath + 'table_delete.png', - - tableInsert: imageBasePath + 'table_insert.png', - - tableScan: imageBasePath + 'table_scan.png', - - tableSpool: imageBasePath + 'table_spool.png', - - tableUpdate: imageBasePath + 'table_update.png', - - tableMerge: imageBasePath + 'table_merge.png', - - tfp: imageBasePath + 'predict.png', - - top: imageBasePath + 'top.png', - - udx: imageBasePath + 'udx.png', - - batchHashTableBuild: imageBasePath + 'batch_hash_table_build.png', - - windowSpool: imageBasePath + 'table_spool.png', - - windowAggregate: imageBasePath + 'window_aggregate.png', - - // cursor operators - fetchQuery: imageBasePath + 'fetch_query.png', - - populateQuery: imageBasePath + 'population_query.png', - - refreshQuery: imageBasePath + 'refresh_query.png', - - // shiloh operators - result: imageBasePath + 'result.png', - - aggregate: imageBasePath + 'aggregate.png', - - assign: imageBasePath + 'assign.png', - - arithmeticExpression: imageBasePath + 'arithmetic_expression.png', - - bookmarkLookup: imageBasePath + 'bookmark_lookup.png', - - convert: imageBasePath + 'convert.png', - - declare: imageBasePath + 'declare.png', - - deleteOperator: imageBasePath + 'delete.png', - - dynamic: imageBasePath + 'dynamic.png', - - hashMatchRoot: imageBasePath + 'hash_match_root.png', - - hashMatchTeam: imageBasePath + 'hash_match_team.png', - - ifOperator: imageBasePath + 'if.png', - - insert: imageBasePath + 'insert.png', - - intrinsic: imageBasePath + 'intrinsic.png', - - keyset: imageBasePath + 'keyset.png', - - locate: imageBasePath + 'locate.png', - - populationQuery: imageBasePath + 'population_query.png', - - setFunction: imageBasePath + 'set_function.png', - - snapshot: imageBasePath + 'snapshot.png', - - spool: imageBasePath + 'spool.png', - - tsql: imageBasePath + 'sql.png', - - update: imageBasePath + 'update.png', - - // fake operators - keyLookup: imageBasePath + 'bookmark_lookup.png', - - // PDW operators - apply: imageBasePath + 'apply.png', - - broadcast: imageBasePath + 'broadcast.png', - - computeToControlNode: imageBasePath + 'compute_to_control_node.png', - - constTableGet: imageBasePath + 'const_table_get.png', - - controlToComputeNodes: imageBasePath + 'control_to_compute_nodes.png', - - externalBroadcast: imageBasePath + 'external_broadcast.png', - - externalExport: imageBasePath + 'external_export.png', - - externalLocalStreaming: imageBasePath + 'external_local_streaming.png', - - externalRoundRobin: imageBasePath + 'external_round_robin.png', - - externalShuffle: imageBasePath + 'external_shuffle.png', - - get: imageBasePath + 'get.png', - - groupByApply: imageBasePath + 'apply.png', - - groupByAggregate: imageBasePath + 'group_by_aggregate.png', - - join: imageBasePath + 'join.png', - - localCube: imageBasePath + 'intrinsic.png', - - project: imageBasePath + 'project.png', - - shuffle: imageBasePath + 'shuffle.png', - - singleSourceRoundRobin: imageBasePath + 'single_source_round_robin.png', - - singleSourceShuffle: imageBasePath + 'single_source_shuffle.png', - - trim: imageBasePath + 'trim.png', - - union: imageBasePath + 'union.png', - - unionAll: imageBasePath + 'union_all.png' - }; } - private populate(node: azdata.QueryPlanGraphNode, diagramNode: any): any { + getHorizontalSashTop(sash: Sash): number { + return 0; + } + getHorizontalSashLeft?(sash: Sash): number { + return 0; + } + getHorizontalSashWidth?(sash: Sash): number { + return this._container.clientWidth; + } + private populate(node: azdata.ExecutionPlanNode, diagramNode: any): any { diagramNode.label = node.name; + if (node.properties && node.properties.length > 0) { - diagramNode.metrics = node.properties.map(e => { return { name: e.name, value: e.formattedValue.substring(0, 75) }; }); + diagramNode.metrics = this.populateProperties(node.properties); } - let icon = this._iconMap[node.type]; - if (icon) { - diagramNode.icon = icon; + if (node.type) { + diagramNode.icon = node.type; } if (node.children) { @@ -502,65 +239,218 @@ export class QueryPlan2 { diagramNode.children.push(this.populate(node.children[i], new Object())); } } + + if (node.edges) { + diagramNode.edges = []; + for (let i = 0; i < node.edges.length; i++) { + diagramNode.edges.push(this.populateEdges(node.edges[i], new Object())); + } + } return diagramNode; } - private createPlanDiagram(container: HTMLDivElement): void { - let diagramRoot: any = new Object(); - let graphRoot: azdata.QueryPlanGraphNode = this._graph.root; - this.populate(graphRoot, diagramRoot); + private populateEdges(edge: azdata.ExecutionPlanEdge, diagramEdge: any) { + diagramEdge.label = ''; + diagramEdge.metrics = this.populateProperties(edge.properties); + diagramEdge.weight = Math.max(0.5, Math.min(0.5 + 0.75 * Math.log10(edge.rowCount), 6)); + return diagramEdge; + } - new azdataGraph.azdataQueryPlan(container, diagramRoot, this._iconPaths); + private populateProperties(props: azdata.ExecutionPlanGraphElementProperty[]) { + return props.filter(e => isString(e.value)) + .map(e => { + return { + name: e.name, + value: e.value.toString().substring(0, 75) + }; + }); + } + + private createPlanDiagram(container: HTMLElement): void { + let diagramRoot: any = new Object(); + let graphRoot: azdata.ExecutionPlanNode = this._graph.root; + this.populate(graphRoot, diagramRoot); + this._azdataGraphDiagram = new azdataGraph.azdataQueryPlan(container, diagramRoot, queryPlanNodeIconPaths); + + registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { + const iconBackground = theme.getColor(editorBackground); + if (iconBackground) { + this._azdataGraphDiagram.setIconBackgroundColor(iconBackground); + } + + const iconLabelColor = theme.getColor(foreground); + if (iconLabelColor) { + this._azdataGraphDiagram.setTextFontColor(iconLabelColor); + } + }); } - public set graph(graph: azdata.QueryPlanGraph | undefined) { + public set graph(graph: azdata.ExecutionPlanGraph | undefined) { this._graph = graph; if (this._graph) { - this._container.appendChild(document.createTextNode(localize('queryIndex', "Query {0}: ", this._graphIndex))); - this._relativeCost = document.createTextNode(localize('relativeToTheScript', "(relative to the script):")); - this._container.appendChild(this._relativeCost); - this._container.appendChild(document.createElement('br')); - this._container.appendChild(document.createTextNode(`${graph.query}`)); - let diagramContainer = document.createElement('div'); + this.planHeader.graphIndex = this._graphIndex; + this.planHeader.query = graph.query; + if (graph.recommendations) { + this.planHeader.recommendations = graph.recommendations; + } + let diagramContainer = DOM.$('.diagram'); this.createPlanDiagram(diagramContainer); - this._container.appendChild(diagramContainer); + this._planContainer.appendChild(diagramContainer); - this._container.appendChild(this.propContainer); - this.setData(this._graph.root.properties); - this._container.appendChild(this._actionBarContainer); + this.propertiesView.graphElement = this._graph.root; } } - public get graph(): azdata.QueryPlanGraph | undefined { + public get graph(): azdata.ExecutionPlanGraph | undefined { return this._graph; } - public set relativeCost(newCost: number) { - this._relativeCost.nodeValue = localize('relativeToTheScriptWithCost', "(relative to the script): {0}%", newCost.toFixed(2)); + public openQuery() { + return this._instantiationService.invokeFunction(openNewQuery, undefined, this.graph.query, RunQueryOnConnectionMode.none).then(); } - public setData(props: azdata.QueryPlanGraphElementProperty[]): void { - this._data = []; - props.forEach((p, i) => { - this._data.push({ - id: p.name, - name: p.name, - propValue: p.formattedValue, - _collapsed: true - }); - }); - this._dataView.beginUpdate(); - this._dataView.setItems(this._data); - this._dataView.endUpdate(); - this._dataView.refresh(); - this._table.autosizeColumns(); - this._table.updateRowCount(); - this._table.resizeCanvas(); - this._table.render(); + public async openGraphFile() { + const input = this._untitledEditorService.create({ mode: this.graph.graphFile.graphFileType, initialValue: this.graph.graphFile.graphFileContent }); + await input.resolve(); + await this._instantiationService.invokeFunction(formatDocumentWithSelectedProvider, input.textEditorModel, FormattingMode.Explicit, Progress.None, CancellationToken.None); + input.setDirty(false); + this.editorService.openEditor(input); } } +class OpenQueryAction extends Action { + public static ID = 'qp.OpenQueryAction'; + public static LABEL = localize('openQueryAction', "Open Query"); + + constructor() { + super(OpenQueryAction.ID, OpenQueryAction.LABEL, Codicon.dash.classNames); + } + + public override async run(context: QueryPlan2): Promise { + context.openQuery(); + } +} + +class PropertiesAction extends Action { + public static ID = 'qp.propertiesAction'; + public static LABEL = localize('queryPlanPropertiesActionLabel', "Properties"); + + constructor() { + super(PropertiesAction.ID, PropertiesAction.LABEL, Codicon.book.classNames); + } + + public override async run(context: QueryPlan2): Promise { + context.propertiesView.toggleVisibility(); + } +} + +class ZoomInAction extends Action { + public static ID = 'qp.ZoomInAction'; + public static LABEL = localize('queryPlanZoomInActionLabel', "Zoom In"); + + constructor() { + super(ZoomInAction.ID, ZoomInAction.LABEL, Codicon.zoomIn.classNames); + } + + public override async run(context: QueryPlan2): Promise { + } +} + +class ZoomOutAction extends Action { + public static ID = 'qp.ZoomOutAction'; + public static LABEL = localize('queryPlanZoomOutActionLabel', "Zoom Out"); + + constructor() { + super(ZoomOutAction.ID, ZoomOutAction.LABEL, Codicon.zoomOut.classNames); + } + + public override async run(context: QueryPlan2): Promise { + } +} + +class ZoomToFitAction extends Action { + public static ID = 'qp.FitGraph'; + public static LABEL = localize('queryPlanFitGraphLabel', "Zoom to fit"); + + constructor() { + super(ZoomToFitAction.ID, ZoomToFitAction.LABEL, Codicon.debugStop.classNames); + } + + public override async run(context: QueryPlan2): Promise { + } +} + +class SaveXml extends Action { + public static ID = 'qp.saveXML'; + public static LABEL = localize('queryPlanSavePlanXML', "Save XML"); + + constructor() { + super(SaveXml.ID, SaveXml.LABEL, Codicon.save.classNames); + } + + public override async run(context: QueryPlan2): Promise { + } +} + + +class CustomZoomAction extends Action { + public static ID = 'qp.customZoom'; + public static LABEL = localize('queryPlanCustomZoom', "Custom Zoom"); + + constructor() { + super(CustomZoomAction.ID, CustomZoomAction.LABEL, Codicon.searchStop.classNames); + } + + public override async run(context: QueryPlan2): Promise { + } +} + +class SearchNodeAction extends Action { + public static ID = 'qp.searchNode'; + public static LABEL = localize('queryPlanSearchNodeAction', "SearchNode"); + + constructor() { + super(SearchNodeAction.ID, SearchNodeAction.LABEL, Codicon.search.classNames); + } + + public override async run(context: QueryPlan2): Promise { + } +} + +class OpenGraphFile extends Action { + public static ID = 'qp.openGraphFile'; + public static Label = localize('queryPlanOpenGraphFile', "Open Graph File"); + + constructor() { + super(OpenGraphFile.ID, OpenGraphFile.Label, Codicon.output.classNames); + } + + public override async run(context: QueryPlan2): Promise { + await context.openGraphFile(); + } +} + +registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { + const menuBackgroundColor = theme.getColor(editorBackground); + if (menuBackgroundColor) { + collector.addRule(` + .qps-container .query-plan .plan .plan-action-container .child { + background-color: ${menuBackgroundColor}; + } + `); + } + const recommendationsColor = theme.getColor(textLinkForeground); + if (recommendationsColor) { + collector.addRule(` + .qps-container .query-plan .plan .header .recommendations { + color: ${recommendationsColor}; + } + `); + } +}); + + /** * Registering a feature flag for query plan. * TODO: This should be removed before taking the feature to public preview. diff --git a/src/sql/workbench/services/query/common/queryModel.ts b/src/sql/workbench/services/query/common/queryModel.ts index 3d2fdf6091..9cae28d522 100644 --- a/src/sql/workbench/services/query/common/queryModel.ts +++ b/src/sql/workbench/services/query/common/queryModel.ts @@ -16,7 +16,7 @@ import { EditRevertCellResult, ExecutionPlanOptions, queryeditor, - QueryPlanGraph + ExecutionPlanGraph } from 'azdata'; import { QueryInfo } from 'sql/workbench/services/query/common/queryModelService'; import { IRange } from 'vs/editor/common/core/range'; @@ -34,7 +34,7 @@ export interface IQueryPlanInfo { export interface IQueryPlan2Info { providerId: string; fileUri: string; - planGraphs: QueryPlanGraph[]; + planGraphs: ExecutionPlanGraph[]; } export interface IQueryInfo { diff --git a/src/sql/workbench/services/query/common/queryRunner.ts b/src/sql/workbench/services/query/common/queryRunner.ts index 673776d435..cda05f9c66 100644 --- a/src/sql/workbench/services/query/common/queryRunner.ts +++ b/src/sql/workbench/services/query/common/queryRunner.ts @@ -387,7 +387,7 @@ export default class QueryRunner extends Disposable { } } - public handleQueryPlan2Available(queryPlans: azdata.QueryPlanGraph[] | undefined) { + public handleQueryPlan2Available(queryPlans: azdata.ExecutionPlanGraph[] | undefined) { if (queryPlans) { this._onQueryPlan2Available.fire({ providerId: mssqlProviderName, diff --git a/yarn.lock b/yarn.lock index 2cbb9f36b1..21525bccbc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1831,9 +1831,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.7": - version "0.0.7" - resolved "https://codeload.github.com/Microsoft/azdataGraph/tar.gz/e6c21240186cad9829a729b60e260e6d5b57c316" +"azdataGraph@github:Microsoft/azdataGraph#0.0.9": + version "0.0.9" + resolved "https://codeload.github.com/Microsoft/azdataGraph/tar.gz/4d80cfee29c4761e31b6e4e3612e450ce42b55ae" azure-storage@^2.10.2: version "2.10.2"