rendering performance improvements (#968)

* freeze objects

* component rendering perf work

* use PureComponent where safe

* remove obsolete WorldUtil code

* make brushable histogram a pure component
This commit is contained in:
Bruce Martin
2019-10-04 07:59:22 -07:00
committed by GitHub
parent 700c871e6d
commit 3f2811d9da
12 changed files with 47 additions and 190 deletions
@@ -1,62 +0,0 @@
import {
countCategoryValues2D,
clearCaches
} from "../../../src/util/stateManager/worldUtil";
import * as Dataframe from "../../../src/util/dataframe";
describe("WorldUtil cache management", () => {
test("empty", () => {
const count = countCategoryValues2D(
"a",
"b",
new Dataframe.Dataframe([0, 0], [])
);
expect(count).toMatchObject(new Map());
expect(count.size).toBe(0);
});
test("simple couts", () => {
const df = new Dataframe.Dataframe(
[3, 2],
[[0, 0, 1], [false, true, false]],
null,
new Dataframe.KeyIndex(["a", "b"])
);
const count = countCategoryValues2D("a", "b", df);
expect(count).toMatchObject(
new Map([
[0, new Map([[true, 1], [false, 1]])],
[1, new Map([[false, 1]])]
])
);
});
test("memo cache clear", () => {
clearCaches();
const df1 = new Dataframe.Dataframe([0, 0], []);
const df2 = new Dataframe.Dataframe(
[3, 2],
[[0, 0, 1], [false, true, false]],
null,
new Dataframe.KeyIndex(["a", "b"])
);
const count1 = countCategoryValues2D("a", "b", df1);
const count2 = countCategoryValues2D("a", "b", df1);
const count3 = countCategoryValues2D("a", "b", df1.clone());
const count4 = countCategoryValues2D("a", "b", df2);
clearCaches();
const count10 = countCategoryValues2D("a", "b", df1);
const count11 = countCategoryValues2D("a", "b", df2);
expect(count1).toEqual(count2);
expect(count1).toEqual(count3);
expect(count1).toEqual(count10);
expect(count1).not.toBe(count3);
expect(count1).not.toBe(count10);
expect(count4).toEqual(count11);
expect(count4).not.toBe(count11);
});
});
@@ -13,15 +13,21 @@ import * as globals from "../../globals";
import actions from "../../actions"; import actions from "../../actions";
import { makeContinuousDimensionName } from "../../util/nameCreators"; import { makeContinuousDimensionName } from "../../util/nameCreators";
@connect(state => ({ @connect((state, ownProps) => {
world: state.world, const { isObs, isUserDefined, isDiffExp, field } = ownProps;
scatterplotXXaccessor: state.controls.scatterplotXXaccessor, const myName = makeContinuousDimensionName(
scatterplotYYaccessor: state.controls.scatterplotYYaccessor, { isObs, isUserDefined, isDiffExp },
continuousSelection: state.continuousSelection, field
differential: state.differential, );
colorAccessor: state.colors.colorAccessor return {
})) world: state.world,
class HistogramBrush extends React.Component { scatterplotXXaccessor: state.controls.scatterplotXXaccessor,
scatterplotYYaccessor: state.controls.scatterplotYYaccessor,
continuousSelectionRange: state.continuousSelection[myName],
colorAccessor: state.colors.colorAccessor
};
})
class HistogramBrush extends React.PureComponent {
static getColumn(world, field, clipped = true) { static getColumn(world, field, clipped = true) {
/* /*
Return the underlying Dataframe column for our field. By default, Return the underlying Dataframe column for our field. By default,
@@ -84,7 +90,7 @@ class HistogramBrush extends React.Component {
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
const { field, world, continuousSelection } = this.props; const { field, world } = this.props;
const { x, y, bins, svgRef } = this._histogram; const { x, y, bins, svgRef } = this._histogram;
let { brushXselection, brushX } = this.state; let { brushXselection, brushX } = this.state;
let forceBrushUpdate = false; let forceBrushUpdate = false;
@@ -112,16 +118,8 @@ class HistogramBrush extends React.Component {
if the selection has changed, ensure that the brush correctly reflects if the selection has changed, ensure that the brush correctly reflects
the underlying selection. the underlying selection.
*/ */
if ( const { continuousSelectionRange: range } = this.props;
forceBrushUpdate || if (forceBrushUpdate || range !== prevProps.continuousSelectionRange) {
continuousSelection !== prevProps.continuousSelection
) {
const { isObs, isUserDefined, isDiffExp } = this.props;
const myName = makeContinuousDimensionName(
{ isObs, isUserDefined, isDiffExp },
field
);
const range = continuousSelection[myName];
if (brushXselection) { if (brushXselection) {
const selection = d3.brushSelection(brushXselection.node()); const selection = d3.brushSelection(brushXselection.node());
if (!range && selection) { if (!range && selection) {
@@ -417,8 +415,8 @@ class HistogramBrush extends React.Component {
isDiffExp isDiffExp
? "histogram-diffexp" ? "histogram-diffexp"
: isUserDefined : isUserDefined
? "histogram-user-gene" ? "histogram-user-gene"
: "histogram-continuous-metadata" : "histogram-continuous-metadata"
} }
style={{ style={{
padding: globals.leftSidebarSectionPadding, padding: globals.leftSidebarSectionPadding,
@@ -76,30 +76,26 @@ class Categories extends React.Component {
> >
{/* READ ONLY CATEGORICAL FIELDS */} {/* READ ONLY CATEGORICAL FIELDS */}
{/* this is duplicative but flat, could be abstracted */} {/* this is duplicative but flat, could be abstracted */}
{_.map( {_.map(allCategoryNames, catName =>
allCategoryNames, !schema.annotations.obsByName[catName].writable ? (
catName => <Category
!schema.annotations.obsByName[catName].writable ? ( key={catName}
<Category metadataField={catName}
key={catName} createAnnoModeActive={createAnnoModeActive}
metadataField={catName} isUserAnno={false}
createAnnoModeActive={createAnnoModeActive} />
isUserAnno={false} ) : null
/>
) : null
)} )}
{/* WRITEABLE FIELDS */} {/* WRITEABLE FIELDS */}
{_.map( {_.map(allCategoryNames, catName =>
allCategoryNames, schema.annotations.obsByName[catName].writable ? (
catName => <Category
schema.annotations.obsByName[catName].writable ? ( key={catName}
<Category metadataField={catName}
key={catName} createAnnoModeActive={createAnnoModeActive}
metadataField={catName} isUserAnno
createAnnoModeActive={createAnnoModeActive} />
isUserAnno ) : null
/>
) : null
)} )}
{writableCategoriesEnabled ? ( {writableCategoriesEnabled ? (
<div> <div>
+1 -1
View File
@@ -83,7 +83,7 @@ function renderThrottle(callback) {
graphInteractionMode: state.controls.graphInteractionMode, graphInteractionMode: state.controls.graphInteractionMode,
colorAccessor: state.colors.colorAccessor colorAccessor: state.colors.colorAccessor
})) }))
class Graph extends React.Component { class Graph extends React.PureComponent {
computePointPositions = memoize((X, Y, modelTF) => { computePointPositions = memoize((X, Y, modelTF) => {
/* /*
compute the model coordinate for each point compute the model coordinate for each point
@@ -6,7 +6,7 @@ import { World } from "../../util/stateManager";
import { tooltipHoverOpenDelay } from "../../globals"; import { tooltipHoverOpenDelay } from "../../globals";
@connect() @connect()
class CellSetButton extends React.Component { class CellSetButton extends React.PureComponent {
set() { set() {
const { const {
differential, differential,
@@ -57,7 +57,7 @@ function createProjectionTF(viewportWidth, viewportHeight) {
responsive: state.responsive responsive: state.responsive
}; };
}) })
class Scatterplot extends React.Component { class Scatterplot extends React.PureComponent {
computePointPositions = memoize((X, Y, xScale, yScale) => { computePointPositions = memoize((X, Y, xScale, yScale) => {
const positions = new Float32Array(2 * X.length); const positions = new Float32Array(2 * X.length);
for (let i = 0, len = X.length; i < len; i += 1) { for (let i = 0, len = X.length; i < len; i += 1) {
-5
View File
@@ -2,8 +2,6 @@
import _ from "lodash"; import _ from "lodash";
import { WorldUtil } from "../util/stateManager";
const Controls = ( const Controls = (
state = { state = {
// data loading flag // data loading flag
@@ -45,7 +43,6 @@ const Controls = (
} }
case "initial data load complete (universe exists)": { case "initial data load complete (universe exists)": {
/* first light - create world & other data-driven defaults */ /* first light - create world & other data-driven defaults */
WorldUtil.clearCaches();
return { return {
...state, ...state,
loading: false, loading: false,
@@ -54,14 +51,12 @@ const Controls = (
}; };
} }
case "reset World to eq Universe": { case "reset World to eq Universe": {
WorldUtil.clearCaches();
return { return {
...state, ...state,
resettingInterface: false resettingInterface: false
}; };
} }
case "set World to current selection": { case "set World to current selection": {
WorldUtil.clearCaches();
return { return {
...state, ...state,
loading: false, loading: false,
+3
View File
@@ -127,6 +127,7 @@ class Dataframe {
this.__id = Dataframe.__getId(); this.__id = Dataframe.__getId();
this.__compile(__columnsAccessor); this.__compile(__columnsAccessor);
Object.freeze(this);
} }
static __errorChecks(dims, columnarData, rowIndex, colIndex) { static __errorChecks(dims, columnarData, rowIndex, colIndex) {
@@ -269,6 +270,7 @@ class Dataframe {
get.iget = iget; get.iget = iget;
get.__id = __id; get.__id = __id;
Object.freeze(get);
return get; return get;
} }
@@ -288,6 +290,7 @@ class Dataframe {
} }
return Dataframe.__compileColumn(column, getRowByOffset, getRowByLabel); return Dataframe.__compileColumn(column, getRowByOffset, getRowByLabel);
}); });
Object.freeze(this.__columnsAccessor);
} }
clone() { clone() {
-1
View File
@@ -17,7 +17,6 @@ exists to support those concepts.
export * as ColorHelpers from "./colorHelpers"; export * as ColorHelpers from "./colorHelpers";
export * as Universe from "./universe"; export * as Universe from "./universe";
export * as World from "./world"; export * as World from "./world";
export * as WorldUtil from "./worldUtil";
export * as ControlsHelpers from "./controlsHelpers"; export * as ControlsHelpers from "./controlsHelpers";
export * as AnnotationsHelpers from "./annotationsHelpers"; export * as AnnotationsHelpers from "./annotationsHelpers";
export * as SchemaHelpers from "./schemaHelpers"; export * as SchemaHelpers from "./schemaHelpers";
-75
View File
@@ -1,75 +0,0 @@
/* eslint-disable import/prefer-default-export */
import _ from "lodash";
/*
Various utility functions operating on World/Universe
*/
/*
Count unique category values, binning first by dim1 then by dim2
Return:
Map {
dim1_val1: Map {
dim2_val1: number,
dim2_val2: number,
...
},
...
}
Parameters are:
- dim1: dimension 1 name/label
- dim2: dimension 2 name/label
- df: dataframe containing dim1 and dim2 on the column axis
*/
function _countCategoryValues2D(dim1, dim2, df) {
const dimMap = new Map();
const col1 = df.col(dim1) ? df.col(dim1).asArray() : null;
const col2 = df.col(dim2) ? df.col(dim2).asArray() : null;
if (!col1 || !col2) {
return dimMap;
}
for (let r = 0, l = df.length; r < l; r += 1) {
const val1 = col1[r];
const val2 = col2[r];
let d2Map = dimMap.get(val1);
if (d2Map === undefined) {
d2Map = new Map();
dimMap.set(val1, d2Map);
}
let curCount = d2Map.get(val2);
if (curCount === undefined) {
curCount = 0;
}
d2Map.set(val2, curCount + 1);
}
return dimMap;
}
let __worldUtilMemoId__ = 0;
function _memoizedId(x) {
if (!x.__worldUtilMemoId__) {
__worldUtilMemoId__ += 1;
x.__worldUtilMemoId__ = __worldUtilMemoId__;
}
return x.__worldUtilMemoId__;
}
function _countCategoryValues2DResolver(...args) {
const id = args[0] + args[1] + _memoizedId(args[2]);
return id;
}
export const countCategoryValues2D = _.memoize(
_countCategoryValues2D,
_countCategoryValues2DResolver
);
/*
Clear any cached data within WorldUtil caches, eg, memoized functions
*/
export function clearCaches() {
countCategoryValues2D.cache.clear();
}
@@ -35,6 +35,7 @@ class BitArray {
this.bitmask = new Int32Array(this.width); // dimension allocation mask this.bitmask = new Int32Array(this.width); // dimension allocation mask
this.bitarray = new Int32Array(this.width * this.length); this.bitarray = new Int32Array(this.width * this.length);
Object.seal(this);
} }
// Return the number of records that are selected, ie, have a one bit in // Return the number of records that are selected, ie, have a one bit in
@@ -47,6 +47,7 @@ export default class ImmutableTypedCrossfilter {
this.data = data; this.data = data;
this.selectionCache = selectionCache; /* BitArray */ this.selectionCache = selectionCache; /* BitArray */
this.dimensions = dimensions; /* name: { id, dim, name, selection } */ this.dimensions = dimensions; /* name: { id, dim, name, selection } */
Object.preventExtensions(this);
} }
size() { size() {
@@ -94,6 +95,7 @@ export default class ImmutableTypedCrossfilter {
} }
const DimensionType = DimTypes[type]; const DimensionType = DimTypes[type];
const dim = new DimensionType(name, data, ...rest); const dim = new DimensionType(name, data, ...rest);
Object.freeze(dim);
const dimensions = { const dimensions = {
...this.dimensions, ...this.dimensions,
[name]: { [name]: {