diff --git a/extensions/mssql/package.json b/extensions/mssql/package.json index d4f1977d0f..ebbbec72f9 100644 --- a/extensions/mssql/package.json +++ b/extensions/mssql/package.json @@ -514,14 +514,14 @@ { "command": "mssql.designTable", "when": "connectionProvider == MSSQL && nodeType == Table && config.workbench.enablePreviewFeatures", - "group":"connection@3" + "group": "connection@3" }, { "command": "mssql.newTable", "when": "connectionProvider == MSSQL && nodeType == Folder && nodeLabel == Tables && config.workbench.enablePreviewFeatures", - "group":"connection@1" + "group": "connection@1" } - ], + ], "notebook/toolbar": [ { "command": "mssql.exportNotebookToSql", @@ -759,6 +759,9 @@ "connectionProvider": { "providerId": "MSSQL", "displayName": "%mssql.provider.displayName%", + "supportedExecutionPlanFileExtensions": [ + "sqlplan" + ], "iconPath": [ { "id": "mssql:cloud", diff --git a/extensions/mssql/src/contracts.ts b/extensions/mssql/src/contracts.ts index 789e5cf7ce..bbb75fabc2 100644 --- a/extensions/mssql/src/contracts.ts +++ b/extensions/mssql/src/contracts.ts @@ -1114,3 +1114,16 @@ export namespace DisposeTableDesignerRequest { export const type = new RequestType('tabledesigner/dispose'); } // ------------------------------- < Table Designer > ------------------------------------ + + +// ------------------------------- < Execution Plan > ------------------------------------ + +export interface GetExecutionPlanParams { + graphInfo: azdata.executionPlan.ExecutionPlanGraphInfo, +} + +export namespace GetExecutionPlanRequest { + export const type = new RequestType('queryexecutionplan/getexecutionplan'); +} + +// ------------------------------- < Execution Plan > ------------------------------------ diff --git a/extensions/mssql/src/features.ts b/extensions/mssql/src/features.ts index 17d62415c3..e1be7c8f6d 100644 --- a/extensions/mssql/src/features.ts +++ b/extensions/mssql/src/features.ts @@ -1183,3 +1183,48 @@ export class TableDesignerFeature extends SqlOpsFeature { } } + +/** + * Execution Plan Service Feature + * TODO: Move this feature to data protocol client repo once stablized + */ +export class ExecutionPlanServiceFeature extends SqlOpsFeature { + private static readonly messagesTypes: RPCMessageType[] = [ + contracts.GetExecutionPlanRequest.type, + ]; + + constructor(client: SqlOpsDataClient) { + super(client, ExecutionPlanServiceFeature.messagesTypes); + } + + public fillClientCapabilities(capabilities: ClientCapabilities): void { + } + + public initialize(capabilities: ServerCapabilities): void { + this.register(this.messages, { + id: UUID.generateUuid(), + registerOptions: undefined + }); + } + + protected registerProvider(options: undefined): Disposable { + const client = this._client; + + const getExecutionPlan = (planFile: azdata.executionPlan.ExecutionPlanGraphInfo): Thenable => { + const params: contracts.GetExecutionPlanParams = { graphInfo: planFile }; + return client.sendRequest(contracts.GetExecutionPlanRequest.type, params).then( + r => r, + e => { + client.logFailedRequest(contracts.GetExecutionPlanRequest.type, e); + return Promise.reject(e); + } + ); + }; + + return azdata.dataprotocol.registerExecutionPlanProvider({ + providerId: client.providerId, + getExecutionPlan + }); + } +} + diff --git a/extensions/mssql/src/sqlToolsServer.ts b/extensions/mssql/src/sqlToolsServer.ts index c6d333d48d..35d65c9a05 100644 --- a/extensions/mssql/src/sqlToolsServer.ts +++ b/extensions/mssql/src/sqlToolsServer.ts @@ -11,7 +11,7 @@ import * as path from 'path'; import { getCommonLaunchArgsAndCleanupOldLogFiles, getOrDownloadServer } from './utils'; import { Telemetry, LanguageClientErrorHandler } from './telemetry'; import { SqlOpsDataClient, ClientOptions } from 'dataprotocol-client'; -import { TelemetryFeature, AgentServicesFeature, SerializationFeature, AccountFeature, SqlAssessmentServicesFeature, ProfilerFeature, TableDesignerFeature } from './features'; +import { TelemetryFeature, AgentServicesFeature, SerializationFeature, AccountFeature, SqlAssessmentServicesFeature, ProfilerFeature, TableDesignerFeature, ExecutionPlanServiceFeature } from './features'; import { CredentialStore } from './credentialstore/credentialstore'; import { AzureResourceProvider } from './resourceProvider/resourceProvider'; import { SchemaCompareService } from './schemaCompare/schemaCompareService'; @@ -165,7 +165,8 @@ function getClientOptions(context: AppContext): ClientOptions { ProfilerFeature, SqlMigrationService.asFeature(context), SqlCredentialService.asFeature(context), - TableDesignerFeature + TableDesignerFeature, + ExecutionPlanServiceFeature ], outputChannel: new CustomOutputChannel() }; diff --git a/src/sql/azdata.proposed.d.ts b/src/sql/azdata.proposed.d.ts index e00f603fea..3bce6e4fcc 100644 --- a/src/sql/azdata.proposed.d.ts +++ b/src/sql/azdata.proposed.d.ts @@ -601,7 +601,7 @@ declare module 'azdata' { /** * Contains execution plans returned by the database in ResultSets. */ - executionPlans: ExecutionPlanGraph[]; + executionPlans: executionPlan.ExecutionPlanGraph[]; } export interface ObjectMetadata { @@ -633,11 +633,13 @@ declare module 'azdata' { } export enum DataProviderType { - TableDesignerProvider = 'TableDesignerProvider' + TableDesignerProvider = 'TableDesignerProvider', + ExecutionPlanProvider = 'ExecutionPlanProvider' } export namespace dataprotocol { export function registerTableDesignerProvider(provider: designers.TableDesignerProvider): vscode.Disposable; + export function registerExecutionPlanProvider(provider: executionPlan.ExecutionPlanProvider): vscode.Disposable; } export namespace designers { @@ -1111,138 +1113,154 @@ declare module 'azdata' { } } - export interface ExecutionPlanGraph { - /** - * Root of the execution plan tree - */ - root: ExecutionPlanNode; - /** - * Underlying query for the execution plan graph. - */ - query: string; - /** - * String representation of graph - */ - graphFile: ExecutionPlanGraphFile; - /** - * Query recommendations for optimizing performance - */ - recommendations: ExecutionPlanRecommendations[]; - } + export namespace executionPlan { + export interface ExecutionPlanGraph { + /** + * Root of the execution plan tree + */ + root: ExecutionPlanNode; + /** + * Underlying query for the execution plan graph. + */ + query: string; + /** + * String representation of graph + */ + graphFile: ExecutionPlanGraphInfo; + /** + * Query recommendations for optimizing performance + */ + recommendations: ExecutionPlanRecommendations[]; + } - export interface ExecutionPlanNode { - /** - * Type of the node. This property determines the icon that is displayed for it - */ - type: string; - /** - * 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 take by the node operation in milliseconds - */ - elapsedTimeInMs: number; - /** - * Node properties to be shown in the tooltip - */ - properties: ExecutionPlanGraphElementProperty[]; - /** - * Display name for the node - */ - name: string; - /** - * Description associated with the node. - */ - description: string; - /** - * Subtext displayed under the node name - */ - subtext: string[]; - /** - * Direct children of the nodes. - */ - children: ExecutionPlanNode[]; - /** - * Edges corresponding to the children. - */ - edges: ExecutionPlanEdge[]; - } + export interface ExecutionPlanNode { + /** + * Type of the node. This property determines the icon that is displayed for it + */ + type: string; + /** + * 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 take by the node operation in milliseconds + */ + elapsedTimeInMs: number; + /** + * Node properties to be shown in the tooltip + */ + properties: ExecutionPlanGraphElementProperty[]; + /** + * Display name for the node + */ + name: string; + /** + * Description associated with the node. + */ + description: string; + /** + * Subtext displayed under the node name + */ + subtext: string[]; + /** + * Direct children of the nodes. + */ + children: ExecutionPlanNode[]; + /** + * Edges corresponding to the children. + */ + edges: ExecutionPlanEdge[]; + } - export interface ExecutionPlanEdge { - /** - * Count of the rows returned by the subtree of the edge. - */ - rowCount: number; - /** - * Size of the rows returned by the subtree of the edge. - */ - rowSize: number; - /** - * Edge properties to be shown in the tooltip. - */ - properties: ExecutionPlanGraphElementProperty[] - } + export interface ExecutionPlanEdge { + /** + * Count of the rows returned by the subtree of the edge. + */ + rowCount: number; + /** + * Size of the rows returned by the subtree of the edge. + */ + rowSize: number; + /** + * Edge properties to be shown in the tooltip. + */ + properties: ExecutionPlanGraphElementProperty[] + } - export interface ExecutionPlanGraphElementProperty { - /** - * Name of the property - */ - name: string; - /** - * value for the property - */ - value: string | ExecutionPlanGraphElementProperty[]; - /** - * Flag to show/hide props in tooltip - */ - showInTooltip: boolean; - /** - * Display order of property - */ - displayOrder: number; - /** - * Flag to indicate if the property has a longer value so that it will be shown at the bottom of the tooltip - */ - positionAtBottom: boolean; - /** - * Display value of property to show in tooltip and other UI element. - */ - displayValue: string; - } + export interface ExecutionPlanGraphElementProperty { + /** + * Name of the property + */ + name: string; + /** + * value for the property + */ + value: string | ExecutionPlanGraphElementProperty[]; + /** + * Flag to show/hide props in tooltip + */ + showInTooltip: boolean; + /** + * Display order of property + */ + displayOrder: number; + /** + * Flag to indicate if the property has a longer value so that it will be shown at the bottom of the tooltip + */ + positionAtBottom: boolean; + /** + * Display value of property to show in tooltip and other UI element. + */ + displayValue: string; + } - 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 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; + export interface ExecutionPlanGraphInfo { + /** + * 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; + } + + export interface GetExecutionPlanResult extends ResultStatus { + graphs: ExecutionPlanGraph[] + } + + export interface ExecutionPlanProvider extends DataProvider { + // execution plan service methods + + /** + * Gets the execution plan graph from the provider for a given plan file + * @param planFile file that contains the execution plan + */ + getExecutionPlan(planFile: ExecutionPlanGraphInfo): Thenable; + } } /** diff --git a/src/sql/platform/capabilities/common/capabilitiesService.ts b/src/sql/platform/capabilities/common/capabilitiesService.ts index 38c29519a9..41a8a75066 100644 --- a/src/sql/platform/capabilities/common/capabilitiesService.ts +++ b/src/sql/platform/capabilities/common/capabilitiesService.ts @@ -28,6 +28,7 @@ export interface ConnectionProviderProperties { azureResource?: string; connectionOptions: azdata.ConnectionOption[]; isQueryProvider?: boolean; + supportedExecutionPlanFileExtensions?: string[]; } export interface ProviderFeatures { diff --git a/src/sql/workbench/api/browser/mainThreadDataProtocol.ts b/src/sql/workbench/api/browser/mainThreadDataProtocol.ts index ebcaa5733c..76088c38ac 100644 --- a/src/sql/workbench/api/browser/mainThreadDataProtocol.ts +++ b/src/sql/workbench/api/browser/mainThreadDataProtocol.ts @@ -31,6 +31,7 @@ import { IDataGridProviderService } from 'sql/workbench/services/dataGridProvide import { IAdsTelemetryService, ITelemetryEventProperties } from 'sql/platform/telemetry/common/telemetry'; import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys'; import { ITableDesignerService } from 'sql/workbench/services/tableDesigner/common/interface'; +import { IExecutionPlanService } from 'sql/workbench/services/executionPlan/common/interfaces'; /** * Main thread class for handling data protocol management registration. @@ -61,7 +62,8 @@ export class MainThreadDataProtocol extends Disposable implements MainThreadData @IAssessmentService private _assessmentService: IAssessmentService, @IDataGridProviderService private _dataGridProviderService: IDataGridProviderService, @IAdsTelemetryService private _telemetryService: IAdsTelemetryService, - @ITableDesignerService private _tableDesignerService: ITableDesignerService + @ITableDesignerService private _tableDesignerService: ITableDesignerService, + @IExecutionPlanService private _executionPlanService: IExecutionPlanService ) { super(); if (extHostContext) { @@ -550,6 +552,12 @@ export class MainThreadDataProtocol extends Disposable implements MainThreadData return undefined; } + public $registerExecutionPlanProvider(providerId: string, handle: number): void { + this._executionPlanService.registerProvider(providerId, { + getExecutionPlan: (planFile: azdata.executionPlan.ExecutionPlanGraphInfo) => this._proxy.$getExecutionPlan(handle, planFile) + }); + } + // Connection Management handlers public $onConnectionComplete(handle: number, connectionInfoSummary: azdata.ConnectionInfoSummary): void { this._connectionManagementService.onConnectionComplete(handle, connectionInfoSummary); diff --git a/src/sql/workbench/api/common/extHostDataProtocol.ts b/src/sql/workbench/api/common/extHostDataProtocol.ts index c4ee2f511e..61115b053e 100644 --- a/src/sql/workbench/api/common/extHostDataProtocol.ts +++ b/src/sql/workbench/api/common/extHostDataProtocol.ts @@ -201,6 +201,12 @@ export class ExtHostDataProtocol extends ExtHostDataProtocolShape { return rt; } + $registerExecutionPlanProvider(provider: azdata.executionPlan.ExecutionPlanProvider): vscode.Disposable { + let rt = this.registerProvider(provider, DataProviderType.ExecutionPlanProvider); + this._proxy.$registerExecutionPlanProvider(provider.providerId, provider.handle); + return rt; + } + // Capabilities Discovery handlers override $getServerCapabilities(handle: number, client: azdata.DataProtocolClientCapabilities): Thenable { return this._resolveProvider(handle).getServerCapabilities(client); @@ -921,4 +927,10 @@ export class ExtHostDataProtocol extends ExtHostDataProtocolShape { this._proxy.$openTableDesigner(providerId, tableInfo, telemetryInfo); return Promise.resolve(); } + + // Execution Plan + + public override $getExecutionPlan(handle: number, planFile: azdata.executionPlan.ExecutionPlanGraphInfo): Thenable { + return this._resolveProvider(handle).getExecutionPlan(planFile); + } } diff --git a/src/sql/workbench/api/common/sqlExtHost.api.impl.ts b/src/sql/workbench/api/common/sqlExtHost.api.impl.ts index df62fac4eb..acfcc5f366 100644 --- a/src/sql/workbench/api/common/sqlExtHost.api.impl.ts +++ b/src/sql/workbench/api/common/sqlExtHost.api.impl.ts @@ -385,6 +385,10 @@ export function createAdsApiFactory(accessor: ServicesAccessor): IAdsExtensionAp return extHostDataProvider.$registerTableDesignerProvider(provider); }; + let registerExecutionPlanProvider = (provider: azdata.executionPlan.ExecutionPlanProvider): vscode.Disposable => { + return extHostDataProvider.$registerExecutionPlanProvider(provider); + }; + // namespace: dataprotocol const dataprotocol: typeof azdata.dataprotocol = { registerBackupProvider, @@ -406,6 +410,7 @@ export function createAdsApiFactory(accessor: ServicesAccessor): IAdsExtensionAp registerSqlAssessmentServicesProvider, registerDataGridProvider, registerTableDesignerProvider, + registerExecutionPlanProvider: registerExecutionPlanProvider, onDidChangeLanguageFlavor(listener: (e: azdata.DidChangeLanguageFlavorParams) => any, thisArgs?: any, disposables?: extHostTypes.Disposable[]) { return extHostDataProvider.onDidChangeLanguageFlavor(listener, thisArgs, disposables); }, diff --git a/src/sql/workbench/api/common/sqlExtHost.protocol.ts b/src/sql/workbench/api/common/sqlExtHost.protocol.ts index 35244d32f5..d205e2c0da 100644 --- a/src/sql/workbench/api/common/sqlExtHost.protocol.ts +++ b/src/sql/workbench/api/common/sqlExtHost.protocol.ts @@ -567,6 +567,11 @@ export abstract class ExtHostDataProtocolShape { * Open a new instance of table designer. */ $openTableDesigner(providerId: string, tableInfo: azdata.designers.TableInfo, telemetryInfo?: ITelemetryEventProperties): void { throw ni(); } + + /** + * Gets the generic execution plan graph for a plan file. + */ + $getExecutionPlan(handle: number, planFile: azdata.executionPlan.ExecutionPlanGraphInfo): Thenable { throw ni(); } } /** @@ -637,6 +642,7 @@ export interface MainThreadDataProtocolShape extends IDisposable { $registerSqlAssessmentServicesProvider(providerId: string, handle: number): Promise; $registerDataGridProvider(providerId: string, title: string, handle: number): void; $registerTableDesignerProvider(providerId: string, handle: number): Promise; + $registerExecutionPlanProvider(providerId: string, handle: number): void; $unregisterProvider(handle: number): Promise; $onConnectionComplete(handle: number, connectionInfoSummary: azdata.ConnectionInfoSummary): void; $onIntelliSenseCacheComplete(handle: number, connectionUri: string): void; @@ -661,7 +667,6 @@ export interface MainThreadDataProtocolShape extends IDisposable { $onProfilerSessionCreated(handle: number, response: azdata.ProfilerSessionCreatedParams): void; $onJobDataUpdated(handle: Number): void; $openTableDesigner(providerId: string, tableInfo: azdata.designers.TableInfo, telemetryInfo?: ITelemetryEventProperties): void; - /** * Callback when a session has completed initialization */ diff --git a/src/sql/workbench/api/common/sqlExtHostTypes.ts b/src/sql/workbench/api/common/sqlExtHostTypes.ts index 2ae858c55c..c2ec95a67a 100644 --- a/src/sql/workbench/api/common/sqlExtHostTypes.ts +++ b/src/sql/workbench/api/common/sqlExtHostTypes.ts @@ -382,7 +382,8 @@ export enum DataProviderType { IconProvider = 'IconProvider', SqlAssessmentServicesProvider = 'SqlAssessmentServicesProvider', DataGridProvider = 'DataGridProvider', - TableDesignerProvider = 'TableDesignerProvider' + TableDesignerProvider = 'TableDesignerProvider', + ExecutionPlanProvider = 'ExecutionPlanProvider' } export enum DeclarativeDataType { diff --git a/src/sql/workbench/common/editor/query/executionPlanState.ts b/src/sql/workbench/common/editor/query/executionPlanState.ts index 69024f1ad5..1baffd6152 100644 --- a/src/sql/workbench/common/editor/query/executionPlanState.ts +++ b/src/sql/workbench/common/editor/query/executionPlanState.ts @@ -6,7 +6,7 @@ import type * as azdata from 'azdata'; export class ExecutionPlanState { - graphs: azdata.ExecutionPlanGraph[] = []; + graphs: azdata.executionPlan.ExecutionPlanGraph[] = []; clearExecutionPlanState() { this.graphs = []; } diff --git a/src/sql/workbench/contrib/executionPlan/browser/executionPlan.ts b/src/sql/workbench/contrib/executionPlan/browser/executionPlan.ts index a72834eb46..75356b1e87 100644 --- a/src/sql/workbench/contrib/executionPlan/browser/executionPlan.ts +++ b/src/sql/workbench/contrib/executionPlan/browser/executionPlan.ts @@ -39,17 +39,20 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { URI } from 'vs/base/common/uri'; import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfigurationService'; +import { IExecutionPlanService } from 'sql/workbench/services/executionPlan/common/interfaces'; +import { LoadingSpinner } from 'sql/base/browser/ui/loadingSpinner/loadingSpinner'; +import { InfoBox } from 'sql/base/browser/ui/infoBox/infoBox'; let azdataGraph = azdataGraphModule(); -export interface InternalExecutionPlanNode extends azdata.ExecutionPlanNode { +export interface InternalExecutionPlanNode extends azdata.executionPlan.ExecutionPlanNode { /** * Unique internal id given to graph node by ADS. */ id?: string; } -export interface InternalExecutionPlanEdge extends azdata.ExecutionPlanEdge { +export interface InternalExecutionPlanEdge extends azdata.executionPlan.ExecutionPlanEdge { /** * Unique internal id given to graph edge by ADS. */ @@ -78,17 +81,22 @@ export class ExecutionPlanTab implements IPanelTab { } export class ExecutionPlanView implements IPanelView { + private _loadingSpinner: LoadingSpinner; + private _loadingErrorInfoBox: InfoBox; private _eps?: ExecutionPlan[] = []; - private _graphs?: azdata.ExecutionPlanGraph[] = []; + private _graphs?: azdata.executionPlan.ExecutionPlanGraph[] = []; private _container = DOM.$('.eps-container'); + private _planCache: Map = new Map(); + constructor( @IInstantiationService private instantiationService: IInstantiationService, + @IExecutionPlanService private executionPlanService: IExecutionPlanService ) { } - public render(container: HTMLElement): void { - container.appendChild(this._container); + public render(parent: HTMLElement): void { + parent.appendChild(this._container); } dispose() { @@ -106,7 +114,11 @@ export class ExecutionPlanView implements IPanelView { DOM.clearNode(this._container); } - public addGraphs(newGraphs: azdata.ExecutionPlanGraph[] | undefined) { + /** + * Adds executionPlanGraph to the graph controller. + * @param newGraphs ExecutionPlanGraphs to be added. + */ + public addGraphs(newGraphs: azdata.executionPlan.ExecutionPlanGraph[] | undefined) { if (newGraphs) { newGraphs.forEach(g => { const ep = this.instantiationService.createInstance(ExecutionPlan, this._container, this._eps.length + 1); @@ -118,6 +130,45 @@ export class ExecutionPlanView implements IPanelView { } } + /** + * Loads the graph file by converting the file to generic executionPlan graphs. + * This feature requires the right providers to be registered that can handle + * the graphFileType in the graphFile + * Please note: this method clears the existing graph in the graph control + * @param graphFile graph file to be loaded. + * @returns + */ + public async loadGraphFile(graphFile: azdata.executionPlan.ExecutionPlanGraphInfo) { + this.clear(); + this._loadingSpinner = new LoadingSpinner(this._container, { showText: true, fullSize: true }); + this._loadingSpinner.loadingMessage = localize('loadingExecutionPlanFile', "Generating execution plans"); + try { + this._loadingSpinner.loading = true; + if (this._planCache.has(graphFile.graphFileContent)) { + this.addGraphs(this._planCache.get(graphFile.graphFileContent)); + return; + } else { + const graphs = (await this.executionPlanService.getExecutionPlan({ + graphFileContent: graphFile.graphFileContent, + graphFileType: graphFile.graphFileType + })).graphs; + this.addGraphs(graphs); + this._planCache.set(graphFile.graphFileContent, graphs); + } + this._loadingSpinner.loadingCompletedMessage = localize('executionPlanFileLoadingComplete', "Execution plans are generated"); + } catch (e) { + this._loadingErrorInfoBox = new InfoBox(this._container, { + text: e.toString(), + style: 'error', + isClickable: false + }); + this._loadingErrorInfoBox.isClickable = false; + this._loadingSpinner.loadingCompletedMessage = localize('executionPlanFileLoadingFailed', "Failed to load execution plan"); + } finally { + this._loadingSpinner.loading = false; + } + } + private updateRelativeCosts() { const sum = this._graphs.reduce((prevCost: number, cg) => { return prevCost += cg.root.subTreeCost + cg.root.cost; @@ -132,7 +183,7 @@ export class ExecutionPlanView implements IPanelView { } export class ExecutionPlan implements ISashLayoutProvider { - private _graphModel?: azdata.ExecutionPlanGraph; + private _graphModel?: azdata.executionPlan.ExecutionPlanGraph; private _container: HTMLElement; @@ -327,7 +378,7 @@ export class ExecutionPlan implements ISashLayoutProvider { return diagramEdge; } - private populateProperties(props: azdata.ExecutionPlanGraphElementProperty[]) { + private populateProperties(props: azdata.executionPlan.ExecutionPlanGraphElementProperty[]) { return props.filter(e => isString(e.displayValue) && e.showInTooltip) .sort((a, b) => a.displayOrder - b.displayOrder) .map(e => { @@ -347,7 +398,7 @@ export class ExecutionPlan implements ISashLayoutProvider { private createPlanDiagram(container: HTMLElement) { let diagramRoot: any = new Object(); - let graphRoot: azdata.ExecutionPlanNode = this._graphModel.root; + let graphRoot: azdata.executionPlan.ExecutionPlanNode = this._graphModel.root; this.populate(graphRoot, diagramRoot); this.azdataGraphDiagram = new azdataGraph.azdataQueryPlan(container, diagramRoot, executionPlanNodeIconPaths); @@ -385,7 +436,7 @@ export class ExecutionPlan implements ISashLayoutProvider { } - public set graphModel(graph: azdata.ExecutionPlanGraph | undefined) { + public set graphModel(graph: azdata.executionPlan.ExecutionPlanGraph | undefined) { this._graphModel = graph; if (this._graphModel) { this.planHeader.graphIndex = this._graphIndex; @@ -420,7 +471,7 @@ export class ExecutionPlan implements ISashLayoutProvider { } } - public get graphModel(): azdata.ExecutionPlanGraph | undefined { + public get graphModel(): azdata.executionPlan.ExecutionPlanGraph | undefined { return this._graphModel; } diff --git a/src/sql/workbench/contrib/executionPlan/browser/executionPlanContribution.ts b/src/sql/workbench/contrib/executionPlan/browser/executionPlanContribution.ts new file mode 100644 index 0000000000..52c8778419 --- /dev/null +++ b/src/sql/workbench/contrib/executionPlan/browser/executionPlanContribution.ts @@ -0,0 +1,78 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { EditorPaneDescriptor, IEditorPaneRegistry } from 'vs/workbench/browser/editor'; +import { EditorExtensions } from 'vs/workbench/common/editor'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { IEditorResolverService, RegisteredEditorPriority } from 'vs/workbench/services/editor/common/editorResolverService'; +import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IExecutionPlanService } from 'sql/workbench/services/executionPlan/common/interfaces'; +import { ExecutionPlanInput } from 'sql/workbench/contrib/executionPlan/common/executionPlanInput'; +import { ExecutionPlanEditor } from 'sql/workbench/contrib/executionPlan/browser/executionPlanEditor'; +import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService'; + +// Execution Plan editor registration + +const executionPlanEditorDescriptor = EditorPaneDescriptor.create( + ExecutionPlanEditor, + ExecutionPlanEditor.ID, + ExecutionPlanEditor.LABEL +); + +Registry.as(EditorExtensions.EditorPane) + .registerEditorPane(executionPlanEditorDescriptor, [new SyncDescriptor(ExecutionPlanInput)]); + +export class ExecutionPlanEditorOverrideContribution extends Disposable implements IWorkbenchContribution { + constructor( + @IInstantiationService private _instantiationService: IInstantiationService, + @IEditorResolverService private _editorResolverService: IEditorResolverService, + @IExecutionPlanService private _executionPlanService: IExecutionPlanService, + @ICapabilitiesService private _capabilitiesService: ICapabilitiesService, + ) { + super(); + this.registerEditorOverride(); + + this._capabilitiesService.onCapabilitiesRegistered(e => { + const newFileFormats = this._executionPlanService.getSupportedExecutionPlanExtensionsForProvider(e.id); + if (newFileFormats?.length > 0) { + this._editorResolverService.updateUserAssociations(this.getGlobForFileExtensions(newFileFormats), ExecutionPlanEditor.ID); // Registering new file formats when new providers are registered. + } + }); + } + + public registerEditorOverride(): void { + const supportedFileFormats: string[] = []; + Object.keys(this._capabilitiesService.providers).forEach(e => { + if (this._capabilitiesService.providers[e]?.connection?.supportedExecutionPlanFileExtensions) { + supportedFileFormats.push(... this._capabilitiesService.providers[e].connection.supportedExecutionPlanFileExtensions); + } + }); + + this._editorResolverService.registerEditor( + this.getGlobForFileExtensions(supportedFileFormats), + { + id: ExecutionPlanEditor.ID, + label: ExecutionPlanEditor.LABEL, + priority: RegisteredEditorPriority.builtin + }, + {}, + (editorInput, group) => { + const executionPlanInput = this._instantiationService.createInstance(ExecutionPlanInput, editorInput.resource); + return { editor: executionPlanInput, options: editorInput.options, group: group }; + } + ); + } + + private getGlobForFileExtensions(extensions: string[]): string { + return extensions?.length === 0 ? '' : `*.{${extensions.join()}}`; + } +} + +Registry.as(WorkbenchExtensions.Workbench) + .registerWorkbenchContribution(ExecutionPlanEditorOverrideContribution, LifecyclePhase.Restored); diff --git a/src/sql/workbench/contrib/executionPlan/browser/executionPlanEditor.ts b/src/sql/workbench/contrib/executionPlan/browser/executionPlanEditor.ts new file mode 100644 index 0000000000..7808268ed0 --- /dev/null +++ b/src/sql/workbench/contrib/executionPlan/browser/executionPlanEditor.ts @@ -0,0 +1,64 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { localize } from 'vs/nls'; +import { IEditorOpenContext } from 'vs/workbench/common/editor'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { ExecutionPlanInput } from 'sql/workbench/contrib/executionPlan/common/executionPlanInput'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { IEditorOptions } from 'vs/platform/editor/common/editor'; +import { ExecutionPlanView } from 'sql/workbench/contrib/executionPlan/browser/executionPlan'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; + +export class ExecutionPlanEditor extends EditorPane { + + public static ID: string = 'workbench.editor.executionplan'; + public static LABEL: string = localize('executionPlanEditor', "Query Execution Plan Editor"); + + private view: ExecutionPlanView; + + constructor( + @IInstantiationService instantiationService: IInstantiationService, + @ITelemetryService telemetryService: ITelemetryService, + @IThemeService themeService: IThemeService, + @IStorageService storageService: IStorageService, + ) { + super(ExecutionPlanEditor.ID, telemetryService, themeService, storageService); + this.view = this._register(instantiationService.createInstance(ExecutionPlanView)); + } + + /** + * Called to create the editor in the parent element. + */ + public createEditor(parent: HTMLElement): void { + //Enable scrollbars when drawing area is larger than viewport + parent.style.overflow = 'auto'; + this.view.render(parent); + } + + /** + * Updates the internal variable keeping track of the editor's size, and re-calculates the sash position. + * To be called when the container of this editor changes size. + */ + public layout(dimension: DOM.Dimension): void { + this.view.layout(dimension); + } + + public override async setInput(input: ExecutionPlanInput, options: IEditorOptions, context: IEditorOpenContext): Promise { + if (this.input instanceof ExecutionPlanInput && this.input.matches(input)) { + return Promise.resolve(undefined); + } + await input.resolve(); + await super.setInput(input, options, context, CancellationToken.None); + this.view.loadGraphFile({ + graphFileContent: input.content, + graphFileType: input.getFileExtension().replace('.', '') + }); + } +} diff --git a/src/sql/workbench/contrib/executionPlan/browser/executionPlanPropertiesView.ts b/src/sql/workbench/contrib/executionPlan/browser/executionPlanPropertiesView.ts index fe186fec5f..aa264d08f3 100644 --- a/src/sql/workbench/contrib/executionPlan/browser/executionPlanPropertiesView.ts +++ b/src/sql/workbench/contrib/executionPlan/browser/executionPlanPropertiesView.ts @@ -126,7 +126,7 @@ export class ExecutionPlanPropertiesView { attachTableStyler(this._table, this._themeService); } - public set graphElement(element: azdata.ExecutionPlanNode | azdata.ExecutionPlanEdge) { + public set graphElement(element: azdata.executionPlan.ExecutionPlanNode | azdata.executionPlan.ExecutionPlanEdge) { this._model.graphElement = element; this.sortPropertiesByImportance(); this.renderView(); @@ -186,7 +186,7 @@ export class ExecutionPlanPropertiesView { private renderView(): void { if (this._model.graphElement) { - const nodeName = (this._model.graphElement).name; + const nodeName = (this._model.graphElement).name; this._operationName.innerText = nodeName ? removeLineBreaks(nodeName) : localize('executionPlanPropertiesEdgeOperationName', "Edge"); //since edges do not have names like node, we set the operation name to 'Edge' } this._tableContainer.scrollTo(0, 0); @@ -201,7 +201,7 @@ export class ExecutionPlanPropertiesView { this._table.resizeCanvas(); } - private convertPropertiesToTableRows(props: azdata.ExecutionPlanGraphElementProperty[], parentIndex: number, indent: number, rows: { [key: string]: string }[] = []): { [key: string]: string }[] { + private convertPropertiesToTableRows(props: azdata.executionPlan.ExecutionPlanGraphElementProperty[], parentIndex: number, indent: number, rows: { [key: string]: string }[] = []): { [key: string]: string }[] { if (!props) { return rows; } @@ -228,7 +228,7 @@ export class ExecutionPlanPropertiesView { } export interface GraphElementPropertyViewData { - graphElement: azdata.ExecutionPlanNode | azdata.ExecutionPlanEdge; + graphElement: azdata.executionPlan.ExecutionPlanNode | azdata.executionPlan.ExecutionPlanEdge; } export class ClosePropertyViewAction extends Action { diff --git a/src/sql/workbench/contrib/executionPlan/browser/planHeader.ts b/src/sql/workbench/contrib/executionPlan/browser/planHeader.ts index 37036ee966..4cbf207bf5 100644 --- a/src/sql/workbench/contrib/executionPlan/browser/planHeader.ts +++ b/src/sql/workbench/contrib/executionPlan/browser/planHeader.ts @@ -22,7 +22,7 @@ export class PlanHeader { private _query: string; private _queryContainer: HTMLElement; // container that holds query text - private _recommendations: azdata.ExecutionPlanRecommendations[]; + private _recommendations: azdata.executionPlan.ExecutionPlanRecommendations[]; private _recommendationsContainer: HTMLElement; // container that holds graph recommendations public constructor( @@ -61,7 +61,7 @@ export class PlanHeader { this.renderQueryText(); } - public set recommendations(recommendations: azdata.ExecutionPlanRecommendations[]) { + public set recommendations(recommendations: azdata.executionPlan.ExecutionPlanRecommendations[]) { recommendations.forEach(r => { r.displayString = removeLineBreaks(r.displayString); }); @@ -113,5 +113,5 @@ export interface PlanHeaderData { planIndex?: number; relativeCost?: number; query?: string; - recommendations?: azdata.ExecutionPlanRecommendations[]; + recommendations?: azdata.executionPlan.ExecutionPlanRecommendations[]; } diff --git a/src/sql/workbench/contrib/executionPlan/common/executionPlanInput.ts b/src/sql/workbench/contrib/executionPlan/common/executionPlanInput.ts new file mode 100644 index 0000000000..b80dbc6e9e --- /dev/null +++ b/src/sql/workbench/contrib/executionPlan/common/executionPlanInput.ts @@ -0,0 +1,60 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as path from 'vs/base/common/path'; +import { URI } from 'vs/base/common/uri'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; +import { EditorModel } from 'vs/workbench/common/editor/editorModel'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; + +export class ExecutionPlanInput extends EditorInput { + + public static ID: string = 'workbench.editorinputs.executionplan'; + public static SCHEMA: string = 'executionplan'; + + private _content?: string; + + constructor( + private _uri: URI, + @ITextFileService private readonly _fileService: ITextFileService, + ) { + super(); + } + + override get typeId(): string { + return ExecutionPlanInput.ID; + } + + public override getName(): string { + return path.basename(this._uri.fsPath); + } + + public get content(): string | undefined { + return this._content; + } + + public getUri(): string { + return this._uri.toString(); + } + + public getFileExtension(): string { + return path.extname(this._uri.fsPath); + } + + public supportsSplitEditor(): boolean { + return false; + } + + public override async resolve(refresh?: boolean): Promise { + if (!this._content) { + this._content = (await this._fileService.read(this._uri, { acceptTextOnly: true })).value; + } + return undefined; + } + + get resource(): URI | undefined { + return undefined; + } +} diff --git a/src/sql/workbench/contrib/queryPlan/browser/queryPlan.contribution.ts b/src/sql/workbench/contrib/queryPlan/browser/queryPlan.contribution.ts index 9968d5c0ab..b405c0f13a 100644 --- a/src/sql/workbench/contrib/queryPlan/browser/queryPlan.contribution.ts +++ b/src/sql/workbench/contrib/queryPlan/browser/queryPlan.contribution.ts @@ -37,7 +37,7 @@ export class QueryPlanEditorOverrideContribution extends Disposable implements I private registerEditorOverride(): void { this._editorResolverService.registerEditor( - '*.sqlplan', + '', //Removing sqlplan glob pattern. TODO: to be removed entirely from ADS. { id: QueryPlanEditor.ID, label: QueryPlanEditor.LABEL, diff --git a/src/sql/workbench/services/executionPlan/common/executionPlanService.ts b/src/sql/workbench/services/executionPlan/common/executionPlanService.ts new file mode 100644 index 0000000000..6f09f6b69d --- /dev/null +++ b/src/sql/workbench/services/executionPlan/common/executionPlanService.ts @@ -0,0 +1,131 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from 'vs/nls'; +import type * as azdata from 'azdata'; +import { IExecutionPlanService } from 'sql/workbench/services/executionPlan/common/interfaces'; +import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService'; +import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { Event, Emitter } from 'vs/base/common/event'; + +interface ExecutionPlanProviderRegisteredEvent { + id: string, + provider: azdata.executionPlan.ExecutionPlanProvider +} +export class ExecutionPlanService implements IExecutionPlanService { + private _providers: { [handle: string]: azdata.executionPlan.ExecutionPlanProvider; } = Object.create(null); + private _onProviderRegister: Emitter = new Emitter(); + private _providerRegisterEvent: Event; + constructor( + @ICapabilitiesService private _capabilitiesService: ICapabilitiesService, + @IQuickInputService private _quickInputService: IQuickInputService, + @IExtensionService private _extensionService: IExtensionService + ) { + this._providerRegisterEvent = this._onProviderRegister.event; + } + + /** + * Runs the actions using the provider that supports the fileFormat provided. + * @param fileFormat fileformat of the underlying execution plan file. It is used to get the provider that support it. + * @param action executionPlanService action to be performed. + */ + private async _runAction(fileFormat: string, action: (handler: azdata.executionPlan.ExecutionPlanProvider) => Thenable): Promise { + let providers = Object.keys(this._capabilitiesService.providers); + if (!providers) { + providers = await new Promise(resolve => { + this._capabilitiesService.onCapabilitiesRegistered(e => { + resolve(Object.keys(this._capabilitiesService.providers)); + }); + }); + } + + let epProviders: string[] = []; + for (let i = 0; i < providers.length; i++) { + const providerCapabilities = this._capabilitiesService.getCapabilities(providers[i]); + if (providerCapabilities.connection.supportedExecutionPlanFileExtensions?.includes(fileFormat)) { + epProviders.push(providers[i]); + } + } + + let selectedProvider: string; + + /** + * This handles the case when multiple providers support the same execution plan extension. + * The code shows a quick pick and lets user select the provider they want to open the execution plan file with. + */ + if (epProviders.length > 1) { + const providerQuickPick = this._quickInputService.createQuickPick(); + providerQuickPick.items = epProviders.map(p => { + return { + label: p, + ariaLabel: p + }; + }); + providerQuickPick.placeholder = localize('selectExecutionPlanProvider', "Select a provider to open execution plan"); + + selectedProvider = await new Promise((resolve) => { + providerQuickPick.onDidChangeSelection(e => { + providerQuickPick.hide(); + resolve(e[0].label); + }); + providerQuickPick.show(); + }); + } else { + selectedProvider = epProviders[0]; + } + + + if (!selectedProvider) { + return Promise.reject(new Error(localize('providerIdNotValidError', "Valid provider is required in order to interact with ExecutionPlanService"))); + } + await this._extensionService.whenInstalledExtensionsRegistered(); + let handler = this._providers[selectedProvider]; + if (!handler) { + handler = await new Promise((resolve, reject) => { + this._providerRegisterEvent(e => { + if (e.id === selectedProvider) { + resolve(e.provider); + } + }); + setTimeout(() => { + /** + * Handling a possible edge case where provider registered event + * might have been called before we await for it. + */ + resolve(this._providers[selectedProvider]); + }, 30000); + }); + } + if (handler) { + return Promise.resolve(action(handler)); + } else { + return Promise.reject(new Error(localize('noHandlerRegistered', "No valid execution plan handler is registered"))); + } + } + + registerProvider(providerId: string, provider: azdata.executionPlan.ExecutionPlanProvider): void { + if (this._providers[providerId]) { + throw new Error(`A execution plan provider with id "${providerId}" is already registered`); + } + this._providers[providerId] = provider; + this._onProviderRegister.fire({ + id: providerId, + provider: provider + }); + } + + getExecutionPlan(planFile: azdata.executionPlan.ExecutionPlanGraphInfo): Promise { + return this._runAction(planFile.graphFileType, (runner) => { + return runner.getExecutionPlan(planFile); + }); + } + + getSupportedExecutionPlanExtensionsForProvider(providerId: string): string[] | undefined { + return this._capabilitiesService.getCapabilities(providerId).connection.supportedExecutionPlanFileExtensions; + } + + _serviceBrand: undefined; +} diff --git a/src/sql/workbench/services/executionPlan/common/interfaces.ts b/src/sql/workbench/services/executionPlan/common/interfaces.ts new file mode 100644 index 0000000000..332187f036 --- /dev/null +++ b/src/sql/workbench/services/executionPlan/common/interfaces.ts @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { createDecorator } from 'vs/platform/instantiation/common/instantiation'; + + +export const SERVICE_ID = 'executionPlanService'; + +export const IExecutionPlanService = createDecorator(SERVICE_ID); + +export interface IExecutionPlanService { + _serviceBrand: undefined; + + /** + * Registers an execution plan service provider. + */ + registerProvider(providerId: string, provider: azdata.executionPlan.ExecutionPlanProvider): void; + /** + * Gets an execution plan for the given planFile. + */ + getExecutionPlan(planFile: azdata.executionPlan.ExecutionPlanGraphInfo): Promise; + + /** + * Get execution plan file extensions supported by the provider. + */ + getSupportedExecutionPlanExtensionsForProvider(providerId: string): string[]; +} diff --git a/src/sql/workbench/services/query/common/queryModel.ts b/src/sql/workbench/services/query/common/queryModel.ts index 731342e73e..cd17aac6ee 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, - ExecutionPlanGraph + executionPlan } 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 IExecutionPlanInfo { providerId: string; fileUri: string; - planGraphs: ExecutionPlanGraph[]; + planGraphs: executionPlan.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 ed877a9355..2fe216b7be 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 handleExecutionPlanAvailable(executionPlans: azdata.ExecutionPlanGraph[] | undefined) { + public handleExecutionPlanAvailable(executionPlans: azdata.executionPlan.ExecutionPlanGraph[] | undefined) { if (executionPlans) { this._onExecutionPlanAvailable.fire({ providerId: mssqlProviderName, diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index 9495653652..5b8f25ee53 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -213,6 +213,8 @@ import { DataGridProviderService } from 'sql/workbench/services/dataGridProvider import { IDataGridProviderService } from 'sql/workbench/services/dataGridProvider/common/dataGridProviderService'; import { ITableDesignerService } from 'sql/workbench/services/tableDesigner/common/interface'; import { TableDesignerService } from 'sql/workbench/services/tableDesigner/browser/tableDesignerService'; +import { IExecutionPlanService } from 'sql/workbench/services/executionPlan/common/interfaces'; +import { ExecutionPlanService } from 'sql/workbench/services/executionPlan/common/executionPlanService'; registerSingleton(IDashboardService, DashboardService); registerSingleton(IDashboardViewService, DashboardViewService); @@ -253,7 +255,7 @@ registerSingleton(IOEShimService, OEShimService); registerSingleton(IAssessmentService, AssessmentService); registerSingleton(IDataGridProviderService, DataGridProviderService); registerSingleton(ITableDesignerService, TableDesignerService); - +registerSingleton(IExecutionPlanService, ExecutionPlanService); //#endregion @@ -539,4 +541,7 @@ import 'sql/workbench/contrib/charts/browser/charts.contribution'; // table designer import 'sql/workbench/contrib/tableDesigner/browser/tableDesigner.contribution'; +// execution plan +import 'sql/workbench/contrib/executionPlan/browser/executionPlanContribution'; + //#endregion