Adding Execution Plan Editor to ADS (#18696)

* Pushing Execution Plan Editor

* Renaming class
Handling error

* Awaiting for handlers to be registered

* Addressing some PR comments

* Fixing return type for provider

* Fixing editor id and removing unnecessary overrides

* Adding a namespace

* adding execution plan namespace

* Adding protocol comment

* Fixing if logic

* Fixing error message

* Cleaning up code

* cleanup code

* Adding help comments

* Fixing method call

* Using path.ts to get the base file name

* converting to lambda functions

* Adding comment for run action

* Fixing pr comments

* Fixing editor label

* Fixing doc comments

* Adding some more comments

* Fixign branding in comments
This commit is contained in:
Aasim Khan
2022-03-16 15:07:29 -07:00
committed by GitHub
parent 95980130c8
commit a0c2dc199e
24 changed files with 692 additions and 161 deletions

View File

@@ -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, <azdata.executionPlan.ExecutionPlanProvider>{
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);

View File

@@ -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<azdata.DataProtocolServerCapabilities> {
return this._resolveProvider<azdata.CapabilitiesProvider>(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<azdata.executionPlan.GetExecutionPlanResult> {
return this._resolveProvider<azdata.executionPlan.ExecutionPlanProvider>(handle).getExecutionPlan(planFile);
}
}

View File

@@ -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);
},

View File

@@ -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<azdata.executionPlan.GetExecutionPlanResult> { throw ni(); }
}
/**
@@ -637,6 +642,7 @@ export interface MainThreadDataProtocolShape extends IDisposable {
$registerSqlAssessmentServicesProvider(providerId: string, handle: number): Promise<any>;
$registerDataGridProvider(providerId: string, title: string, handle: number): void;
$registerTableDesignerProvider(providerId: string, handle: number): Promise<any>;
$registerExecutionPlanProvider(providerId: string, handle: number): void;
$unregisterProvider(handle: number): Promise<any>;
$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
*/

View File

@@ -382,7 +382,8 @@ export enum DataProviderType {
IconProvider = 'IconProvider',
SqlAssessmentServicesProvider = 'SqlAssessmentServicesProvider',
DataGridProvider = 'DataGridProvider',
TableDesignerProvider = 'TableDesignerProvider'
TableDesignerProvider = 'TableDesignerProvider',
ExecutionPlanProvider = 'ExecutionPlanProvider'
}
export enum DeclarativeDataType {

View File

@@ -6,7 +6,7 @@
import type * as azdata from 'azdata';
export class ExecutionPlanState {
graphs: azdata.ExecutionPlanGraph[] = [];
graphs: azdata.executionPlan.ExecutionPlanGraph[] = [];
clearExecutionPlanState() {
this.graphs = [];
}

View File

@@ -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<string, azdata.executionPlan.ExecutionPlanGraph[]> = 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;
}

View File

@@ -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<IEditorPaneRegistry>(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<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench)
.registerWorkbenchContribution(ExecutionPlanEditorOverrideContribution, LifecyclePhase.Restored);

View File

@@ -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<void> {
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('.', '')
});
}
}

View File

@@ -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 = (<azdata.ExecutionPlanNode>this._model.graphElement).name;
const nodeName = (<azdata.executionPlan.ExecutionPlanNode>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 {

View File

@@ -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[];
}

View File

@@ -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<EditorModel | undefined> {
if (!this._content) {
this._content = (await this._fileService.read(this._uri, { acceptTextOnly: true })).value;
}
return undefined;
}
get resource(): URI | undefined {
return undefined;
}
}

View File

@@ -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,

View File

@@ -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<ExecutionPlanProviderRegisteredEvent> = new Emitter<ExecutionPlanProviderRegisteredEvent>();
private _providerRegisterEvent: Event<ExecutionPlanProviderRegisteredEvent>;
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<T>(fileFormat: string, action: (handler: azdata.executionPlan.ExecutionPlanProvider) => Thenable<T>): Promise<T> {
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<IQuickPickItem>();
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<azdata.executionPlan.GetExecutionPlanResult> {
return this._runAction(planFile.graphFileType, (runner) => {
return runner.getExecutionPlan(planFile);
});
}
getSupportedExecutionPlanExtensionsForProvider(providerId: string): string[] | undefined {
return this._capabilitiesService.getCapabilities(providerId).connection.supportedExecutionPlanFileExtensions;
}
_serviceBrand: undefined;
}

View File

@@ -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<IExecutionPlanService>(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<azdata.executionPlan.GetExecutionPlanResult>;
/**
* Get execution plan file extensions supported by the provider.
*/
getSupportedExecutionPlanExtensionsForProvider(providerId: string): string[];
}

View File

@@ -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 {

View File

@@ -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,