mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-10-02 15:58:11 +08:00
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:
@@ -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>
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Vendored
-5
@@ -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,
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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]: {
|
||||||
|
|||||||
Reference in New Issue
Block a user