Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Revert "Use sparse array for cell position caches" #1382

Merged
merged 1 commit into from
Jun 4, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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