perf(dashboard): Virtualization POC (#21438)

This commit is contained in:
Kamil Gabryjelski 2022-10-11 15:06:00 +02:00 committed by GitHub
parent 070b865e32
commit 406e44bba1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 126 additions and 7 deletions

View File

@ -31,6 +31,7 @@ export enum FeatureFlag {
DASHBOARD_FILTERS_EXPERIMENTAL = 'DASHBOARD_FILTERS_EXPERIMENTAL',
DASHBOARD_NATIVE_FILTERS = 'DASHBOARD_NATIVE_FILTERS',
DASHBOARD_NATIVE_FILTERS_SET = 'DASHBOARD_NATIVE_FILTERS_SET',
DASHBOARD_VIRTUALIZATION = 'DASHBOARD_VIRTUALIZATION',
DASHBOARD_RBAC = 'DASHBOARD_RBAC',
DATAPANEL_CLOSED_BY_DEFAULT = 'DATAPANEL_CLOSED_BY_DEFAULT',
DISABLE_DATASET_SOURCE_EDIT = 'DISABLE_DATASET_SOURCE_EDIT',

View File

@ -28,6 +28,7 @@ import ErrorBoundary from 'src/components/ErrorBoundary';
import { Logger, LOG_ACTIONS_RENDER_CHART } from 'src/logger/LogUtils';
import { URL_PARAMS } from 'src/constants';
import { getUrlParam } from 'src/utils/urlUtils';
import { isCurrentUserBot } from 'src/utils/isBot';
import { ResourceStatus } from 'src/hooks/apiResources/apiResources';
import ChartRenderer from './ChartRenderer';
import { ChartErrorMessage } from './ChartErrorMessage';
@ -74,6 +75,7 @@ const propTypes = {
ownState: PropTypes.object,
postTransformProps: PropTypes.func,
datasetsStatus: PropTypes.oneOf(['loading', 'error', 'complete']),
isInView: PropTypes.bool,
};
const BLANK = {};
@ -92,6 +94,7 @@ const defaultProps = {
chartStackTrace: null,
isDeactivatedViz: false,
force: false,
isInView: true,
};
const Styles = styled.div`
@ -307,11 +310,17 @@ class Chart extends React.PureComponent {
width={width}
>
<div className="slice_container" data-test="slice-container">
<ChartRenderer
{...this.props}
source={this.props.dashboardId ? 'dashboard' : 'explore'}
data-test={this.props.vizType}
/>
{this.props.isInView ||
!isFeatureEnabled(FeatureFlag.DASHBOARD_VIRTUALIZATION) ||
isCurrentUserBot() ? (
<ChartRenderer
{...this.props}
source={this.props.dashboardId ? 'dashboard' : 'explore'}
data-test={this.props.vizType}
/>
) : (
<Loading />
)}
</div>
{isLoading && !isDeactivatedViz && <Loading />}
</Styles>

View File

@ -92,6 +92,7 @@ const propTypes = {
filterState: PropTypes.object,
postTransformProps: PropTypes.func,
datasetsStatus: PropTypes.oneOf(['loading', 'error', 'complete']),
isInView: PropTypes.bool,
};
const defaultProps = {
@ -382,6 +383,7 @@ class Chart extends React.Component {
filterboxMigrationState,
postTransformProps,
datasetsStatus,
isInView,
} = this.props;
const { width } = this.state;
@ -511,6 +513,7 @@ class Chart extends React.Component {
filterboxMigrationState={filterboxMigrationState}
postTransformProps={postTransformProps}
datasetsStatus={datasetsStatus}
isInView={isInView}
/>
</div>
</SliceContainer>

View File

@ -137,7 +137,7 @@ describe('ChartHolder', () => {
rerender(
<Provider store={store}>
<ChartHolder {...defaultProps} editMode />
<ChartHolder {...defaultProps} editMode isInView />
</Provider>,
);
@ -414,6 +414,7 @@ describe('ChartHolder', () => {
deleteComponent={deleteComponent}
fullSizeChartId={null}
editMode
isInView
/>
</Provider>,
);

View File

@ -67,6 +67,8 @@ interface ChartHolderProps {
handleComponentDrop: (...args: unknown[]) => unknown;
setFullSizeChartId: (chartId: number | null) => void;
postAddSliceFromDashboard?: () => void;
isInView: boolean;
}
const ChartHolder: React.FC<ChartHolderProps> = ({
@ -91,6 +93,7 @@ const ChartHolder: React.FC<ChartHolderProps> = ({
handleComponentDrop,
setFullSizeChartId,
postAddSliceFromDashboard,
isInView,
}) => {
const { chartId } = component.meta;
const isFullSize = fullSizeChartId === chartId;
@ -314,6 +317,7 @@ const ChartHolder: React.FC<ChartHolderProps> = ({
setControlValue={handleExtraControl}
extraControls={extraControls}
postTransformProps={handlePostTransformProps}
isInView={isInView}
/>
{editMode && (
<HoverMenu position="top">

View File

@ -19,6 +19,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
import { FeatureFlag, isFeatureEnabled } from '@superset-ui/core';
import DragDroppable from 'src/dashboard/components/dnd/DragDroppable';
import DragHandle from 'src/dashboard/components/dnd/DragHandle';
@ -32,6 +33,7 @@ import WithPopoverMenu from 'src/dashboard/components/menu/WithPopoverMenu';
import { componentShape } from 'src/dashboard/util/propShapes';
import backgroundStyleOptions from 'src/dashboard/util/backgroundStyleOptions';
import { BACKGROUND_TRANSPARENT } from 'src/dashboard/util/constants';
import { isCurrentUserBot } from 'src/utils/isBot';
const propTypes = {
id: PropTypes.string.isRequired,
@ -61,6 +63,7 @@ class Row extends React.PureComponent {
super(props);
this.state = {
isFocused: false,
isInView: false,
};
this.handleDeleteComponent = this.handleDeleteComponent.bind(this);
this.handleUpdateMeta = this.handleUpdateMeta.bind(this);
@ -69,6 +72,50 @@ class Row extends React.PureComponent {
'background',
);
this.handleChangeFocus = this.handleChangeFocus.bind(this);
this.containerRef = React.createRef();
this.observerEnabler = null;
this.observerDisabler = null;
}
// if chart not rendered - render it if it's less than 1 view height away from current viewport
// if chart rendered - remove it if it's more than 4 view heights away from current viewport
componentDidMount() {
if (
isFeatureEnabled(FeatureFlag.DASHBOARD_VIRTUALIZATION) &&
!isCurrentUserBot()
) {
this.observerEnabler = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting && !this.state.isInView) {
this.setState({ isInView: true });
}
},
{
rootMargin: '100% 0px',
},
);
this.observerDisabler = new IntersectionObserver(
([entry]) => {
if (!entry.isIntersecting && this.state.isInView) {
this.setState({ isInView: false });
}
},
{
rootMargin: '400% 0px',
},
);
const element = this.containerRef.current;
if (element) {
this.observerEnabler.observe(element);
this.observerDisabler.observe(element);
}
}
}
componentWillUnmount() {
this.observerEnabler?.disconnect();
this.observerDisabler?.disconnect();
}
handleChangeFocus(nextFocus) {
@ -161,6 +208,7 @@ class Row extends React.PureComponent {
backgroundStyle.className,
)}
data-test={`grid-row-${backgroundStyle.className}`}
ref={this.containerRef}
>
{rowItems.map((componentId, itemIndex) => (
<DashboardComponent
@ -178,6 +226,7 @@ class Row extends React.PureComponent {
onResizeStop={onResizeStop}
isComponentVisible={isComponentVisible}
onChangeTab={onChangeTab}
isInView={this.state.isInView}
/>
))}

View File

@ -0,0 +1,21 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
// navigator.webdriver is true when browser is controlled by a bot
export const isCurrentUserBot = () => window?.navigator?.webdriver;

View File

@ -0,0 +1,30 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export enum DASHBOARD_VIRTUALIZATION_MODE {
NONE = 'NONE',
VIEWPORT = 'VIEWPORT',
PAGINATED = 'PAGINATED',
}
export const isDashboardVirtualizationEnabled = (
virtualizationMode: DASHBOARD_VIRTUALIZATION_MODE,
) =>
virtualizationMode === DASHBOARD_VIRTUALIZATION_MODE.VIEWPORT ||
virtualizationMode === DASHBOARD_VIRTUALIZATION_MODE.PAGINATED;

View File

@ -425,6 +425,7 @@ DEFAULT_FEATURE_FLAGS: Dict[str, bool] = {
# Feature is under active development and breaking changes are expected
"DASHBOARD_NATIVE_FILTERS_SET": False,
"DASHBOARD_FILTERS_EXPERIMENTAL": False,
"DASHBOARD_VIRTUALIZATION": False,
"GLOBAL_ASYNC_QUERIES": False,
"VERSIONED_EXPORT": True,
"EMBEDDED_SUPERSET": False,
@ -761,7 +762,6 @@ SQLLAB_SCHEDULE_WARNING_MESSAGE = None
# Force refresh while auto-refresh in dashboard
DASHBOARD_AUTO_REFRESH_MODE: Literal["fetch", "force"] = "force"
# Default celery config is to use SQLA as a broker, in a production setting
# you'll want to use a proper broker as specified here:
# http://docs.celeryproject.org/en/latest/getting-started/brokers/index.html

View File

@ -103,6 +103,7 @@ FRONTEND_CONF_KEYS = (
"SQLALCHEMY_DISPLAY_TEXT",
"GLOBAL_ASYNC_QUERIES_WEBSOCKET_URL",
"DASHBOARD_AUTO_REFRESH_MODE",
"DASHBOARD_VIRTUALIZATION",
"SCHEDULED_QUERIES",
"EXCEL_EXTENSIONS",
"CSV_EXTENSIONS",