diff --git a/src/sql/base/browser/ui/table/tableDataView.ts b/src/sql/base/browser/ui/table/tableDataView.ts index d962742158..fd6d79008e 100644 --- a/src/sql/base/browser/ui/table/tableDataView.ts +++ b/src/sql/base/browser/ui/table/tableDataView.ts @@ -8,6 +8,7 @@ import { Observer } from 'rxjs/Observer'; import { Event, Emitter } from 'vs/base/common/event'; import { TPromise } from 'vs/base/common/winjs.base'; import * as types from 'vs/base/common/types'; +import { compare as stringCompare } from 'vs/base/common/strings'; import { IDisposableDataProvider } from 'sql/base/browser/ui/table/interfaces'; @@ -19,7 +20,23 @@ export interface IFindPosition { function defaultSort(args: Slick.OnSortEventArgs, data: Array): Array { let field = args.sortCol.field; let sign = args.sortAsc ? 1 : -1; - return data.sort((a, b) => (a[field] === b[field] ? 0 : (a[field] > b[field] ? 1 : -1)) * sign); + let comparer: (a, b) => number; + if (types.isString(data[0][field])) { + if (Number(data[0][field]) !== NaN) { + comparer = (a: number, b: number) => { + let anum = Number(a[field]); + let bnum = Number(b[field]); + return anum === bnum ? 0 : anum > bnum ? 1 : -1; + }; + } else { + comparer = stringCompare; + } + } else { + comparer = (a: number, b: number) => { + return a[field] === b[field] ? 0 : (a[field] > b[field] ? 1 : -1); + }; + } + return data.sort((a, b) => comparer(a, b) * sign); } export class TableDataView implements IDisposableDataProvider { diff --git a/src/sql/parts/query/common/queryResultsInput.ts b/src/sql/parts/query/common/queryResultsInput.ts index 096a3d12b3..f22689bede 100644 --- a/src/sql/parts/query/common/queryResultsInput.ts +++ b/src/sql/parts/query/common/queryResultsInput.ts @@ -9,18 +9,20 @@ import { localize } from 'vs/nls'; import { TPromise } from 'vs/base/common/winjs.base'; import { EditorInput } from 'vs/workbench/common/editor'; import { Emitter } from 'vs/base/common/event'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { GridPanelState } from 'sql/parts/query/editor/gridPanel'; import { MessagePanelState } from 'sql/parts/query/editor/messagePanel'; import { QueryPlanState } from 'sql/parts/queryPlan/queryPlan'; import { ChartState } from 'sql/parts/query/editor/charting/chartView'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { TopOperationsState } from 'sql/parts/queryPlan/topOperations'; export class ResultsViewState { public gridPanelState: GridPanelState = new GridPanelState(); public messagePanelState: MessagePanelState = new MessagePanelState(this.configurationService); public chartState: ChartState = new ChartState(); public queryPlanState: QueryPlanState = new QueryPlanState(); + public topOperationsState = new TopOperationsState(); public gridPanelSize: number; public messagePanelSize: number; public activeTab: string; diff --git a/src/sql/parts/query/editor/queryResultsView.ts b/src/sql/parts/query/editor/queryResultsView.ts index e24b260978..b3cbc958d4 100644 --- a/src/sql/parts/query/editor/queryResultsView.ts +++ b/src/sql/parts/query/editor/queryResultsView.ts @@ -12,6 +12,7 @@ import { MessagePanel } from './messagePanel'; import { GridPanel } from './gridPanel'; import { ChartTab } from './charting/chartTab'; import { QueryPlanTab } from 'sql/parts/queryPlan/queryPlan'; +import { TopOperationsTab } from 'sql/parts/queryPlan/topOperations'; import * as nls from 'vs/nls'; import { PanelViewlet } from 'vs/workbench/browser/parts/views/panelViewlet'; @@ -172,6 +173,7 @@ export class QueryResultsView extends Disposable { private resultsTab: ResultsTab; private chartTab: ChartTab; private qpTab: QueryPlanTab; + private topOperationsTab: TopOperationsTab; private runnerDisposables: IDisposable[]; @@ -185,6 +187,8 @@ export class QueryResultsView extends Disposable { this.chartTab = this._register(new ChartTab(instantiationService)); this._panelView = this._register(new TabbedPanel(container, { showHeaderWhenSingleView: false })); this.qpTab = this._register(new QueryPlanTab()); + this.topOperationsTab = this._register(new TopOperationsTab(instantiationService)); + this._panelView.pushTab(this.resultsTab); this._register(this._panelView.onTabChange(e => { if (this.input) { @@ -202,6 +206,7 @@ export class QueryResultsView extends Disposable { this.runnerDisposables = []; this.resultsTab.view.state = this.input.state; this.qpTab.view.state = this.input.state.queryPlanState; + this.topOperationsTab.view.state = this.input.state.topOperationsState; this.chartTab.view.state = this.input.state.chartState; let queryRunner = this.queryModelService._getQueryInfo(input.uri).queryRunner; this.resultsTab.queryRunner = queryRunner; @@ -222,6 +227,11 @@ export class QueryResultsView extends Disposable { this._panelView.pushTab(this.qpTab); } } + if (this.input.state.visibleTabs.has(this.topOperationsTab.identifier)) { + if (!this._panelView.contains(this.topOperationsTab)) { + this._panelView.pushTab(this.topOperationsTab); + } + } this.runnerDisposables.push(queryRunner.onQueryEnd(() => { if (queryRunner.isQueryPlan) { queryRunner.planXml.then(e => { @@ -238,6 +248,7 @@ export class QueryResultsView extends Disposable { this._input = undefined; this.resultsTab.clear(); this.qpTab.clear(); + this.topOperationsTab.clear(); this.chartTab.clear(); } @@ -270,9 +281,14 @@ export class QueryResultsView extends Disposable { if (!this._panelView.contains(this.qpTab)) { this._panelView.pushTab(this.qpTab); } + this.input.state.visibleTabs.add(this.topOperationsTab.identifier); + if (!this._panelView.contains(this.topOperationsTab)) { + this._panelView.pushTab(this.topOperationsTab); + } this._panelView.showTab(this.qpTab.identifier); this.qpTab.view.showPlan(xml); + this.topOperationsTab.view.showPlan(xml); } public hidePlan() { diff --git a/src/sql/parts/queryPlan/planXmlParser.ts b/src/sql/parts/queryPlan/planXmlParser.ts index ae5c76486b..9f308d6a25 100644 --- a/src/sql/parts/queryPlan/planXmlParser.ts +++ b/src/sql/parts/queryPlan/planXmlParser.ts @@ -292,4 +292,4 @@ export class PlanXmlParser { } } } -} \ No newline at end of file +} diff --git a/src/sql/parts/queryPlan/topOperations.ts b/src/sql/parts/queryPlan/topOperations.ts new file mode 100644 index 0000000000..b73271f98a --- /dev/null +++ b/src/sql/parts/queryPlan/topOperations.ts @@ -0,0 +1,136 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import { Dimension } from 'vs/base/browser/dom'; +import { localize } from 'vs/nls'; +import { dispose, IDisposable } from 'vs/base/common/lifecycle'; + +import { Table } from 'sql/base/browser/ui/table/table'; +import { PlanXmlParser } from 'sql/parts/queryPlan/planXmlParser'; +import { IPanelView, IPanelTab } from 'sql/base/browser/ui/panel/panel'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { attachTableStyler } from 'sql/common/theme/styler'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { TableDataView } from 'sql/base/browser/ui/table/tableDataView'; + +const topOperationColumns: Array> = [ + { name: localize('topOperations.operation', 'Operation'), field: 'operation', sortable: true }, + { name: localize('topOperations.object', 'Object'), field: 'object', sortable: true }, + { name: localize('topOperations.estCost', 'Est Cost'), field: 'estCost', sortable: true }, + { name: localize('topOperations.estSubtreeCost', 'Est Subtree Cost'), field: 'estSubtreeCost', sortable: true }, + { name: localize('topOperations.actualRows', 'Actual Rows'), field: 'actualRows', sortable: true }, + { name: localize('topOperations.estRows', 'Est Rows'), field: 'estRows', sortable: true }, + { name: localize('topOperations.actualExecutions', 'Actual Executions'), field: 'actualExecutions', sortable: true }, + { name: localize('topOperations.estCPUCost', 'Est CPU Cost'), field: 'estCPUCost', sortable: true }, + { name: localize('topOperations.estIOCost', 'Est IO Cost'), field: 'estIOCost', sortable: true }, + { name: localize('topOperations.parallel', 'Parallel'), field: 'parallel', sortable: true }, + { name: localize('topOperations.actualRebinds', 'Actual Rebinds'), field: 'actualRebinds', sortable: true }, + { name: localize('topOperations.estRebinds', 'Est Rebinds'), field: 'estRebinds', sortable: true }, + { name: localize('topOperations.actualRewinds', 'Actual Rewinds'), field: 'actualRewinds', sortable: true }, + { name: localize('topOperations.estRewinds', 'Est Rewinds'), field: 'estRewinds', sortable: true }, + { name: localize('topOperations.partitioned', 'Partitioned'), field: 'partitioned', sortable: true } +]; + +export class TopOperationsState { + xml: string; + dispose() { + + } +} + +export class TopOperationsTab implements IPanelTab { + public readonly title = localize('topOperationsTitle', 'Top Operation'); + public readonly identifier = 'TopOperationsTab'; + public readonly view: TopOperationsView; + + constructor(@IInstantiationService instantiationService: IInstantiationService) { + this.view = instantiationService.createInstance(TopOperationsView); + } + + public dispose() { + dispose(this.view); + } + + public clear() { + this.view.clear(); + } +} + +export class TopOperationsView implements IPanelView { + private _state: TopOperationsState; + private table: Table; + private disposables: IDisposable[] = []; + private container = document.createElement('div'); + private dataView = new TableDataView(); + + constructor(@IThemeService private themeService: IThemeService) { + this.table = new Table(this.container, { + columns: topOperationColumns, + dataProvider: this.dataView, + sorter: { + sort: (args) => { + this.dataView.sort(args); + } + } + }); + this.disposables.push(this.table); + this.disposables.push(attachTableStyler(this.table, this.themeService)); + } + + public render(container: HTMLElement): void { + container.appendChild(this.container); + } + + dispose() { + dispose(this.disposables); + } + + public layout(dimension: Dimension): void { + this.table.layout(dimension); + } + + public clear() { + this.dataView.clear(); + } + + public showPlan(xml: string) { + this.state.xml = xml; + this.dataView.clear(); + let parser = new PlanXmlParser(xml); + let operations = parser.topOperations; + let data = operations.map(i => { + return { + operation: i.title, + object: i.indexObject.title, + estCost: i.estimatedOperatorCost, + estSubtreeCost: i.subtreeCost, + actualRows: i.runtimeInfo.actualRows, + estRows: i.estimateRows, + actualExecutions: i.runtimeInfo.actualExecutions, + estCPUCost: i.estimateCpu, + estIOCost: i.estimateIo, + parallel: i.parallel, + actualRebinds: '', + estRebinds: i.estimateRebinds, + actualRewinds: '', + estRewinds: i.estimateRewinds, + partitioned: i.partitioned + }; + }); + this.dataView.push(data); + } + + public set state(val: TopOperationsState) { + this._state = val; + if (this.state.xml) { + this.showPlan(this.state.xml); + } + } + + public get state(): TopOperationsState { + return this._state; + } +}