Skip to content

Commit

Permalink
Revert "Use sparse array for cell position caches (#1312)"
Browse files Browse the repository at this point in the history
This reverts commit 7be1258.
  • Loading branch information
wuweiweiwu authored May 22, 2019
1 parent 0b7c1d1 commit 917a9a3
Show file tree
Hide file tree
Showing 3 changed files with 117 additions and 71 deletions.
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,6 @@
"babel-runtime": "^6.26.0",
"clsx": "^1.0.1",
"dom-helpers": "^2.4.0 || ^3.0.0",
"linear-layout-vector": "0.0.1",
"loose-envify": "^1.3.0",
"prop-types": "^15.6.0",
"react-lifecycles-compat": "^3.0.4"
Expand Down
182 changes: 117 additions & 65 deletions source/Grid/utils/CellSizeAndPositionManager.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
/** @flow */

import LinearLayoutVector from 'linear-layout-vector';
import type {Alignment, CellSizeGetter, VisibleCellRange} from '../types';

type CellSizeAndPositionManagerParams = {
Expand Down Expand Up @@ -39,11 +38,14 @@ type SizeAndPositionData = {
export default class CellSizeAndPositionManager {
// Cache of size and position data for cells, mapped by cell index.
// Note that invalid values may exist in this map so only rely on cells up to this._lastMeasuredIndex
_layoutVector: LinearLayoutVector;
_cellSizeAndPositionData = {};

// Measurements for cells up to this index can be trusted; cells afterward should be estimated.
_lastMeasuredIndex = -1;

// Used in deferred mode to track which cells have been queued for measurement.
_lastBatchedIndex = -1;

_cellCount: number;
_cellSizeGetter: CellSizeGetter;
_estimatedCellSize: number;
Expand All @@ -56,9 +58,6 @@ export default class CellSizeAndPositionManager {
this._cellSizeGetter = cellSizeGetter;
this._cellCount = cellCount;
this._estimatedCellSize = estimatedCellSize;
this._layoutVector = new LinearLayoutVector();
this._layoutVector.setLength(cellCount);
this._layoutVector.setDefaultSize(estimatedCellSize);
}

areOffsetsAdjusted() {
Expand All @@ -69,8 +68,6 @@ export default class CellSizeAndPositionManager {
this._cellCount = cellCount;
this._estimatedCellSize = estimatedCellSize;
this._cellSizeGetter = cellSizeGetter;
this._layoutVector.setLength(cellCount);
this._layoutVector.setDefaultSize(estimatedCellSize);
}

getCellCount(): number {
Expand Down Expand Up @@ -99,42 +96,50 @@ export default class CellSizeAndPositionManager {
`Requested index ${index} is outside of range 0..${this._cellCount}`,
);
}
const vector = this._layoutVector;

if (index > this._lastMeasuredIndex) {
const token = {index: this._lastMeasuredIndex + 1};
let lastMeasuredCellSizeAndPosition = this.getSizeAndPositionOfLastMeasuredCell();
let offset =
lastMeasuredCellSizeAndPosition.offset +
lastMeasuredCellSizeAndPosition.size;

for (var i = this._lastMeasuredIndex + 1; i <= index; i++) {
let size = this._cellSizeGetter({index: i});

for (var i = token.index; i <= index; token.index = ++i) {
const size = this._cellSizeGetter(token);
// undefined or NaN probably means a logic error in the size getter.
// null means we're using CellMeasurer and haven't yet measured a given index.
if (size === undefined || size !== size) {
if (size === undefined || isNaN(size)) {
throw Error(`Invalid size returned for cell ${i} of value ${size}`);
} else if (size !== null) {
vector.setItemSize(i, size);
} else if (size === null) {
this._cellSizeAndPositionData[i] = {
offset,
size: 0,
};

this._lastBatchedIndex = index;
} else {
this._cellSizeAndPositionData[i] = {
offset,
size,
};

offset += size;

this._lastMeasuredIndex = index;
}
}
this._lastMeasuredIndex = Math.min(index, this._cellCount - 1);
}

return {
offset: vector.start(index),
size: vector.getItemSize(index),
};
return this._cellSizeAndPositionData[index];
}

getSizeAndPositionOfLastMeasuredCell(): SizeAndPositionData {
const index = this._lastMeasuredIndex;
if (index <= 0) {
return {
offset: 0,
size: 0,
};
}
const vector = this._layoutVector;
return {
offset: vector.start(index),
size: vector.getItemSize(index),
};
return this._lastMeasuredIndex >= 0
? this._cellSizeAndPositionData[this._lastMeasuredIndex]
: {
offset: 0,
size: 0,
};
}

/**
Expand All @@ -143,8 +148,14 @@ export default class CellSizeAndPositionManager {
* As cells are measured, the estimate will be updated.
*/
getTotalSize(): number {
const lastIndex = this._cellCount - 1;
return lastIndex >= 0 ? this._layoutVector.end(lastIndex) : 0;
const lastMeasuredCellSizeAndPosition = this.getSizeAndPositionOfLastMeasuredCell();
const totalSizeOfMeasuredCells =
lastMeasuredCellSizeAndPosition.offset +
lastMeasuredCellSizeAndPosition.size;
const numUnmeasuredCells = this._cellCount - this._lastMeasuredIndex - 1;
const totalSizeOfUnmeasuredCells =
numUnmeasuredCells * this._estimatedCellSize;
return totalSizeOfMeasuredCells + totalSizeOfUnmeasuredCells;
}

/**
Expand Down Expand Up @@ -195,15 +206,31 @@ export default class CellSizeAndPositionManager {
}

getVisibleCellRange(params: GetVisibleCellRangeParams): VisibleCellRange {
if (this.getTotalSize() === 0) {
let {containerSize, offset} = params;

const totalSize = this.getTotalSize();

if (totalSize === 0) {
return {};
}

const {containerSize, offset} = params;
const maxOffset = offset + containerSize - 1;
const maxOffset = offset + containerSize;
const start = this._findNearestCell(offset);

const datum = this.getSizeAndPositionOfCell(start);
offset = datum.offset + datum.size;

let stop = start;

while (offset < maxOffset && stop < this._cellCount - 1) {
stop++;

offset += this.getSizeAndPositionOfCell(stop).size;
}

return {
start: this._findNearestCell(offset),
stop: this._findNearestCell(maxOffset),
start,
stop,
};
}

Expand All @@ -216,6 +243,45 @@ export default class CellSizeAndPositionManager {
this._lastMeasuredIndex = Math.min(this._lastMeasuredIndex, index - 1);
}

_binarySearch(high: number, low: number, offset: number): number {
while (low <= high) {
const middle = low + Math.floor((high - low) / 2);
const currentOffset = this.getSizeAndPositionOfCell(middle).offset;

if (currentOffset === offset) {
return middle;
} else if (currentOffset < offset) {
low = middle + 1;
} else if (currentOffset > offset) {
high = middle - 1;
}
}

if (low > 0) {
return low - 1;
} else {
return 0;
}
}

_exponentialSearch(index: number, offset: number): number {
let interval = 1;

while (
index < this._cellCount &&
this.getSizeAndPositionOfCell(index).offset < offset
) {
index += interval;
interval *= 2;
}

return this._binarySearch(
Math.min(index, this._cellCount - 1),
Math.floor(index / 2),
offset,
);
}

/**
* Searches for the cell (index) nearest the specified offset.
*
Expand All @@ -227,35 +293,21 @@ export default class CellSizeAndPositionManager {
throw Error(`Invalid offset ${offset} specified`);
}

const vector = this._layoutVector;
const lastIndex = this._cellCount - 1;
// Our search algorithms find the nearest match at or below the specified offset.
// So make sure the offset is at least 0 or no match will be found.
let targetOffset = Math.max(0, Math.min(offset, vector.start(lastIndex)));
// First interrogate the constant-time lookup table
let nearestCellIndex = vector.indexOf(targetOffset);

// If we haven't yet measured this high, compute sizes for each cell up to the desired offset.
while (nearestCellIndex > this._lastMeasuredIndex) {
// Measure all the cells up to the one we want to find presently.
// Do this before the last-index check to ensure the sparse array
// is fully populated.
this.getSizeAndPositionOfCell(nearestCellIndex);
// No need to search and compare again if we're at the end.
if (nearestCellIndex === lastIndex) {
return nearestCellIndex;
}
nearestCellIndex = vector.indexOf(targetOffset);
// Guard in case `getSizeAndPositionOfCell` didn't fully measure to
// the nearestCellIndex. This might happen scrolling quickly down
// and back up on large lists -- possible race with React or DOM?
if (nearestCellIndex === -1) {
nearestCellIndex = this._lastMeasuredIndex;
this._lastMeasuredIndex = nearestCellIndex - 1;
targetOffset = Math.max(0, Math.min(offset, vector.start(lastIndex)));
}
offset = Math.max(0, offset);

const lastMeasuredCellSizeAndPosition = this.getSizeAndPositionOfLastMeasuredCell();
const lastMeasuredIndex = Math.max(0, this._lastMeasuredIndex);

if (lastMeasuredCellSizeAndPosition.offset >= offset) {
// If we've already measured cells within this range just use a binary search as it's faster.
return this._binarySearch(lastMeasuredIndex, 0, offset);
} else {
// If we haven't yet measured this high, fallback to an exponential search with an inner binary search.
// The exponential search avoids pre-computing sizes for the full set of cells as a binary search would.
// The overall complexity for this approach is O(log n).
return this._exponentialSearch(lastMeasuredIndex, offset);
}

return nearestCellIndex;
}
}
5 changes: 0 additions & 5 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5641,11 +5641,6 @@ levn@^0.3.0, levn@~0.3.0:
prelude-ls "~1.1.2"
type-check "~0.3.2"

linear-layout-vector@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/linear-layout-vector/-/linear-layout-vector-0.0.1.tgz#398114d7303b6ecc7fd6b273af7b8401d8ba9c70"
integrity sha1-OYEU1zA7bsx/1rJzr3uEAdi6nHA=

lint-staged@^7.0.4:
version "7.0.4"
resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-7.0.4.tgz#1aa7f27427e4c4c85d4d6524ac98aac10cbaf1b8"
Expand Down

0 comments on commit 917a9a3

Please sign in to comment.