render perf refinements (#1632)

* render performance improvements

* improve render perf

* remove logging

* lint

* improve memoziation
This commit is contained in:
Bruce Martin
2020-07-16 09:53:46 -07:00
committed by GitHub
parent 17c1145ee4
commit 2265bfc921
11 changed files with 163 additions and 114 deletions
+1
View File
@@ -284,4 +284,5 @@ export default {
saveObsAnnotationsAction: annoActions.saveObsAnnotationsAction, saveObsAnnotationsAction: annoActions.saveObsAnnotationsAction,
needToSaveObsAnnotations: annoActions.needToSaveObsAnnotations, needToSaveObsAnnotations: annoActions.needToSaveObsAnnotations,
layoutChoiceAction: selnActions.layoutChoiceAction, layoutChoiceAction: selnActions.layoutChoiceAction,
setCellSetFromSelection: selnActions.setCellSetFromSelection,
}; };
+13
View File
@@ -209,3 +209,16 @@ export const layoutChoiceAction = (newLayoutChoice) => async (
obsCrossfilter, obsCrossfilter,
}); });
}; };
/*
Differential expression set selection
*/
export const setCellSetFromSelection = (cellSetId) => (dispatch, getState) => {
const { obsCrossfilter } = getState();
const selected = obsCrossfilter.allSelectedLabels();
dispatch({
type: `store current cell selection as differential set ${cellSetId}`,
data: selected.length > 0 ? selected : null,
});
};
+40 -12
View File
@@ -139,6 +139,32 @@ class Graph extends React.Component {
} }
); );
computeHighlightFlags = memoize(
(nObs, pointDilationData, pointDilationLabel) => {
const flags = new Float32Array(nObs);
if (pointDilationData) {
for (let i = 0, len = flags.length; i < len; i += 1) {
if (pointDilationData[i] === pointDilationLabel) {
flags[i] = flagHighlight;
}
}
}
return flags;
}
);
computeColorByFlags = memoize((nObs, colorByData) => {
const flags = new Float32Array(nObs);
if (colorByData) {
for (let i = 0, len = flags.length; i < len; i += 1) {
if (!Number.isFinite(colorByData[i])) {
flags[i] = flagNaN;
}
}
}
return flags;
});
computePointFlags = memoize( computePointFlags = memoize(
(crossfilter, colorByData, pointDilationData, pointDilationLabel) => { (crossfilter, colorByData, pointDilationData, pointDilationLabel) => {
/* /*
@@ -154,23 +180,25 @@ class Graph extends React.Component {
continuous metadata, as they rely on different tests, and some of the flags continuous metadata, as they rely on different tests, and some of the flags
(eg, isNaN) are meaningless in the face of categorical metadata. (eg, isNaN) are meaningless in the face of categorical metadata.
*/ */
const flags = this.computeSelectedFlags( const nObs = crossfilter.size();
const flags = new Float32Array(nObs);
const selectedFlags = this.computeSelectedFlags(
crossfilter, crossfilter,
flagSelected, flagSelected,
0 0
).slice(); );
const highlightFlags = this.computeHighlightFlags(
nObs,
pointDilationData,
pointDilationLabel
);
const colorByFlags = this.computeColorByFlags(nObs, colorByData);
if (colorByData || pointDilationData) { for (let i = 0; i < nObs; i += 1) {
for (let i = 0, len = flags.length; i < len; i += 1) { flags[i] = selectedFlags[i] + highlightFlags[i] + colorByFlags[i];
if (pointDilationData) {
flags[i] +=
pointDilationData[i] === pointDilationLabel ? flagHighlight : 0;
}
if (colorByData) {
flags[i] += Number.isFinite(colorByData[i]) ? 0 : flagNaN;
}
}
} }
return flags; return flags;
} }
); );
@@ -3,25 +3,18 @@ import React from "react";
import { AnchorButton, Tooltip } from "@blueprintjs/core"; import { AnchorButton, Tooltip } from "@blueprintjs/core";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { tooltipHoverOpenDelay } from "../../globals"; import { tooltipHoverOpenDelay } from "../../globals";
import actions from "../../actions";
@connect() @connect((state) => ({
differential: state.differential,
}))
class CellSetButton extends React.PureComponent { class CellSetButton extends React.PureComponent {
set() { set() {
const { const { differential, dispatch, eitherCellSetOneOrTwo } = this.props;
differential,
crossfilter,
dispatch,
eitherCellSetOneOrTwo,
} = this.props;
let set = crossfilter.allSelectedLabels();
if (set.length === 0) set = null;
if (!differential.diffExp) { if (!differential.diffExp) {
/* diffexp needs to be cleared before we store a new set */ // disallow this action if the user has active differential expression results
dispatch({ dispatch(actions.setCellSetFromSelection(eitherCellSetOneOrTwo));
type: `store current cell selection as differential set ${eitherCellSetOneOrTwo}`,
data: set,
});
} }
} }
+2 -3
View File
@@ -1,4 +1,3 @@
// jshint esversion: 6
import React from "react"; import React from "react";
import { import {
Position, Position,
@@ -11,7 +10,7 @@ import {
import { tooltipHoverOpenDelay } from "../../globals"; import { tooltipHoverOpenDelay } from "../../globals";
import styles from "./menubar.css"; import styles from "./menubar.css";
function Clip(props) { const Clip = React.memo((props) => {
const { const {
pendingClipPercentiles, pendingClipPercentiles,
clipPercentileMin, clipPercentileMin,
@@ -129,6 +128,6 @@ function Clip(props) {
/> />
</div> </div>
); );
} });
export default Clip; export default Clip;
@@ -1,4 +1,3 @@
// jshint esversion: 6
import React from "react"; import React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Button, ButtonGroup, AnchorButton, Tooltip } from "@blueprintjs/core"; import { Button, ButtonGroup, AnchorButton, Tooltip } from "@blueprintjs/core";
@@ -8,15 +7,13 @@ import actions from "../../actions";
import CellSetButton from "./cellSetButtons"; import CellSetButton from "./cellSetButtons";
@connect((state) => ({ @connect((state) => ({
config: state.config,
crossfilter: state.obsCrossfilter,
differential: state.differential, differential: state.differential,
celllist1: state.differential?.celllist1, celllist1: state.differential?.celllist1,
celllist2: state.differential?.celllist2, celllist2: state.differential?.celllist2,
diffexpMayBeSlow: state.config?.parameters?.["diffexp-may-be-slow"] ?? false, diffexpMayBeSlow: state.config?.parameters?.["diffexp-may-be-slow"] ?? false,
diffexpCellcountMax: state.config?.limits?.["diffexp_cellcount_max"], diffexpCellcountMax: state.config?.limits?.["diffexp_cellcount_max"],
})) }))
class DiffexpButtons extends React.Component { class DiffexpButtons extends React.PureComponent {
computeDiffExp = () => { computeDiffExp = () => {
const { dispatch, differential } = this.props; const { dispatch, differential } = this.props;
if (differential.celllist1 && differential.celllist2) { if (differential.celllist1 && differential.celllist2) {
@@ -42,13 +39,7 @@ class DiffexpButtons extends React.Component {
render() { render() {
/* diffexp-related buttons may be disabled */ /* diffexp-related buttons may be disabled */
const { const { differential, diffexpMayBeSlow, diffexpCellcountMax } = this.props;
differential,
diffexpMayBeSlow,
diffexpCellcountMax,
crossfilter,
dispatch,
} = this.props;
const haveBothCellSets = const haveBothCellSets =
!!differential.celllist1 && !!differential.celllist2; !!differential.celllist1 && !!differential.celllist2;
@@ -72,17 +63,8 @@ class DiffexpButtons extends React.Component {
return ( return (
<ButtonGroup className={styles.menubarButton}> <ButtonGroup className={styles.menubarButton}>
{/* eslint-disable react/jsx-props-no-spreading --- disable until eslint-config-airbnb v18.1.1*/} <CellSetButton eitherCellSetOneOrTwo={1} />
<CellSetButton <CellSetButton eitherCellSetOneOrTwo={2} />
{...{ differential, crossfilter, dispatch }}
eitherCellSetOneOrTwo={1}
/>
<CellSetButton
{...{ differential, crossfilter, dispatch }}
eitherCellSetOneOrTwo={2}
/>
{/* eslint-enable react/jsx-props-no-spreading --- end disable*/}
{!differential.diffExp ? ( {!differential.diffExp ? (
<Tooltip <Tooltip
content={warnMaxSizeExceeded ? tipMessageWarn : tipMessage} content={warnMaxSizeExceeded ? tipMessageWarn : tipMessage}
+50 -43
View File
@@ -12,32 +12,44 @@ import Subset from "./subset";
import UndoRedoReset from "./undoRedo"; import UndoRedoReset from "./undoRedo";
import DiffexpButtons from "./diffexpButtons"; import DiffexpButtons from "./diffexpButtons";
@connect((state) => ({ @connect((state) => {
annoMatrix: state.annoMatrix, const { annoMatrix } = state;
crossfilter: state.obsCrossfilter, const crossfilter = state.obsCrossfilter;
differential: state.differential, const selectedCount = crossfilter.countSelected();
graphInteractionMode: state.controls.graphInteractionMode,
clipPercentileMin: Math.round(100 * (state.annoMatrix?.clipRange?.[0] ?? 0)), const subsetPossible =
clipPercentileMax: Math.round(100 * (state.annoMatrix?.clipRange?.[1] ?? 1)), selectedCount !== 0 && selectedCount !== crossfilter.size(); // ie, not all are selected
userDefinedGenes: state.controls.userDefinedGenes, const subsetResetPossible =
diffexpGenes: state.controls.diffexpGenes, annoMatrix.nObs !== annoMatrix.schema.dataframe.nObs;
colorAccessor: state.colors.colorAccessor,
scatterplotXXaccessor: state.controls.scatterplotXXaccessor, return {
scatterplotYYaccessor: state.controls.scatterplotYYaccessor, subsetPossible,
celllist1: state.differential.celllist1, subsetResetPossible,
celllist2: state.differential.celllist2, differential: state.differential,
libraryVersions: state.config?.["library_versions"], graphInteractionMode: state.controls.graphInteractionMode,
undoDisabled: state["@@undoable/past"].length === 0, clipPercentileMin: Math.round(100 * (annoMatrix?.clipRange?.[0] ?? 0)),
redoDisabled: state["@@undoable/future"].length === 0, clipPercentileMax: Math.round(100 * (annoMatrix?.clipRange?.[1] ?? 1)),
aboutLink: state.config?.links?.["about-dataset"], userDefinedGenes: state.controls.userDefinedGenes,
disableDiffexp: state.config?.parameters?.["disable-diffexp"] ?? false, diffexpGenes: state.controls.diffexpGenes,
diffexpMayBeSlow: state.config?.parameters?.["diffexp-may-be-slow"] ?? false, colorAccessor: state.colors.colorAccessor,
showCentroidLabels: state.centroidLabels.showLabels, scatterplotXXaccessor: state.controls.scatterplotXXaccessor,
tosURL: state.config?.parameters?.["about_legal_tos"], scatterplotYYaccessor: state.controls.scatterplotYYaccessor,
privacyURL: state.config?.parameters?.["about_legal_privacy"], celllist1: state.differential.celllist1,
categoricalSelection: state.categoricalSelection, celllist2: state.differential.celllist2,
})) libraryVersions: state.config?.["library_versions"],
class MenuBar extends React.Component { undoDisabled: state["@@undoable/past"].length === 0,
redoDisabled: state["@@undoable/future"].length === 0,
aboutLink: state.config?.links?.["about-dataset"],
disableDiffexp: state.config?.parameters?.["disable-diffexp"] ?? false,
diffexpMayBeSlow:
state.config?.parameters?.["diffexp-may-be-slow"] ?? false,
showCentroidLabels: state.centroidLabels.showLabels,
tosURL: state.config?.parameters?.["about_legal_tos"],
privacyURL: state.config?.parameters?.["about_legal_privacy"],
categoricalSelection: state.categoricalSelection,
};
})
class MenuBar extends React.PureComponent {
static isValidDigitKeyEvent(e) { static isValidDigitKeyEvent(e) {
/* /*
Return true if this event is necessary to enter a percent number input. Return true if this event is necessary to enter a percent number input.
@@ -171,17 +183,14 @@ class MenuBar extends React.Component {
}); });
}; };
subsetPossible = () => { handleSubset = () => {
const { crossfilter } = this.props; const { dispatch } = this.props;
const count = crossfilter.countSelected(); dispatch(actions.subsetAction());
return (
count !== 0 && count !== crossfilter.size() // ie, not all are selected
);
}; };
subsetResetPossible = () => { handleSubsetReset = () => {
const { annoMatrix } = this.props; const { dispatch } = this.props;
return annoMatrix.nObs !== annoMatrix.schema.dataframe.nObs; dispatch(actions.resetSubsetAction());
}; };
render() { render() {
@@ -201,6 +210,8 @@ class MenuBar extends React.Component {
tosURL, tosURL,
categoricalSelection, categoricalSelection,
colorAccessor, colorAccessor,
subsetPossible,
subsetResetPossible,
} = this.props; } = this.props;
const { pendingClipPercentiles } = this.state; const { pendingClipPercentiles } = this.state;
@@ -309,14 +320,10 @@ class MenuBar extends React.Component {
</Tooltip> </Tooltip>
</ButtonGroup> </ButtonGroup>
<Subset <Subset
subsetPossible={this.subsetPossible()} subsetPossible={subsetPossible}
subsetResetPossible={this.subsetResetPossible()} subsetResetPossible={subsetResetPossible}
handleSubset={() => { handleSubset={this.handleSubset}
dispatch(actions.subsetAction()); handleSubsetReset={this.handleSubsetReset}
}}
handleSubsetReset={() => {
dispatch(actions.resetSubsetAction());
}}
/> />
{disableDiffexp ? null : <DiffexpButtons />} {disableDiffexp ? null : <DiffexpButtons />}
</div> </div>
+2 -2
View File
@@ -3,7 +3,7 @@ import React from "react";
import { Button, Popover, Menu, MenuItem, Position } from "@blueprintjs/core"; import { Button, Popover, Menu, MenuItem, Position } from "@blueprintjs/core";
import styles from "./menubar.css"; import styles from "./menubar.css";
function InformationMenu(props) { const InformationMenu = React.memo((props) => {
const { libraryVersions, aboutLink, tosURL, privacyURL } = props; const { libraryVersions, aboutLink, tosURL, privacyURL } = props;
return ( return (
<div className={`bp3-button-group ${styles.menubarButton}`}> <div className={`bp3-button-group ${styles.menubarButton}`}>
@@ -72,6 +72,6 @@ function InformationMenu(props) {
</Popover> </Popover>
</div> </div>
); );
} });
export default InformationMenu; export default InformationMenu;
+2 -2
View File
@@ -3,7 +3,7 @@ import { AnchorButton, ButtonGroup, Tooltip } from "@blueprintjs/core";
import styles from "./menubar.css"; import styles from "./menubar.css";
import * as globals from "../../globals"; import * as globals from "../../globals";
function Subset(props) { const Subset = React.memo((props) => {
const { const {
subsetPossible, subsetPossible,
subsetResetPossible, subsetResetPossible,
@@ -41,6 +41,6 @@ function Subset(props) {
</Tooltip> </Tooltip>
</ButtonGroup> </ButtonGroup>
); );
} });
export default Subset; export default Subset;
+3 -4
View File
@@ -1,10 +1,9 @@
// jshint esversion: 6
import React from "react"; import React from "react";
import { AnchorButton, Tooltip } from "@blueprintjs/core"; import { AnchorButton, Tooltip } from "@blueprintjs/core";
import { tooltipHoverOpenDelay } from "../../globals"; import { tooltipHoverOpenDelay } from "../../globals";
import styles from "./menubar.css"; import styles from "./menubar.css";
function InformationMenu(props) { const UndoRedo = React.memo((props) => {
const { undoDisabled, redoDisabled, dispatch } = props; const { undoDisabled, redoDisabled, dispatch } = props;
return ( return (
<div className={`bp3-button-group ${styles.menubarButton}`}> <div className={`bp3-button-group ${styles.menubarButton}`}>
@@ -46,6 +45,6 @@ function InformationMenu(props) {
</Tooltip> </Tooltip>
</div> </div>
); );
} });
export default InformationMenu; export default UndoRedo;
@@ -117,6 +117,32 @@ class Scatterplot extends React.PureComponent {
} }
); );
computeHighlightFlags = memoize(
(nObs, pointDilationData, pointDilationLabel) => {
const flags = new Float32Array(nObs);
if (pointDilationData) {
for (let i = 0, len = flags.length; i < len; i += 1) {
if (pointDilationData[i] === pointDilationLabel) {
flags[i] = flagHighlight;
}
}
}
return flags;
}
);
computeColorByFlags = memoize((nObs, colorByData) => {
const flags = new Float32Array(nObs);
if (colorByData) {
for (let i = 0, len = flags.length; i < len; i += 1) {
if (!Number.isFinite(colorByData[i])) {
flags[i] = flagNaN;
}
}
}
return flags;
});
computePointFlags = memoize( computePointFlags = memoize(
(crossfilter, colorByData, pointDilationData, pointDilationLabel) => { (crossfilter, colorByData, pointDilationData, pointDilationLabel) => {
/* /*
@@ -132,24 +158,25 @@ class Scatterplot extends React.PureComponent {
continuous metadata, as they rely on different tests, and some of the flags continuous metadata, as they rely on different tests, and some of the flags
(eg, isNaN) are meaningless in the face of categorical metadata. (eg, isNaN) are meaningless in the face of categorical metadata.
*/ */
const nObs = crossfilter.size();
const flags = this.computeSelectedFlags( const selectedFlags = this.computeSelectedFlags(
crossfilter, crossfilter,
flagSelected, flagSelected,
0 0
).slice(); );
const highlightFlags = this.computeHighlightFlags(
nObs,
pointDilationData,
pointDilationLabel
);
const colorByFlags = this.computeColorByFlags(nObs, colorByData);
if (colorByData || pointDilationData) { const flags = new Float32Array(nObs);
for (let i = 0, len = flags.length; i < len; i += 1) { for (let i = 0; i < nObs; i += 1) {
if (pointDilationData) { flags[i] = selectedFlags[i] + highlightFlags[i] + colorByFlags[i];
flags[i] +=
pointDilationData[i] === pointDilationLabel ? flagHighlight : 0;
}
if (colorByData) {
flags[i] += Number.isFinite(colorByData[i]) ? 0 : flagNaN;
}
}
} }
return flags; return flags;
} }
); );