From 726eb8d0e1699ab49e108104897ebfbc340c8d57 Mon Sep 17 00:00:00 2001 From: Anthony Dresser Date: Mon, 29 Oct 2018 15:24:08 -0700 Subject: [PATCH] Time elapsed status item (#3006) * added time elapsed status item * add missing files --- .../connection/common/connectionStatus.ts | 2 - src/sql/parts/connection/common/utils.ts | 4 +- src/sql/parts/query/common/rowCountStatus.ts | 11 +- .../parts/query/common/timeElapsedStatus.ts | 107 ++++++++++++++++++ .../query/execution/queryModelService.ts | 34 ++++-- src/sql/parts/query/execution/queryRunner.ts | 22 +++- 6 files changed, 156 insertions(+), 24 deletions(-) create mode 100644 src/sql/parts/query/common/timeElapsedStatus.ts diff --git a/src/sql/parts/connection/common/connectionStatus.ts b/src/sql/parts/connection/common/connectionStatus.ts index a741b1934e..12c5b2e066 100644 --- a/src/sql/parts/connection/common/connectionStatus.ts +++ b/src/sql/parts/connection/common/connectionStatus.ts @@ -23,9 +23,7 @@ export class ConnectionStatusbarItem implements IStatusbarItem { constructor( @IConnectionManagementService private _connectionManagementService: IConnectionManagementService, - @IEditorGroupsService private _editorGroupService: IEditorGroupsService, @IEditorService private _editorService: EditorServiceImpl, - @ICapabilitiesService private _capabilitiesService: ICapabilitiesService, @IObjectExplorerService private _objectExplorerService: IObjectExplorerService, ) { } diff --git a/src/sql/parts/connection/common/utils.ts b/src/sql/parts/connection/common/utils.ts index 4a07c63301..02117d347d 100644 --- a/src/sql/parts/connection/common/utils.ts +++ b/src/sql/parts/connection/common/utils.ts @@ -69,7 +69,7 @@ export function parseTimeString(value: string): number | boolean { * @param value The number of milliseconds to convert to a timespan string * @returns A properly formatted timespan string. */ -export function parseNumAsTimeString(value: number): string { +export function parseNumAsTimeString(value: number, includeFraction: boolean = true): string { let tempVal = value; let h = Math.floor(tempVal / msInH); tempVal %= msInH; @@ -85,7 +85,7 @@ export function parseNumAsTimeString(value: number): string { let rs = hs + ':' + ms + ':' + ss; - return tempVal > 0 ? rs + '.' + mss : rs; + return tempVal > 0 && includeFraction ? rs + '.' + mss : rs; } export function generateUri(connection: IConnectionProfile, purpose?: 'dashboard' | 'insights' | 'connection'): string { diff --git a/src/sql/parts/query/common/rowCountStatus.ts b/src/sql/parts/query/common/rowCountStatus.ts index 594d761293..ec45d137ef 100644 --- a/src/sql/parts/query/common/rowCountStatus.ts +++ b/src/sql/parts/query/common/rowCountStatus.ts @@ -9,7 +9,6 @@ import QueryRunner from 'sql/parts/query/execution/queryRunner'; import { IStatusbarItem } from 'vs/workbench/browser/parts/statusbar/statusbar'; import { IDisposable, combinedDisposable, dispose } from 'vs/base/common/lifecycle'; -import { IEditorGroupsService } from 'vs/workbench/services/group/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IEditorCloseEvent } from 'vs/workbench/common/editor'; import { append, $, hide, show } from 'vs/base/browser/dom'; @@ -25,7 +24,6 @@ export class RowCountStatusBarItem implements IStatusbarItem { constructor( @IEditorService private _editorService: EditorServiceImpl, - @IEditorGroupsService private _editorGroupService: IEditorGroupsService, @IQueryModelService private _queryModelService: IQueryModelService ) { } @@ -36,7 +34,7 @@ export class RowCountStatusBarItem implements IStatusbarItem { ]; this._element = append(container, $('.query-statusbar-group')); - this._flavorElement = append(this._element, $('a.editor-status-selection')); + this._flavorElement = append(this._element, $('.editor-status-selection')); this._flavorElement.title = nls.localize('rowStatus', "Row Count"); hide(this._flavorElement); @@ -65,11 +63,10 @@ export class RowCountStatusBarItem implements IStatusbarItem { if (queryRunner) { if (queryRunner.hasCompleted) { this._displayValue(queryRunner); - } else if (queryRunner.isExecuting) { - this.dispose = queryRunner.addListener('complete', () => { - this._displayValue(queryRunner); - }); } + this.dispose = queryRunner.onQueryEnd(e => { + this._displayValue(queryRunner); + }); } else { this.dispose = this._queryModelService.onRunQueryComplete(e => { if (e === currentUri) { diff --git a/src/sql/parts/query/common/timeElapsedStatus.ts b/src/sql/parts/query/common/timeElapsedStatus.ts new file mode 100644 index 0000000000..199e9ef517 --- /dev/null +++ b/src/sql/parts/query/common/timeElapsedStatus.ts @@ -0,0 +1,107 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as WorkbenchUtils from 'sql/workbench/common/sqlWorkbenchUtils'; +import { IQueryModelService } from '../execution/queryModel'; +import QueryRunner from 'sql/parts/query/execution/queryRunner'; +import { parseNumAsTimeString } from 'sql/parts/connection/common/utils'; + +import { IStatusbarItem } from 'vs/workbench/browser/parts/statusbar/statusbar'; +import { IDisposable, combinedDisposable, dispose } from 'vs/base/common/lifecycle'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IEditorCloseEvent } from 'vs/workbench/common/editor'; +import { append, $, hide, show } from 'vs/base/browser/dom'; +import * as nls from 'vs/nls'; +import { EditorServiceImpl } from 'vs/workbench/browser/parts/editor/editor'; +import { IntervalTimer } from 'vs/base/common/async'; + +export class TimeElapsedStatusBarItem implements IStatusbarItem { + + private _element: HTMLElement; + private _flavorElement: HTMLElement; + + private dispose: IDisposable[] = []; + private intervalTimer = new IntervalTimer(); + + constructor( + @IEditorService private _editorService: EditorServiceImpl, + @IQueryModelService private _queryModelService: IQueryModelService + ) { } + + render(container: HTMLElement): IDisposable { + let disposables = [ + this._editorService.onDidVisibleEditorsChange(() => this._onEditorsChanged()), + this._editorService.onDidCloseEditor(event => this._onEditorClosed(event)) + ]; + + this._element = append(container, $('.query-statusbar-group')); + this._flavorElement = append(this._element, $('.editor-status-selection')); + this._flavorElement.title = nls.localize('timeElapsed', "Time Elapsed"); + hide(this._flavorElement); + + this._showStatus(); + + return combinedDisposable(disposables); + } + + private _onEditorsChanged() { + this._showStatus(); + } + + private _onEditorClosed(event: IEditorCloseEvent) { + hide(this._flavorElement); + } + + // Show/hide query status for active editor + private _showStatus(): void { + this.intervalTimer.cancel(); + hide(this._flavorElement); + dispose(this.dispose); + this._flavorElement.innerText = ''; + this.dispose = []; + let activeEditor = this._editorService.activeControl; + if (activeEditor) { + let currentUri = WorkbenchUtils.getEditorUri(activeEditor.input); + if (currentUri) { + let queryRunner = this._queryModelService.getQueryRunner(currentUri); + if (queryRunner) { + if (queryRunner.hasCompleted || queryRunner.isExecuting) { + this._displayValue(queryRunner); + } + this.dispose.push(queryRunner.onQueryStart(e => { + this._displayValue(queryRunner); + })); + this.dispose.push(queryRunner.onQueryEnd(e => { + this._displayValue(queryRunner); + })); + } else { + this.dispose.push(this._queryModelService.onRunQueryStart(e => { + if (e === currentUri) { + this._displayValue(this._queryModelService.getQueryRunner(currentUri)); + } + })); + this.dispose.push(this._queryModelService.onRunQueryComplete(e => { + if (e === currentUri) { + this._displayValue(this._queryModelService.getQueryRunner(currentUri)); + } + })); + } + } + } + } + + private _displayValue(runner: QueryRunner) { + this.intervalTimer.cancel(); + if (runner.isExecuting) { + this.intervalTimer.cancelAndSet(() => { + this._flavorElement.innerText = parseNumAsTimeString(Date.now() - runner.queryStartTime.getTime(), false); + }, 1000); + this._flavorElement.innerText = parseNumAsTimeString(Date.now() - runner.queryStartTime.getTime(), false); + } else { + this._flavorElement.innerText = parseNumAsTimeString(runner.queryEndTime.getTime() - runner.queryStartTime.getTime(), false); + } + show(this._flavorElement); + } +} diff --git a/src/sql/parts/query/execution/queryModelService.ts b/src/sql/parts/query/execution/queryModelService.ts index 7e5ee6d546..af062d48f8 100644 --- a/src/sql/parts/query/execution/queryModelService.ts +++ b/src/sql/parts/query/execution/queryModelService.ts @@ -14,6 +14,7 @@ import { QueryInput } from 'sql/parts/query/common/queryInput'; import { QueryStatusbarItem } from 'sql/parts/query/execution/queryStatus'; import { SqlFlavorStatusbarItem } from 'sql/parts/query/common/flavorStatus'; import { RowCountStatusBarItem } from 'sql/parts/query/common/rowCountStatus'; +import { TimeElapsedStatusBarItem } from 'sql/parts/query/common/timeElapsedStatus'; import * as sqlops from 'sqlops'; @@ -86,6 +87,12 @@ export class QueryModelService implements IQueryModelService { // Register Statusbar items + (platform.Registry.as(statusbar.Extensions.Statusbar)).registerStatusbarItem(new statusbar.StatusbarItemDescriptor( + TimeElapsedStatusBarItem, + statusbar.StatusbarAlignment.RIGHT, + 100 /* Should appear to the right of the SQL editor status */ + )); + (platform.Registry.as(statusbar.Extensions.Statusbar)).registerStatusbarItem(new statusbar.StatusbarItemDescriptor( RowCountStatusBarItem, statusbar.StatusbarAlignment.RIGHT, @@ -352,7 +359,7 @@ export class QueryModelService implements IQueryModelService { public disposeQuery(ownerUri: string): void { // Get existing query runner - let queryRunner = this.getQueryRunner(ownerUri); + let queryRunner = this.internalGetQueryRunner(ownerUri); if (queryRunner) { queryRunner.disposeQuery(); } @@ -444,7 +451,7 @@ export class QueryModelService implements IQueryModelService { public disposeEdit(ownerUri: string): Thenable { // Get existing query runner - let queryRunner = this.getQueryRunner(ownerUri); + let queryRunner = this.internalGetQueryRunner(ownerUri); if (queryRunner) { return queryRunner.disposeEdit(ownerUri); } @@ -453,7 +460,7 @@ export class QueryModelService implements IQueryModelService { public updateCell(ownerUri: string, rowId: number, columnId: number, newValue: string): Thenable { // Get existing query runner - let queryRunner = this.getQueryRunner(ownerUri); + let queryRunner = this.internalGetQueryRunner(ownerUri); if (queryRunner) { return queryRunner.updateCell(ownerUri, rowId, columnId, newValue).then((result) => result, error => { this._notificationService.notify({ @@ -468,7 +475,7 @@ export class QueryModelService implements IQueryModelService { public commitEdit(ownerUri): Thenable { // Get existing query runner - let queryRunner = this.getQueryRunner(ownerUri); + let queryRunner = this.internalGetQueryRunner(ownerUri); if (queryRunner) { return queryRunner.commitEdit(ownerUri).then(() => { }, error => { this._notificationService.notify({ @@ -483,7 +490,7 @@ export class QueryModelService implements IQueryModelService { public createRow(ownerUri: string): Thenable { // Get existing query runner - let queryRunner = this.getQueryRunner(ownerUri); + let queryRunner = this.internalGetQueryRunner(ownerUri); if (queryRunner) { return queryRunner.createRow(ownerUri); } @@ -492,7 +499,7 @@ export class QueryModelService implements IQueryModelService { public deleteRow(ownerUri: string, rowId: number): Thenable { // Get existing query runner - let queryRunner = this.getQueryRunner(ownerUri); + let queryRunner = this.internalGetQueryRunner(ownerUri); if (queryRunner) { return queryRunner.deleteRow(ownerUri, rowId); } @@ -501,7 +508,7 @@ export class QueryModelService implements IQueryModelService { public revertCell(ownerUri: string, rowId: number, columnId: number): Thenable { // Get existing query runner - let queryRunner = this.getQueryRunner(ownerUri); + let queryRunner = this.internalGetQueryRunner(ownerUri); if (queryRunner) { return queryRunner.revertCell(ownerUri, rowId, columnId); } @@ -510,16 +517,25 @@ export class QueryModelService implements IQueryModelService { public revertRow(ownerUri: string, rowId: number): Thenable { // Get existing query runner - let queryRunner = this.getQueryRunner(ownerUri); + let queryRunner = this.internalGetQueryRunner(ownerUri); if (queryRunner) { return queryRunner.revertRow(ownerUri, rowId); } return TPromise.as(null); } + public getQueryRunner(ownerUri): QueryRunner { + let queryRunner: QueryRunner = undefined; + if (this._queryInfoMap.has(ownerUri)) { + queryRunner = this._getQueryInfo(ownerUri).queryRunner; + } + // return undefined if not found or is already executing + return queryRunner; + } + // PRIVATE METHODS ////////////////////////////////////////////////////// - public getQueryRunner(ownerUri): QueryRunner { + private internalGetQueryRunner(ownerUri): QueryRunner { let queryRunner: QueryRunner = undefined; if (this._queryInfoMap.has(ownerUri)) { let existingRunner = this._getQueryInfo(ownerUri).queryRunner; diff --git a/src/sql/parts/query/execution/queryRunner.ts b/src/sql/parts/query/execution/queryRunner.ts index 03cdfd3ea6..96be349085 100644 --- a/src/sql/parts/query/execution/queryRunner.ts +++ b/src/sql/parts/query/execution/queryRunner.ts @@ -112,6 +112,15 @@ export default class QueryRunner { private _onBatchEnd = new Emitter(); public readonly onBatchEnd: Event = this._onBatchEnd.event; + private _queryStartTime: Date; + public get queryStartTime(): Date { + return this._queryStartTime; + } + private _queryEndTime: Date; + public get queryEndTime(): Date { + return this._queryEndTime; + } + // CONSTRUCTOR ///////////////////////////////////////////////////////// constructor( public uri: string, @@ -185,9 +194,10 @@ export default class QueryRunner { this._debouncedMessage.clear(); this._debouncedResultSet.clear(); this._planXml = new Deferred(); - let ownerUri = this.uri; this._batchSets = []; this._hasCompleted = false; + this._queryStartTime = undefined; + this._queryEndTime = undefined; if (types.isObject(input) || types.isUndefinedOrNull(input)) { // Update internal state to show that we're executing the query this._resultLineOffset = input ? input.startLine : 0; @@ -203,20 +213,22 @@ export default class QueryRunner { // Send the request to execute the query return runCurrentStatement - ? this._queryManagementService.runQueryStatement(ownerUri, input.startLine, input.startColumn).then(() => this.handleSuccessRunQueryResult(), e => this.handleFailureRunQueryResult(e)) - : this._queryManagementService.runQuery(ownerUri, input, runOptions).then(() => this.handleSuccessRunQueryResult(), e => this.handleFailureRunQueryResult(e)); + ? this._queryManagementService.runQueryStatement(this.uri, input.startLine, input.startColumn).then(() => this.handleSuccessRunQueryResult(), e => this.handleFailureRunQueryResult(e)) + : this._queryManagementService.runQuery(this.uri, input, runOptions).then(() => this.handleSuccessRunQueryResult(), e => this.handleFailureRunQueryResult(e)); } else if (types.isString(input)) { // Update internal state to show that we're executing the query this._isExecuting = true; this._totalElapsedMilliseconds = 0; - return this._queryManagementService.runQueryString(ownerUri, input).then(() => this.handleSuccessRunQueryResult(), e => this.handleFailureRunQueryResult(e)); + return this._queryManagementService.runQueryString(this.uri, input).then(() => this.handleSuccessRunQueryResult(), e => this.handleFailureRunQueryResult(e)); } else { return Promise.reject('Unknown input'); } } private handleSuccessRunQueryResult() { + // this isn't exact, but its the best we can do + this._queryStartTime = new Date(); // The query has started, so lets fire up the result pane this._onQueryStart.fire(); this._eventEmitter.emit(EventType.START); @@ -241,6 +253,8 @@ export default class QueryRunner { * Handle a QueryComplete from the service layer */ public handleQueryComplete(result: sqlops.QueryExecuteCompleteNotificationResult): void { + // this also isn't exact but its the best we can do + this._queryEndTime = new Date(); // Store the batch sets we got back as a source of "truth" this._isExecuting = false;