limit the data size for chart rendering (#14949)

* limit the rows feed to charts

* add telemetry and option to hide

* fix typo

* updates

* comments

* notebook fix
This commit is contained in:
Alan Ren
2021-04-02 19:36:10 -07:00
committed by GitHub
parent 13ad4c9497
commit c2d2cf5a82
11 changed files with 151 additions and 37 deletions

View File

@@ -2,33 +2,34 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./media/chartView';
import { IPanelView } from 'sql/base/browser/ui/panel/panel';
import { Insight } from './insight';
import QueryRunner from 'sql/workbench/services/query/common/queryRunner';
import { ICellValue, VisualizationOptions } from 'sql/workbench/services/query/common/query';
import { ChartOptions, IChartOption, ControlType } from './chartOptions';
import { Extensions, IInsightRegistry, IInsightData } from 'sql/platform/dashboard/browser/insightRegistry';
import { Registry } from 'vs/platform/registry/common/platform';
import * as DOM from 'vs/base/browser/dom';
import { SelectBox } from 'sql/base/browser/ui/selectBox/selectBox';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox';
import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle';
import { attachSelectBoxStyler, attachInputBoxStyler } from 'vs/platform/theme/common/styler';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { isUndefinedOrNull } from 'vs/base/common/types';
import { CreateInsightAction, CopyAction, SaveImageAction, IChartActionContext, ConfigureChartAction } from 'sql/workbench/contrib/charts/browser/actions';
import { Taskbar, ITaskbarContent } from 'sql/base/browser/ui/taskbar/taskbar';
import { Checkbox } from 'sql/base/browser/ui/checkbox/checkbox';
import { IInsightOptions, ChartType, InsightType } from 'sql/workbench/contrib/charts/common/interfaces';
import { IPanelView } from 'sql/base/browser/ui/panel/panel';
import { SelectBox } from 'sql/base/browser/ui/selectBox/selectBox';
import { ITaskbarContent, Taskbar } from 'sql/base/browser/ui/taskbar/taskbar';
import { Extensions, IInsightData, IInsightRegistry } from 'sql/platform/dashboard/browser/insightRegistry';
import { ChartState } from 'sql/workbench/common/editor/query/chartState';
import { ConfigureChartAction, CopyAction, CreateInsightAction, IChartActionContext, SaveImageAction } from 'sql/workbench/contrib/charts/browser/actions';
import { getChartMaxRowCount } from 'sql/workbench/contrib/charts/browser/utils';
import { ChartType, IInsightOptions, InsightType } from 'sql/workbench/contrib/charts/common/interfaces';
import { ICellValue, VisualizationOptions } from 'sql/workbench/services/query/common/query';
import QueryRunner from 'sql/workbench/services/query/common/queryRunner';
import * as DOM from 'vs/base/browser/dom';
import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox';
import { Emitter, Event } from 'vs/base/common/event';
import { Disposable, dispose, IDisposable } from 'vs/base/common/lifecycle';
import { isUndefinedOrNull } from 'vs/base/common/types';
import * as nls from 'vs/nls';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { Event, Emitter } from 'vs/base/common/event';
import { Registry } from 'vs/platform/registry/common/platform';
import { attachInputBoxStyler, attachSelectBoxStyler } from 'vs/platform/theme/common/styler';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { ChartOptions, ControlType, IChartOption } from './chartOptions';
import { Insight } from './insight';
const insightRegistry = Registry.as<IInsightRegistry>(Extensions.InsightContribution);
@@ -89,7 +90,8 @@ export class ChartView extends Disposable implements IPanelView {
@IContextViewService private _contextViewService: IContextViewService,
@IThemeService private _themeService: IThemeService,
@IInstantiationService private _instantiationService: IInstantiationService,
@INotificationService private readonly _notificationService: INotificationService
@INotificationService private readonly _notificationService: INotificationService,
@IConfigurationService private readonly _configurationService: IConfigurationService
) {
super();
this.taskbarContainer = DOM.$('div.taskbar-container');
@@ -195,7 +197,7 @@ export class ChartView extends Disposable implements IPanelView {
public chart(dataId: { batchId: number, resultId: number }) {
this.state.dataId = dataId;
this._currentData = dataId;
this.shouldGraph();
this.fetchData();
}
layout(dimension: DOM.Dimension): void {
@@ -209,7 +211,7 @@ export class ChartView extends Disposable implements IPanelView {
public set queryRunner(runner: QueryRunner) {
this._queryRunner = runner;
this.shouldGraph();
this.fetchData();
}
public setData(rows: ICellValue[][], columns: string[]): void {
@@ -228,7 +230,7 @@ export class ChartView extends Disposable implements IPanelView {
}
}
private shouldGraph() {
private fetchData(): void {
// Check if we have the necessary information
if (this._currentData && this._queryRunner) {
// check if we are being asked to graph something that is available
@@ -236,7 +238,7 @@ export class ChartView extends Disposable implements IPanelView {
if (batch) {
let summary = batch.resultSetSummaries[this._currentData.resultId];
if (summary) {
this._queryRunner.getQueryRows(0, summary.rowCount, this._currentData.batchId, this._currentData.resultId).then(d => {
this._queryRunner.getQueryRows(0, Math.min(getChartMaxRowCount(this._configurationService), summary.rowCount), this._currentData.batchId, this._currentData.resultId).then(d => {
let rows = d.rows;
let columns = summary.columnInfo.map(c => c.columnName);
this.setData(rows, columns);

View File

@@ -0,0 +1,25 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Extensions, IConfigurationRegistry, IConfigurationNode } from 'vs/platform/configuration/common/configurationRegistry';
import { Registry } from 'vs/platform/registry/common/platform';
import * as nls from 'vs/nls';
const configurationRegistry = Registry.as<IConfigurationRegistry>(Extensions.Configuration);
const chartsConfiguration: IConfigurationNode = {
id: 'builtinCharts',
type: 'object',
title: nls.localize('builtinChartsConfigurationTitle', "Built-in Charts"),
properties: {
'builtinCharts.maxRowCount': {
type: 'number',
default: 300,
description: nls.localize('builtinCharts.maxRowCountDescription', "The maximum number of rows for charts to display. Warning: increasing this may impact performance.")
}
}
};
configurationRegistry.registerConfiguration(chartsConfiguration);

View File

@@ -46,3 +46,7 @@ export interface IInsightCtor {
new <Services extends BrandedService[]>(container: HTMLElement, options: IInsightOptions, ...services: Services): IInsight;
readonly types: Array<InsightType | ChartType>;
}
export interface IChartsConfiguration {
readonly maxRowCount: number;
}

View File

@@ -0,0 +1,35 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IChartsConfiguration } from 'sql/workbench/contrib/charts/browser/interfaces';
import * as nls from 'vs/nls';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
/**
* Gets the max allowed row count for chart rendering.
*/
export function getChartMaxRowCount(configurationService: IConfigurationService): number {
return configurationService.getValue<IChartsConfiguration>('builtinCharts').maxRowCount;
}
/**
* Show a toast notification about the max row count for chart has exceeded.
*/
export function notifyMaxRowCountExceeded(storageService: IStorageService, notificationService: INotificationService, configurationService: IConfigurationService): void {
const storageKey = 'charts/ignoreMaxRowCountExceededNotification';
if (!storageService.getBoolean(storageKey, StorageScope.GLOBAL, false)) {
notificationService.prompt(Severity.Info,
nls.localize('charts.maxAllowedRowsExceeded', "Maximum row count for built-in charts has been exceeded, only the first {0} rows are used. To configure the value, you can open user settings and search for: 'builtinCharts.maxRowCount'.", getChartMaxRowCount(configurationService)),
[{
label: nls.localize('charts.neverShowAgain', "Don't Show Again"),
isSecondary: true,
run: () => {
storageService.store(storageKey, true, StorageScope.GLOBAL, StorageTarget.MACHINE);
}
}]);
}
}

View File

@@ -11,6 +11,7 @@ import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService';
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
suite('Chart View', () => {
test('initializes without error', () => {
@@ -40,6 +41,7 @@ function createChartView(isQueryEditorChart: boolean): ChartView {
const themeService = new TestThemeService();
const instantiationService = new TestInstantiationService();
const notificationService = new TestNotificationService();
const configurationService = new TestConfigurationService();
instantiationService.stub(IThemeService, themeService);
return new ChartView(isQueryEditorChart, contextViewService, themeService, instantiationService, notificationService);
return new ChartView(isQueryEditorChart, contextViewService, themeService, instantiationService, notificationService, configurationService);
}

View File

@@ -44,6 +44,11 @@ import { assign } from 'vs/base/common/objects';
import { QueryResultId } from 'sql/workbench/services/notebook/browser/models/cell';
import { equals } from 'vs/base/common/arrays';
import { IDisposableDataProvider } from 'sql/base/common/dataProvider';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { getChartMaxRowCount, notifyMaxRowCountExceeded } from 'sql/workbench/contrib/charts/browser/utils';
import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry';
import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys';
@Component({
selector: GridOutputComponent.SELECTOR,
template: `<div #output class="notebook-cellTable"></div>`
@@ -284,8 +289,9 @@ class DataResourceTable extends GridTableBase<any> {
public updateChartData(rowCount: number, columnCount: number, gridDataProvider: IGridDataProvider): void {
if (this.chartDisplayed) {
gridDataProvider.getRowData(0, rowCount).then(result => {
let range = new Slick.Range(0, 0, rowCount - 1, columnCount - 1);
const actualRowCount = Math.min(getChartMaxRowCount(this.configurationService), rowCount);
gridDataProvider.getRowData(0, actualRowCount).then(result => {
let range = new Slick.Range(0, 0, actualRowCount - 1, columnCount - 1);
let columns = gridDataProvider.getColumnHeaders(range);
this._chart.setData(result.rows, columns);
});
@@ -535,7 +541,11 @@ export class NotebookChartAction extends ToggleableAction {
public static SHOWTABLE_LABEL = localize('notebook.showTable', "Show table");
public static SHOWTABLE_ICON = 'table';
constructor(private resourceTable: DataResourceTable) {
constructor(private resourceTable: DataResourceTable,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IStorageService private readonly storageService: IStorageService,
@INotificationService private readonly notificationService: INotificationService,
@IAdsTelemetryService private readonly adsTelemetryService: IAdsTelemetryService) {
super(NotebookChartAction.ID, {
toggleOnLabel: NotebookChartAction.SHOWTABLE_LABEL,
toggleOnClass: NotebookChartAction.SHOWTABLE_ICON,
@@ -549,9 +559,17 @@ export class NotebookChartAction extends ToggleableAction {
this.resourceTable.toggleChartVisibility();
this.toggle(!this.state.isOn);
if (this.state.isOn) {
let rowCount = context.table.getData().getLength();
let columnCount = context.table.columns.length;
this.resourceTable.updateChartData(rowCount, columnCount, context.gridDataProvider);
const rowCount = context.table.getData().getLength();
const columnCount = context.table.columns.length;
const maxRowCount = getChartMaxRowCount(this.configurationService);
const maxRowCountExceeded = rowCount > maxRowCount;
if (maxRowCountExceeded) {
notifyMaxRowCountExceeded(this.storageService, this.notificationService, this.configurationService);
}
this.adsTelemetryService.createActionEvent(TelemetryKeys.TelemetryView.Notebook, TelemetryKeys.TelemetryAction.ShowChart).withAdditionalProperties(
{ [TelemetryKeys.TelemetryPropertyName.ChartMaxRowCountExceeded]: maxRowCountExceeded }
).send();
this.resourceTable.updateChartData(Math.min(rowCount, maxRowCount), columnCount, context.gridDataProvider);
}
return true;
}

View File

@@ -36,6 +36,11 @@ let sqlTocItems: ITOCEntry<string>[] = [{
id: 'data/profiler',
label: localize('profiler', "Profiler"),
settings: ['profiler.*']
},
{
id: 'data/builtinCharts',
label: localize('builtinCharts', "Built-in Charts"),
settings: ['builtinCharts.*']
}
]
}];

View File

@@ -20,6 +20,9 @@ import { getErrorMessage } from 'vs/base/common/errors';
import { SaveFormat } from 'sql/workbench/services/query/common/resultSerializer';
import { IExtensionRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations';
import { IEncodingSupport } from 'vs/workbench/common/editor';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { getChartMaxRowCount, notifyMaxRowCountExceeded } from 'sql/workbench/contrib/charts/browser/utils';
export interface IGridActionContext {
gridDataProvider: IGridDataProvider;
@@ -167,7 +170,11 @@ export class ChartDataAction extends Action {
constructor(
@IEditorService private editorService: IEditorService,
@IExtensionRecommendationsService private readonly extensionTipsService: IExtensionRecommendationsService
@IExtensionRecommendationsService private readonly extensionTipsService: IExtensionRecommendationsService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IStorageService private readonly storageService: IStorageService,
@INotificationService private readonly notificationService: INotificationService,
@IAdsTelemetryService private readonly adsTelemetryService: IAdsTelemetryService
) {
super(ChartDataAction.ID, ChartDataAction.LABEL, ChartDataAction.ICON);
}
@@ -175,7 +182,15 @@ export class ChartDataAction extends Action {
public run(context: IGridActionContext): Promise<boolean> {
// show the visualizer extension recommendation notification
this.extensionTipsService.promptRecommendedExtensionsByScenario(Constants.visualizerExtensions);
const maxRowCount = getChartMaxRowCount(this.configurationService);
const rowCount = context.table.getData().getLength();
const maxRowCountExceeded = rowCount > maxRowCount;
if (maxRowCountExceeded) {
notifyMaxRowCountExceeded(this.storageService, this.notificationService, this.configurationService);
}
this.adsTelemetryService.createActionEvent(TelemetryKeys.TelemetryView.ResultsPanel, TelemetryKeys.TelemetryAction.ShowChart).withAdditionalProperties(
{ [TelemetryKeys.TelemetryPropertyName.ChartMaxRowCountExceeded]: maxRowCountExceeded }
).send();
const activeEditor = this.editorService.activeEditorPane as QueryEditor;
activeEditor.chart({ batchId: context.batchId, resultId: context.resultId });
return Promise.resolve(true);

View File

@@ -383,7 +383,7 @@ export abstract class GridTableBase<T> extends Disposable implements IView {
@IInstantiationService protected readonly instantiationService: IInstantiationService,
@IEditorService private readonly editorService: IEditorService,
@IUntitledTextEditorService private readonly untitledEditorService: IUntitledTextEditorService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IConfigurationService protected readonly configurationService: IConfigurationService,
@IQueryModelService private readonly queryModelService: IQueryModelService,
@IThemeService private readonly themeService: IThemeService
) {