diff --git a/extensions/mssql/src/contracts.ts b/extensions/mssql/src/contracts.ts index 455609012b..c57b16a0fe 100644 --- a/extensions/mssql/src/contracts.ts +++ b/extensions/mssql/src/contracts.ts @@ -1298,9 +1298,14 @@ export interface StartProfilingParams { ownerUri: string; /** - * Session name + * Session name or full path of XEL file to open */ sessionName: string; + + /** + * Identifies which type of target session name identifies + */ + sessionType: azdata.ProfilingSessionType; } export interface StartProfilingResponse { } diff --git a/extensions/mssql/src/features.ts b/extensions/mssql/src/features.ts index eec1ebcb31..ac318e67d3 100644 --- a/extensions/mssql/src/features.ts +++ b/extensions/mssql/src/features.ts @@ -1001,10 +1001,11 @@ export class ProfilerFeature extends SqlOpsFeature { ); }; - let startSession = (ownerUri: string, sessionName: string): Thenable => { + let startSession = (ownerUri: string, sessionName: string, sessionType: azdata.ProfilingSessionType = azdata.ProfilingSessionType.RemoteSession): Thenable => { let params: contracts.StartProfilingParams = { ownerUri, - sessionName + sessionName, + sessionType }; return client.sendRequest(contracts.StartProfilingRequest.type, params).then( diff --git a/extensions/profiler/package.json b/extensions/profiler/package.json index 9292e846db..9ef53c5ec8 100644 --- a/extensions/profiler/package.json +++ b/extensions/profiler/package.json @@ -51,6 +51,11 @@ "command": "profiler.openCreateSessionDialog", "title": "profiler.contributes.title.openCreateSessionDialog", "category": "%profiler.category%" + }, + { + "command": "profiler.openFile", + "title": "%profiler.contributes.title.openXELFile%", + "category": "%profiler.category%" } ], "menus": { diff --git a/extensions/profiler/package.nls.json b/extensions/profiler/package.nls.json index 84a6395767..4f00636aed 100644 --- a/extensions/profiler/package.nls.json +++ b/extensions/profiler/package.nls.json @@ -5,5 +5,6 @@ "profiler.contributes.title.start": "Start", "profiler.contributes.title.stop": "Stop", "profiler.contributes.title.openCreateSessionDialog": "Create Profiler Season", - "profiler.category": "Profiler" + "profiler.category": "Profiler", + "profiler.contributes.title.openXELFile": "Open XEL File" } diff --git a/src/sql/azdata.proposed.d.ts b/src/sql/azdata.proposed.d.ts index d9277ba130..c683cad71e 100644 --- a/src/sql/azdata.proposed.d.ts +++ b/src/sql/azdata.proposed.d.ts @@ -2028,4 +2028,13 @@ declare module 'azdata' { */ setActiveCell(row: number, column: number): void; } + + export interface ProfilerProvider { + startSession(sessionId: string, sessionName: string, sessionType?: ProfilingSessionType): Thenable; + } + + export enum ProfilingSessionType { + RemoteSession = 0, + LocalFile = 1 + } } diff --git a/src/sql/workbench/api/browser/mainThreadDataProtocol.ts b/src/sql/workbench/api/browser/mainThreadDataProtocol.ts index c202bb657c..2f2cf98e5a 100644 --- a/src/sql/workbench/api/browser/mainThreadDataProtocol.ts +++ b/src/sql/workbench/api/browser/mainThreadDataProtocol.ts @@ -359,8 +359,8 @@ export class MainThreadDataProtocol extends Disposable implements MainThreadData createSession(sessionId: string, createStatement: string, template: azdata.ProfilerSessionTemplate): Thenable { return self._proxy.$createSession(handle, sessionId, createStatement, template); }, - startSession(sessionId: string, sessionName: string): Thenable { - return self._proxy.$startSession(handle, sessionId, sessionName); + startSession(sessionId: string, sessionName: string, sessionType?: azdata.ProfilingSessionType): Thenable { + return self._proxy.$startSession(handle, sessionId, sessionName, sessionType); }, stopSession(sessionId: string): Thenable { return self._proxy.$stopSession(handle, sessionId); diff --git a/src/sql/workbench/api/common/extHostDataProtocol.ts b/src/sql/workbench/api/common/extHostDataProtocol.ts index ca86392cf5..2f22800b5c 100644 --- a/src/sql/workbench/api/common/extHostDataProtocol.ts +++ b/src/sql/workbench/api/common/extHostDataProtocol.ts @@ -686,8 +686,8 @@ export class ExtHostDataProtocol extends ExtHostDataProtocolShape { /** * Start a profiler session */ - public override $startSession(handle: number, sessionId: string, sessionName: string): Thenable { - return this._resolveProvider(handle).startSession(sessionId, sessionName); + public override $startSession(handle: number, sessionId: string, sessionName: string, sessionType?: azdata.ProfilingSessionType): Thenable { + return this._resolveProvider(handle).startSession(sessionId, sessionName, sessionType); } /** @@ -739,7 +739,6 @@ export class ExtHostDataProtocol extends ExtHostDataProtocolShape { this._proxy.$onProfilerSessionCreated(handle, response); } - /** * Agent Job Provider methods */ diff --git a/src/sql/workbench/api/common/sqlExtHost.api.impl.ts b/src/sql/workbench/api/common/sqlExtHost.api.impl.ts index ab244e1f58..ec53ffedcd 100644 --- a/src/sql/workbench/api/common/sqlExtHost.api.impl.ts +++ b/src/sql/workbench/api/common/sqlExtHost.api.impl.ts @@ -687,7 +687,8 @@ export function createAdsApiFactory(accessor: ServicesAccessor): IAdsExtensionAp designers: designers, executionPlan: executionPlan, diagnostics: diagnostics, - env + env, + ProfilingSessionType: sqlExtHostTypes.ProfilingSessionType }; } }; diff --git a/src/sql/workbench/api/common/sqlExtHost.protocol.ts b/src/sql/workbench/api/common/sqlExtHost.protocol.ts index dca78bc5fc..4887b381d3 100644 --- a/src/sql/workbench/api/common/sqlExtHost.protocol.ts +++ b/src/sql/workbench/api/common/sqlExtHost.protocol.ts @@ -385,7 +385,7 @@ export abstract class ExtHostDataProtocolShape { /** * Start a profiler session */ - $startSession(handle: number, sessionId: string, sessionName: string): Thenable { throw ni(); } + $startSession(handle: number, sessionId: string, sessionName: string, sessionType?: azdata.ProfilingSessionType): Thenable { throw ni(); } /** * Stop a profiler session diff --git a/src/sql/workbench/api/common/sqlExtHostTypes.ts b/src/sql/workbench/api/common/sqlExtHostTypes.ts index ba0778d36b..8d745cf2d4 100644 --- a/src/sql/workbench/api/common/sqlExtHostTypes.ts +++ b/src/sql/workbench/api/common/sqlExtHostTypes.ts @@ -219,6 +219,11 @@ export enum StepCompletionAction { GoToStep = 4 } +export enum ProfilingSessionType { + RemoteSession = 0, + LocalFile = 1 +} + export interface CheckBoxInfo { row: number; columnName: string; diff --git a/src/sql/workbench/browser/editor/profiler/profilerInput.ts b/src/sql/workbench/browser/editor/profiler/profilerInput.ts index 4e764f725e..b40277252f 100644 --- a/src/sql/workbench/browser/editor/profiler/profilerInput.ts +++ b/src/sql/workbench/browser/editor/profiler/profilerInput.ts @@ -44,7 +44,8 @@ export class ProfilerInput extends EditorInput implements IProfilerSession { private _filter: ProfilerFilter = { clauses: [] }; constructor( - public connection: IConnectionProfile, + public connection: IConnectionProfile | undefined, + public fileURI: URI | undefined, @IProfilerService private _profilerService: IProfilerService, @INotificationService private _notificationService: INotificationService ) { @@ -63,6 +64,7 @@ export class ProfilerInput extends EditorInput implements IProfilerSession { this._id = id; this.state.change({ isConnected: true }); }); + let searchFn = (val: { [x: string]: string }, exp: string): Array => { let ret = new Array(); for (let i = 0; i < this._columns.length; i++) { @@ -156,6 +158,16 @@ export class ProfilerInput extends EditorInput implements IProfilerSession { } } + public get isFileSession(): boolean { + return !!this.fileURI; + } + + public setConnectionState(isConnected: boolean): void { + this.state.change({ + isConnected: isConnected + }); + } + public setColumns(columns: Array) { this._columns = columns; this._onColumnsChanged.fire(this.columns); @@ -193,7 +205,9 @@ export class ProfilerInput extends EditorInput implements IProfilerSession { } public onSessionStopped(notification: azdata.ProfilerSessionStoppedParams) { - this._notificationService.error(nls.localize("profiler.sessionStopped", "XEvent Profiler Session stopped unexpectedly on the server {0}.", this.connection.serverName)); + if (!this.isFileSession) { // File session do not have serverName, so ignore notification error based off of server + this._notificationService.error(nls.localize("profiler.sessionStopped", "XEvent Profiler Session stopped unexpectedly on the server {0}.", this.connection.serverName)); + } this.state.change({ isStopped: true, diff --git a/src/sql/workbench/contrib/profiler/browser/profilerActions.contribution.ts b/src/sql/workbench/contrib/profiler/browser/profilerActions.contribution.ts index 77bb252652..1196101aa8 100644 --- a/src/sql/workbench/contrib/profiler/browser/profilerActions.contribution.ts +++ b/src/sql/workbench/contrib/profiler/browser/profilerActions.contribution.ts @@ -9,7 +9,7 @@ import { IEditorService, ACTIVE_GROUP } from 'vs/workbench/services/editor/commo import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement'; import { ProfilerInput } from 'sql/workbench/browser/editor/profiler/profilerInput'; import * as TaskUtilities from 'sql/workbench/browser/taskUtilities'; -import { IProfilerService } from 'sql/workbench/services/profiler/browser/interfaces'; +import { IProfilerService, ProfilingSessionType } from 'sql/workbench/services/profiler/browser/interfaces'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { ProfilerEditor } from 'sql/workbench/contrib/profiler/browser/profilerEditor'; import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile'; @@ -18,6 +18,7 @@ import { mssqlProviderName } from 'sql/platform/connection/common/constants'; import { IConnectionDialogService } from 'sql/workbench/services/connection/common/connectionDialogService'; import { IObjectExplorerService } from 'sql/workbench/services/objectExplorer/browser/objectExplorerService'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; CommandsRegistry.registerCommand({ id: 'profiler.newProfiler', @@ -59,7 +60,7 @@ CommandsRegistry.registerCommand({ } if (connectionProfile && connectionProfile.providerName === mssqlProviderName) { - let profilerInput = instantiationService.createInstance(ProfilerInput, connectionProfile); + let profilerInput = instantiationService.createInstance(ProfilerInput, connectionProfile, undefined); editorService.openEditor(profilerInput, { pinned: true }, ACTIVE_GROUP).then(() => Promise.resolve(true)); } }); @@ -93,9 +94,23 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ } else { // clear data when profiler is started profilerInput.data.clear(); - return profilerService.startSession(profilerInput.id, profilerInput.sessionName); + return profilerService.startSession(profilerInput.id, profilerInput.sessionName, ProfilingSessionType.RemoteSession); } } return Promise.resolve(false); } }); + +CommandsRegistry.registerCommand({ + id: 'profiler.openFile', + handler: async (accessor: ServicesAccessor, ...args: any[]) => { + const editorService: IEditorService = accessor.get(IEditorService); + const fileDialogService: IFileDialogService = accessor.get(IFileDialogService); + const profilerService: IProfilerService = accessor.get(IProfilerService); + const instantiationService: IInstantiationService = accessor.get(IInstantiationService) + + const result = await profilerService.openFile(fileDialogService, editorService, instantiationService); + + return result; + } +}); diff --git a/src/sql/workbench/contrib/profiler/browser/profilerActions.ts b/src/sql/workbench/contrib/profiler/browser/profilerActions.ts index ce165b9f4b..1cc11901c9 100644 --- a/src/sql/workbench/contrib/profiler/browser/profilerActions.ts +++ b/src/sql/workbench/contrib/profiler/browser/profilerActions.ts @@ -275,7 +275,7 @@ export class NewProfilerAction extends Task { } public async runTask(accessor: ServicesAccessor, profile: IConnectionProfile): Promise { - let profilerInput = accessor.get(IInstantiationService).createInstance(ProfilerInput, profile); + let profilerInput = accessor.get(IInstantiationService).createInstance(ProfilerInput, profile, undefined); await accessor.get(IEditorService).openEditor(profilerInput, { pinned: true }, ACTIVE_GROUP); let options: IConnectionCompletionOptions = { saveTheConnection: false, diff --git a/src/sql/workbench/contrib/profiler/browser/profilerEditor.ts b/src/sql/workbench/contrib/profiler/browser/profilerEditor.ts index e869e389aa..fce0f7642a 100644 --- a/src/sql/workbench/contrib/profiler/browser/profilerEditor.ts +++ b/src/sql/workbench/contrib/profiler/browser/profilerEditor.ts @@ -549,11 +549,13 @@ export class ProfilerEditor extends EditorPane { // Launch the create session dialog if openning a new window. let uiState = this._profilerService.getSessionViewState(this.input.id); let previousSessionName = uiState && uiState.previousSessionName; - if (!this.input.sessionName && !previousSessionName) { + if (!this.input.sessionName && !previousSessionName && !this.input.isFileSession) { this._profilerService.launchCreateSessionDialog(this.input); } - this._updateSessionSelector(previousSessionName); + if (previousSessionName) { // skip updating session selector if there is no previous session name + this._updateSessionSelector(previousSessionName); + } } else { this._startAction.enabled = false; this._stopAction.enabled = false; @@ -579,7 +581,9 @@ export class ProfilerEditor extends EditorPane { } if (this.input.state.isStopped) { this._updateToolbar(); - this._updateSessionSelector(); + if (!this.input.isFileSession) { // skip updating session selector for File sessions + this._updateSessionSelector(); + } } } } diff --git a/src/sql/workbench/services/profiler/browser/interfaces.ts b/src/sql/workbench/services/profiler/browser/interfaces.ts index 2fafadc861..e8d5f8db05 100644 --- a/src/sql/workbench/services/profiler/browser/interfaces.ts +++ b/src/sql/workbench/services/profiler/browser/interfaces.ts @@ -6,9 +6,11 @@ import { IConnectionProfile } from 'sql/platform/connection/common/interfaces'; import { ProfilerInput } from 'sql/workbench/browser/editor/profiler/profilerInput'; -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService, createDecorator } from 'vs/platform/instantiation/common/instantiation'; import * as azdata from 'azdata'; import { INewProfilerState } from 'sql/workbench/common/editor/profiler/profilerState'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; const PROFILER_SERVICE_ID = 'profilerService'; export const IProfilerService = createDecorator(PROFILER_SERVICE_ID); @@ -69,9 +71,9 @@ export interface IProfilerService { */ createSession(id: string, createStatement: string, template: azdata.ProfilerSessionTemplate): Thenable; /** - * Starts the session specified by the id + * Starts the session specified by the id or a session for opening file */ - startSession(sessionId: ProfilerSessionID, sessionName: string): Thenable; + startSession(sessionId: ProfilerSessionID, sessionName: string, sessionType?: ProfilingSessionType): Thenable; /** * Pauses the session specified by the id */ @@ -140,6 +142,18 @@ export interface IProfilerService { * @param filter filter object */ saveFilter(filter: ProfilerFilter): Promise; + /** + * Launches the dialog for picking a file to open in Profiler extension + * @param fileDialogService service to open file dialog + * @param editorService service to open profiler editor + * @param instantiationService service to create profiler instance + */ + openFile(fileDialogService: IFileDialogService, editorService: IEditorService, instantiationService: IInstantiationService): Promise; +} + +export enum ProfilingSessionType { + RemoteSession = 0, + LocalFile = 1 } export interface IProfilerSettings { diff --git a/src/sql/workbench/services/profiler/browser/profilerService.ts b/src/sql/workbench/services/profiler/browser/profilerService.ts index 4482b10a8d..2ff9566a17 100644 --- a/src/sql/workbench/services/profiler/browser/profilerService.ts +++ b/src/sql/workbench/services/profiler/browser/profilerService.ts @@ -4,12 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import { IConnectionManagementService, IConnectionCompletionOptions, ConnectionType, RunQueryOnConnectionMode } from 'sql/platform/connection/common/connectionManagement'; -import { ProfilerSessionID, IProfilerSession, IProfilerService, IProfilerViewTemplate, IProfilerSessionTemplate, PROFILER_SETTINGS, IProfilerSettings, EngineType, ProfilerFilter, PROFILER_FILTER_SETTINGS } from './interfaces'; +import { ProfilerSessionID, IProfilerSession, IProfilerService, IProfilerViewTemplate, IProfilerSessionTemplate, PROFILER_SETTINGS, IProfilerSettings, EngineType, ProfilerFilter, PROFILER_FILTER_SETTINGS, ProfilingSessionType } from './interfaces'; import { IConnectionProfile } from 'sql/platform/connection/common/interfaces'; import { ProfilerInput } from 'sql/workbench/browser/editor/profiler/profilerInput'; import { ProfilerColumnEditorDialog } from 'sql/workbench/services/profiler/browser/profilerColumnEditorDialog'; import * as azdata from 'azdata'; +import * as nls from 'vs/nls'; import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -19,6 +20,8 @@ import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storag import { Memento } from 'vs/workbench/common/memento'; import { ProfilerFilterDialog } from 'sql/workbench/services/profiler/browser/profilerFilterDialog'; import { mssqlProviderName } from 'sql/platform/connection/common/constants'; +import { ACTIVE_GROUP, IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; class TwoWayMap { private forwardMap: Map; @@ -145,12 +148,21 @@ export class ProfilerService implements IProfilerService { return false; } - public async startSession(id: ProfilerSessionID, sessionName: string): Promise { + /** + * Starts the session specified by the id or a session for opening file + * @param id session ID + * @param sessionName session name or file path to start session with + * @param sessionType distinguisher between remote session and local file + * @returns state of the run as success or failure + */ + public async startSession(id: ProfilerSessionID, sessionName: string, sessionType: ProfilingSessionType): Promise { if (this._idMap.has(id)) { this.updateMemento(id, { previousSessionName: sessionName }); try { - await this._runAction(id, provider => provider.startSession(this._idMap.get(id)!, sessionName)); - this._sessionMap.get(this._idMap.reverseGet(id)!)!.onSessionStateChanged({ isRunning: true, isStopped: false, isPaused: false }); + await this._runAction(id, provider => provider.startSession(this._idMap.get(id)!, sessionName, sessionType)); + let isRunning = sessionType === ProfilingSessionType.RemoteSession ? true : false; // Reading session stops when the file reading completes + this._sessionMap.get(this._idMap.reverseGet(id)!)!.onSessionStateChanged({ isRunning: isRunning, isStopped: false, isPaused: false }); + return true; } catch (reason) { this._notificationService.error(reason.message); @@ -290,4 +302,28 @@ export class ProfilerService implements IProfilerService { const config = [filter]; await this._configurationService.updateValue(PROFILER_FILTER_SETTINGS, config, ConfigurationTarget.USER); } + + public async openFile(fileDialogService: IFileDialogService, editorService: IEditorService, instantiationService: IInstantiationService): Promise { + const fileURIs = await fileDialogService.showOpenDialog({ + filters: [ + { + extensions: ['xel'], + name: nls.localize('FileFilterDescription', "XEL Files") + } + ], + canSelectMany: false + }); + + if (fileURIs?.length === 1) { + const fileURI = fileURIs[0]; + + let profilerInput: ProfilerInput = instantiationService.createInstance(ProfilerInput, undefined, fileURI); + await editorService.openEditor(profilerInput, { pinned: true }, ACTIVE_GROUP); + profilerInput.setConnectionState(false); // Reset connection to be not connected for File session, so that "Start" is not enabled. + const result = await this.startSession(profilerInput.id, profilerInput.fileURI.fsPath, ProfilingSessionType.LocalFile); + return result; + } + + return true; + } }